atatus 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +57 -0
- data/LICENSE +65 -0
- data/LICENSE-THIRD-PARTY +205 -0
- data/README.md +13 -0
- data/Rakefile +19 -0
- data/atatus.gemspec +36 -0
- data/atatus.yml +2 -0
- data/bench/.gitignore +2 -0
- data/bench/app.rb +53 -0
- data/bench/benchmark.rb +36 -0
- data/bench/report.rb +55 -0
- data/bench/rubyprof.rb +39 -0
- data/bench/stackprof.rb +23 -0
- data/bin/build_docs +5 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/with_framework +7 -0
- data/lib/atatus.rb +325 -0
- data/lib/atatus/agent.rb +260 -0
- data/lib/atatus/central_config.rb +141 -0
- data/lib/atatus/central_config/cache_control.rb +34 -0
- data/lib/atatus/collector/base.rb +329 -0
- data/lib/atatus/collector/builder.rb +317 -0
- data/lib/atatus/collector/transport.rb +72 -0
- data/lib/atatus/config.rb +248 -0
- data/lib/atatus/config/bytes.rb +25 -0
- data/lib/atatus/config/duration.rb +23 -0
- data/lib/atatus/config/options.rb +134 -0
- data/lib/atatus/config/regexp_list.rb +13 -0
- data/lib/atatus/context.rb +33 -0
- data/lib/atatus/context/request.rb +11 -0
- data/lib/atatus/context/request/socket.rb +19 -0
- data/lib/atatus/context/request/url.rb +42 -0
- data/lib/atatus/context/response.rb +22 -0
- data/lib/atatus/context/user.rb +42 -0
- data/lib/atatus/context_builder.rb +97 -0
- data/lib/atatus/deprecations.rb +22 -0
- data/lib/atatus/error.rb +22 -0
- data/lib/atatus/error/exception.rb +46 -0
- data/lib/atatus/error/log.rb +24 -0
- data/lib/atatus/error_builder.rb +76 -0
- data/lib/atatus/instrumenter.rb +224 -0
- data/lib/atatus/internal_error.rb +6 -0
- data/lib/atatus/logging.rb +55 -0
- data/lib/atatus/metadata.rb +19 -0
- data/lib/atatus/metadata/process_info.rb +18 -0
- data/lib/atatus/metadata/service_info.rb +61 -0
- data/lib/atatus/metadata/system_info.rb +35 -0
- data/lib/atatus/metadata/system_info/container_info.rb +121 -0
- data/lib/atatus/metadata/system_info/hw_info.rb +118 -0
- data/lib/atatus/metadata/system_info/os_info.rb +31 -0
- data/lib/atatus/metrics.rb +98 -0
- data/lib/atatus/metrics/cpu_mem.rb +240 -0
- data/lib/atatus/metrics/vm.rb +60 -0
- data/lib/atatus/metricset.rb +19 -0
- data/lib/atatus/middleware.rb +76 -0
- data/lib/atatus/naively_hashable.rb +21 -0
- data/lib/atatus/normalizers.rb +68 -0
- data/lib/atatus/normalizers/action_controller.rb +27 -0
- data/lib/atatus/normalizers/action_mailer.rb +26 -0
- data/lib/atatus/normalizers/action_view.rb +77 -0
- data/lib/atatus/normalizers/active_record.rb +45 -0
- data/lib/atatus/opentracing.rb +346 -0
- data/lib/atatus/rails.rb +61 -0
- data/lib/atatus/railtie.rb +30 -0
- data/lib/atatus/span.rb +125 -0
- data/lib/atatus/span/context.rb +40 -0
- data/lib/atatus/span_helpers.rb +44 -0
- data/lib/atatus/spies.rb +86 -0
- data/lib/atatus/spies/action_dispatch.rb +28 -0
- data/lib/atatus/spies/delayed_job.rb +68 -0
- data/lib/atatus/spies/elasticsearch.rb +36 -0
- data/lib/atatus/spies/faraday.rb +70 -0
- data/lib/atatus/spies/http.rb +44 -0
- data/lib/atatus/spies/json.rb +22 -0
- data/lib/atatus/spies/mongo.rb +87 -0
- data/lib/atatus/spies/net_http.rb +70 -0
- data/lib/atatus/spies/rake.rb +45 -0
- data/lib/atatus/spies/redis.rb +27 -0
- data/lib/atatus/spies/sequel.rb +47 -0
- data/lib/atatus/spies/sidekiq.rb +89 -0
- data/lib/atatus/spies/sinatra.rb +41 -0
- data/lib/atatus/spies/tilt.rb +27 -0
- data/lib/atatus/sql_summarizer.rb +35 -0
- data/lib/atatus/stacktrace.rb +16 -0
- data/lib/atatus/stacktrace/frame.rb +52 -0
- data/lib/atatus/stacktrace_builder.rb +104 -0
- data/lib/atatus/subscriber.rb +77 -0
- data/lib/atatus/trace_context.rb +85 -0
- data/lib/atatus/transaction.rb +100 -0
- data/lib/atatus/transport/base.rb +174 -0
- data/lib/atatus/transport/connection.rb +156 -0
- data/lib/atatus/transport/connection/http.rb +116 -0
- data/lib/atatus/transport/connection/proxy_pipe.rb +75 -0
- data/lib/atatus/transport/filters.rb +43 -0
- data/lib/atatus/transport/filters/secrets_filter.rb +74 -0
- data/lib/atatus/transport/serializers.rb +93 -0
- data/lib/atatus/transport/serializers/context_serializer.rb +85 -0
- data/lib/atatus/transport/serializers/error_serializer.rb +77 -0
- data/lib/atatus/transport/serializers/metadata_serializer.rb +70 -0
- data/lib/atatus/transport/serializers/metricset_serializer.rb +28 -0
- data/lib/atatus/transport/serializers/span_serializer.rb +80 -0
- data/lib/atatus/transport/serializers/transaction_serializer.rb +37 -0
- data/lib/atatus/transport/worker.rb +73 -0
- data/lib/atatus/util.rb +42 -0
- data/lib/atatus/util/inflector.rb +93 -0
- data/lib/atatus/util/lru_cache.rb +48 -0
- data/lib/atatus/util/prefixed_logger.rb +18 -0
- data/lib/atatus/util/throttle.rb +35 -0
- data/lib/atatus/version.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +190 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Atatus
|
|
4
|
+
module Transport
|
|
5
|
+
module Serializers
|
|
6
|
+
# @api private
|
|
7
|
+
class ErrorSerializer < Serializer
|
|
8
|
+
def context_serializer
|
|
9
|
+
@context_serializer ||= ContextSerializer.new(config)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
13
|
+
def build(error)
|
|
14
|
+
base = {
|
|
15
|
+
id: error.id,
|
|
16
|
+
transaction_id: error.transaction_id,
|
|
17
|
+
transaction: build_transaction(error.transaction),
|
|
18
|
+
trace_id: error.trace_id,
|
|
19
|
+
parent_id: error.parent_id,
|
|
20
|
+
|
|
21
|
+
culprit: keyword_field(error.culprit),
|
|
22
|
+
timestamp: error.timestamp
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (context = context_serializer.build(error.context))
|
|
26
|
+
base[:context] = context
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if (exception = error.exception)
|
|
30
|
+
base[:exception] = build_exception exception
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if (log = error.log)
|
|
34
|
+
base[:log] = build_log log
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{ error: base }
|
|
38
|
+
end
|
|
39
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_exception(exception)
|
|
44
|
+
{
|
|
45
|
+
message: exception.message,
|
|
46
|
+
type: keyword_field(exception.type),
|
|
47
|
+
module: keyword_field(exception.module),
|
|
48
|
+
code: keyword_field(exception.code),
|
|
49
|
+
attributes: exception.attributes,
|
|
50
|
+
stacktrace: exception.stacktrace.to_a,
|
|
51
|
+
handled: exception.handled,
|
|
52
|
+
cause: exception.cause && [build_exception(exception.cause)]
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_log(log)
|
|
57
|
+
{
|
|
58
|
+
message: log.message,
|
|
59
|
+
level: keyword_field(log.level),
|
|
60
|
+
logger_name: keyword_field(log.logger_name),
|
|
61
|
+
param_message: keyword_field(log.param_message),
|
|
62
|
+
stacktrace: log.stacktrace.to_a
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_transaction(transaction)
|
|
67
|
+
return unless transaction
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
sampled: transaction[:sampled],
|
|
71
|
+
type: keyword_field(transaction[:type])
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Atatus
|
|
4
|
+
module Transport
|
|
5
|
+
module Serializers
|
|
6
|
+
# @api private
|
|
7
|
+
class MetadataSerializer < Serializer
|
|
8
|
+
def build(metadata)
|
|
9
|
+
{
|
|
10
|
+
metadata: {
|
|
11
|
+
service: build_service(metadata.service),
|
|
12
|
+
process: build_process(metadata.process),
|
|
13
|
+
system: build_system(metadata.system),
|
|
14
|
+
labels: build_labels(metadata.labels)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
22
|
+
def build_service(service)
|
|
23
|
+
{
|
|
24
|
+
name: keyword_field(service.name),
|
|
25
|
+
environment: keyword_field(service.environment),
|
|
26
|
+
version: keyword_field(service.version),
|
|
27
|
+
agent: {
|
|
28
|
+
name: keyword_field(service.agent.name),
|
|
29
|
+
version: keyword_field(service.agent.version)
|
|
30
|
+
},
|
|
31
|
+
framework: {
|
|
32
|
+
name: keyword_field(service.framework.name),
|
|
33
|
+
version: keyword_field(service.framework.version)
|
|
34
|
+
},
|
|
35
|
+
language: {
|
|
36
|
+
name: keyword_field(service.language.name),
|
|
37
|
+
version: keyword_field(service.language.version)
|
|
38
|
+
},
|
|
39
|
+
runtime: {
|
|
40
|
+
name: keyword_field(service.runtime.name),
|
|
41
|
+
version: keyword_field(service.runtime.version)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
46
|
+
|
|
47
|
+
def build_process(process)
|
|
48
|
+
{
|
|
49
|
+
pid: process.pid,
|
|
50
|
+
title: keyword_field(process.title),
|
|
51
|
+
argv: process.argv
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_system(system)
|
|
56
|
+
{
|
|
57
|
+
hostname: keyword_field(system.hostname),
|
|
58
|
+
architecture: keyword_field(system.architecture),
|
|
59
|
+
platform: keyword_field(system.platform),
|
|
60
|
+
kubernetes: keyword_object(system.kubernetes)
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_labels(labels)
|
|
65
|
+
keyword_object(labels)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Atatus
|
|
4
|
+
module Transport
|
|
5
|
+
module Serializers
|
|
6
|
+
# @api private
|
|
7
|
+
class MetricsetSerializer < Serializer
|
|
8
|
+
def build(metricset)
|
|
9
|
+
{
|
|
10
|
+
metricset: {
|
|
11
|
+
timestamp: metricset.timestamp.to_i,
|
|
12
|
+
tags: keyword_object(metricset.labels),
|
|
13
|
+
samples: build_samples(metricset.samples)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def build_samples(samples)
|
|
21
|
+
samples.each_with_object({}) do |(key, value), hsh|
|
|
22
|
+
hsh[key] = { value: value }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Atatus
|
|
4
|
+
module Transport
|
|
5
|
+
module Serializers
|
|
6
|
+
# @api private
|
|
7
|
+
class SpanSerializer < Serializer
|
|
8
|
+
def initialize(config)
|
|
9
|
+
super
|
|
10
|
+
|
|
11
|
+
@context_serializer = ContextSerializer.new(config)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :context_serializer
|
|
15
|
+
|
|
16
|
+
# rubocop:disable Metrics/MethodLength
|
|
17
|
+
def build(span)
|
|
18
|
+
{
|
|
19
|
+
span: {
|
|
20
|
+
id: span.id,
|
|
21
|
+
transaction_id: span.transaction_id,
|
|
22
|
+
parent_id: span.parent_id,
|
|
23
|
+
name: keyword_field(span.name),
|
|
24
|
+
type: join_type(span),
|
|
25
|
+
duration: ms(span.duration),
|
|
26
|
+
context: context_serializer.build(span.context),
|
|
27
|
+
stacktrace: span.stacktrace.to_a,
|
|
28
|
+
timestamp: span.timestamp,
|
|
29
|
+
trace_id: span.trace_id
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
# rubocop:enable Metrics/MethodLength
|
|
34
|
+
|
|
35
|
+
# @api private
|
|
36
|
+
class ContextSerializer < Serializer
|
|
37
|
+
def build(context)
|
|
38
|
+
return unless context
|
|
39
|
+
|
|
40
|
+
{ sync: context.sync }.tap do |base|
|
|
41
|
+
base[:db] = build_db(context.db) if context.db
|
|
42
|
+
base[:http] = build_http(context.http) if context.http
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_db(db)
|
|
49
|
+
return unless db
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
instance: db.instance,
|
|
53
|
+
statement: Util.truncate(db.statement, max_length: 10_000),
|
|
54
|
+
type: db.type,
|
|
55
|
+
user: db.user
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_http(http)
|
|
60
|
+
return unless http
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
url: http.url,
|
|
64
|
+
status_code: http.status_code.to_i,
|
|
65
|
+
method: keyword_field(http.method)
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def join_type(span)
|
|
73
|
+
combined = [span.type, span.subtype, span.action]
|
|
74
|
+
combined.compact!
|
|
75
|
+
combined.join '.'
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Atatus
|
|
4
|
+
module Transport
|
|
5
|
+
module Serializers
|
|
6
|
+
# @api private
|
|
7
|
+
class TransactionSerializer < Serializer
|
|
8
|
+
def context_serializer
|
|
9
|
+
@context_serializer ||= ContextSerializer.new(config)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
13
|
+
def build(transaction)
|
|
14
|
+
{
|
|
15
|
+
transaction: {
|
|
16
|
+
id: transaction.id,
|
|
17
|
+
trace_id: transaction.trace_id,
|
|
18
|
+
parent_id: transaction.parent_id,
|
|
19
|
+
name: keyword_field(transaction.name),
|
|
20
|
+
type: keyword_field(transaction.type),
|
|
21
|
+
result: keyword_field(transaction.result.to_s),
|
|
22
|
+
duration: ms(transaction.duration),
|
|
23
|
+
timestamp: transaction.timestamp,
|
|
24
|
+
sampled: transaction.sampled?,
|
|
25
|
+
context: context_serializer.build(transaction.context),
|
|
26
|
+
span_count: {
|
|
27
|
+
started: transaction.started_spans,
|
|
28
|
+
dropped: transaction.dropped_spans
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Atatus
|
|
4
|
+
module Transport
|
|
5
|
+
# @api private
|
|
6
|
+
class Worker
|
|
7
|
+
include Logging
|
|
8
|
+
|
|
9
|
+
# @api private
|
|
10
|
+
class StopMessage; end
|
|
11
|
+
|
|
12
|
+
# @api private
|
|
13
|
+
class FlushMessage; end
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
config,
|
|
17
|
+
queue,
|
|
18
|
+
serializers:,
|
|
19
|
+
filters:,
|
|
20
|
+
conn_adapter: Connection
|
|
21
|
+
)
|
|
22
|
+
@config = config
|
|
23
|
+
@queue = queue
|
|
24
|
+
|
|
25
|
+
@serializers = serializers
|
|
26
|
+
@filters = filters
|
|
27
|
+
|
|
28
|
+
metadata = serializers.serialize(Metadata.new(config))
|
|
29
|
+
@connection = conn_adapter.new(config, metadata)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_reader :queue, :filters, :name, :connection, :serializers
|
|
33
|
+
|
|
34
|
+
# rubocop:disable Metrics/MethodLength
|
|
35
|
+
def work_forever
|
|
36
|
+
while (msg = queue.pop)
|
|
37
|
+
case msg
|
|
38
|
+
when StopMessage
|
|
39
|
+
debug 'Stopping worker -- %s', self
|
|
40
|
+
connection.flush(:halt)
|
|
41
|
+
break
|
|
42
|
+
else
|
|
43
|
+
process msg
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
rescue Exception => e
|
|
47
|
+
warn 'Worker died with exception: %s', e.inspect
|
|
48
|
+
debug e.backtrace.join("\n")
|
|
49
|
+
end
|
|
50
|
+
# rubocop:enable Metrics/MethodLength
|
|
51
|
+
|
|
52
|
+
def process(resource)
|
|
53
|
+
return unless (json = serialize_and_filter(resource))
|
|
54
|
+
connection.write(json)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def serialize_and_filter(resource)
|
|
60
|
+
serialized = serializers.serialize(resource)
|
|
61
|
+
|
|
62
|
+
# if a filter returns nil, it means skip the event
|
|
63
|
+
return nil if @filters.apply!(serialized) == Filters::SKIP
|
|
64
|
+
|
|
65
|
+
JSON.fast_generate(serialized)
|
|
66
|
+
rescue Exception
|
|
67
|
+
error format('Failed converting event to JSON: %s', resource.inspect)
|
|
68
|
+
error serialized.inspect
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/atatus/util.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Atatus
|
|
4
|
+
# @api private
|
|
5
|
+
module Util
|
|
6
|
+
|
|
7
|
+
def self.ms(micros)
|
|
8
|
+
micros.to_f / 1_000
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.micros(target = Time.now)
|
|
12
|
+
utc = target.utc
|
|
13
|
+
utc.to_i * 1_000_000 + utc.usec
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.monotonic_micros
|
|
17
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.git_sha
|
|
21
|
+
sha = `git rev-parse --verify HEAD 2>&1`.chomp
|
|
22
|
+
$? && $?.success? ? sha : nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.hex_to_bits(str)
|
|
26
|
+
str.hex.to_s(2).rjust(str.size * 4, '0')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.reverse_merge!(first, *others)
|
|
30
|
+
others.reduce(first) do |curr, other|
|
|
31
|
+
curr.merge!(other) { |_, _, new| new }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.truncate(value, max_length: 1024)
|
|
36
|
+
return unless value
|
|
37
|
+
return value if value.length <= max_length
|
|
38
|
+
|
|
39
|
+
value[0...(max_length - 1)] + '…'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Atatus
|
|
4
|
+
# rubocop:disable all
|
|
5
|
+
module Util
|
|
6
|
+
# From https://github.com/rails/rails/blob/v5.2.0/activesupport/lib/active_support/inflector/methods.rb#L254-L332
|
|
7
|
+
module Inflector
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# Tries to find a constant with the name specified in the argument string.
|
|
12
|
+
#
|
|
13
|
+
# constantize('Module') # => Module
|
|
14
|
+
# constantize('Foo::Bar') # => Foo::Bar
|
|
15
|
+
#
|
|
16
|
+
# The name is assumed to be the one of a top-level constant, no matter
|
|
17
|
+
# whether it starts with "::" or not. No lexical context is taken into
|
|
18
|
+
# account:
|
|
19
|
+
#
|
|
20
|
+
# C = 'outside'
|
|
21
|
+
# module M
|
|
22
|
+
# C = 'inside'
|
|
23
|
+
# C # => 'inside'
|
|
24
|
+
# constantize('C') # => 'outside', same as ::C
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# NameError is raised when the name is not in CamelCase or the constant is
|
|
28
|
+
# unknown.
|
|
29
|
+
def constantize(camel_cased_word)
|
|
30
|
+
names = camel_cased_word.split("::".freeze)
|
|
31
|
+
|
|
32
|
+
# Trigger a built-in NameError exception including the ill-formed constant in the message.
|
|
33
|
+
Object.const_get(camel_cased_word) if names.empty?
|
|
34
|
+
|
|
35
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
|
36
|
+
names.shift if names.size > 1 && names.first.empty?
|
|
37
|
+
|
|
38
|
+
names.inject(Object) do |constant, name|
|
|
39
|
+
if constant == Object
|
|
40
|
+
constant.const_get(name)
|
|
41
|
+
else
|
|
42
|
+
candidate = constant.const_get(name)
|
|
43
|
+
next candidate if constant.const_defined?(name, false)
|
|
44
|
+
next candidate unless Object.const_defined?(name)
|
|
45
|
+
|
|
46
|
+
# Go down the ancestors to check if it is owned directly. The check
|
|
47
|
+
# stops when we reach Object or the end of ancestors tree.
|
|
48
|
+
constant = constant.ancestors.inject(constant) do |const, ancestor|
|
|
49
|
+
break const if ancestor == Object
|
|
50
|
+
break ancestor if ancestor.const_defined?(name, false)
|
|
51
|
+
const
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# owner is in Object, so raise
|
|
55
|
+
constant.const_get(name, false)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Tries to find a constant with the name specified in the argument string.
|
|
61
|
+
#
|
|
62
|
+
# safe_constantize('Module') # => Module
|
|
63
|
+
# safe_constantize('Foo::Bar') # => Foo::Bar
|
|
64
|
+
#
|
|
65
|
+
# The name is assumed to be the one of a top-level constant, no matter
|
|
66
|
+
# whether it starts with "::" or not. No lexical context is taken into
|
|
67
|
+
# account:
|
|
68
|
+
#
|
|
69
|
+
# C = 'outside'
|
|
70
|
+
# module M
|
|
71
|
+
# C = 'inside'
|
|
72
|
+
# C # => 'inside'
|
|
73
|
+
# safe_constantize('C') # => 'outside', same as ::C
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# +nil+ is returned when the name is not in CamelCase or the constant (or
|
|
77
|
+
# part of it) is unknown.
|
|
78
|
+
#
|
|
79
|
+
# safe_constantize('blargle') # => nil
|
|
80
|
+
# safe_constantize('UnknownModule') # => nil
|
|
81
|
+
# safe_constantize('UnknownModule::Foo::Bar') # => nil
|
|
82
|
+
def safe_constantize(camel_cased_word)
|
|
83
|
+
constantize(camel_cased_word)
|
|
84
|
+
rescue NameError => e
|
|
85
|
+
raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
|
|
86
|
+
e.name.to_s == camel_cased_word.to_s)
|
|
87
|
+
rescue ArgumentError => e
|
|
88
|
+
raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match(e.message)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
# rubocop:enable all
|
|
93
|
+
end
|