elastic-apm 3.1.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ci/.jenkins_exclude.yml +47 -0
- data/.ci/.jenkins_framework.yml +4 -0
- data/.ci/.jenkins_master_framework.yml +1 -0
- data/.ci/.jenkins_ruby.yml +1 -0
- data/.ci/downstreamTests.groovy +1 -1
- data/.gitignore +2 -1
- data/.rspec +1 -0
- data/CHANGELOG.asciidoc +24 -0
- data/Dockerfile +43 -0
- data/Gemfile +34 -15
- data/README.md +30 -1
- data/bin/dev +54 -0
- data/bin/run-tests +27 -0
- data/docker-compose.yml +32 -0
- data/docs/api.asciidoc +13 -2
- data/docs/configuration.asciidoc +30 -0
- data/docs/getting-started-rack.asciidoc +24 -0
- data/docs/release-notes.asciidoc +1 -1
- data/lib/elastic_apm.rb +12 -1
- data/lib/elastic_apm/agent.rb +15 -3
- data/lib/elastic_apm/central_config.rb +39 -19
- data/lib/elastic_apm/child_durations.rb +42 -0
- data/lib/elastic_apm/config.rb +27 -11
- data/lib/elastic_apm/context/request/socket.rb +1 -1
- data/lib/elastic_apm/context_builder.rb +1 -1
- data/lib/elastic_apm/error.rb +10 -0
- data/lib/elastic_apm/error/exception.rb +7 -0
- data/lib/elastic_apm/grape.rb +48 -0
- data/lib/elastic_apm/instrumenter.rb +77 -4
- data/lib/elastic_apm/logging.rb +0 -2
- data/lib/elastic_apm/metrics.rb +39 -26
- data/lib/elastic_apm/metrics/breakdown_set.rb +14 -0
- data/lib/elastic_apm/metrics/{cpu_mem.rb → cpu_mem_set.rb} +62 -54
- data/lib/elastic_apm/metrics/metric.rb +117 -0
- data/lib/elastic_apm/metrics/set.rb +106 -0
- data/lib/elastic_apm/metrics/span_scoped_set.rb +39 -0
- data/lib/elastic_apm/metrics/transaction_set.rb +11 -0
- data/lib/elastic_apm/metrics/vm_set.rb +44 -0
- data/lib/elastic_apm/metricset.rb +31 -4
- data/lib/elastic_apm/normalizers.rb +6 -0
- data/lib/elastic_apm/normalizers/grape.rb +5 -0
- data/lib/elastic_apm/normalizers/grape/endpoint_run.rb +47 -0
- data/lib/elastic_apm/normalizers/rails/active_record.rb +16 -5
- data/lib/elastic_apm/opentracing.rb +4 -4
- data/lib/elastic_apm/rails.rb +12 -2
- data/lib/elastic_apm/railtie.rb +1 -5
- data/lib/elastic_apm/sinatra.rb +1 -1
- data/lib/elastic_apm/span.rb +15 -10
- data/lib/elastic_apm/spies.rb +0 -1
- data/lib/elastic_apm/sql_summarizer.rb +8 -6
- data/lib/elastic_apm/subscriber.rb +4 -1
- data/lib/elastic_apm/transaction.rb +6 -6
- data/lib/elastic_apm/transport/base.rb +7 -0
- data/lib/elastic_apm/transport/connection.rb +11 -69
- data/lib/elastic_apm/transport/connection/http.rb +43 -35
- data/lib/elastic_apm/transport/connection/proxy_pipe.rb +0 -3
- data/lib/elastic_apm/transport/headers.rb +62 -0
- data/lib/elastic_apm/transport/serializers.rb +0 -2
- data/lib/elastic_apm/transport/serializers/metricset_serializer.rb +19 -6
- data/lib/elastic_apm/transport/serializers/span_serializer.rb +3 -3
- data/lib/elastic_apm/transport/user_agent.rb +31 -0
- data/lib/elastic_apm/transport/worker.rb +1 -2
- data/lib/elastic_apm/version.rb +1 -1
- metadata +20 -6
- data/lib/elastic_apm/metrics/vm.rb +0 -60
- data/lib/elastic_apm/util/prefixed_logger.rb +0 -18
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ElasticAPM
|
4
|
+
module Metrics
|
5
|
+
# @api private
|
6
|
+
class Metric
|
7
|
+
def initialize(
|
8
|
+
key,
|
9
|
+
initial_value: nil,
|
10
|
+
tags: nil,
|
11
|
+
reset_on_collect: false
|
12
|
+
)
|
13
|
+
@key = key
|
14
|
+
@initial_value = initial_value
|
15
|
+
@value = initial_value
|
16
|
+
@tags = tags
|
17
|
+
@reset_on_collect = reset_on_collect
|
18
|
+
@mutex = Mutex.new
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :key, :initial_value, :tags, :value
|
22
|
+
|
23
|
+
def value=(value)
|
24
|
+
@mutex.synchronize { @value = value }
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset!
|
28
|
+
self.value = initial_value
|
29
|
+
end
|
30
|
+
|
31
|
+
def tags?
|
32
|
+
!!tags&.any?
|
33
|
+
end
|
34
|
+
|
35
|
+
def reset_on_collect?
|
36
|
+
@reset_on_collect
|
37
|
+
end
|
38
|
+
|
39
|
+
def collect
|
40
|
+
collected = value
|
41
|
+
|
42
|
+
self.value = initial_value if reset_on_collect?
|
43
|
+
|
44
|
+
return nil if reset_on_collect? && collected == 0
|
45
|
+
|
46
|
+
collected
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
class NoopMetric
|
52
|
+
def value; end
|
53
|
+
|
54
|
+
def value=(_); end
|
55
|
+
|
56
|
+
def collect; end
|
57
|
+
|
58
|
+
def reset!; end
|
59
|
+
|
60
|
+
def tags?; end
|
61
|
+
|
62
|
+
def reset_on_collect?; end
|
63
|
+
|
64
|
+
def inc!; end
|
65
|
+
|
66
|
+
def dec!; end
|
67
|
+
|
68
|
+
def update(_, delta: nil); end
|
69
|
+
end
|
70
|
+
|
71
|
+
# @api private
|
72
|
+
class Counter < Metric
|
73
|
+
def initialize(key, initial_value: 0, **args)
|
74
|
+
super(key, initial_value: initial_value, **args)
|
75
|
+
end
|
76
|
+
|
77
|
+
def inc!
|
78
|
+
@value += 1
|
79
|
+
end
|
80
|
+
|
81
|
+
def dec!
|
82
|
+
@value -= 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @api private
|
87
|
+
class Gauge < Metric
|
88
|
+
def initialize(key, **args)
|
89
|
+
super(key, initial_value: 0, **args)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @api private
|
94
|
+
class Timer < Metric
|
95
|
+
def initialize(key, **args)
|
96
|
+
super(key, initial_value: 0, **args)
|
97
|
+
@count = 0
|
98
|
+
end
|
99
|
+
|
100
|
+
attr_accessor :count
|
101
|
+
|
102
|
+
def update(duration, delta: 0)
|
103
|
+
@mutex.synchronize do
|
104
|
+
@value += duration
|
105
|
+
@count += delta
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def reset!
|
110
|
+
@mutex.synchronize do
|
111
|
+
@value = 0
|
112
|
+
@count = 0
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ElasticAPM
|
4
|
+
# @api private
|
5
|
+
module Metrics
|
6
|
+
NOOP = NoopMetric.new
|
7
|
+
|
8
|
+
# @api private
|
9
|
+
class Set
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
DISTINCT_LABEL_LIMIT = 1000
|
13
|
+
|
14
|
+
def initialize(config)
|
15
|
+
@config = config
|
16
|
+
@metrics = {}
|
17
|
+
@disabled = false
|
18
|
+
@lock = Mutex.new
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :metrics
|
22
|
+
|
23
|
+
def disable!
|
24
|
+
@disabled = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def disabled?
|
28
|
+
@disabled
|
29
|
+
end
|
30
|
+
|
31
|
+
def counter(key, tags: nil, **args)
|
32
|
+
metric(Counter, key, tags: tags, **args)
|
33
|
+
end
|
34
|
+
|
35
|
+
def gauge(key, tags: nil, **args)
|
36
|
+
metric(Gauge, key, tags: tags, **args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def timer(key, tags: nil, **args)
|
40
|
+
metric(Timer, key, tags: tags, **args)
|
41
|
+
end
|
42
|
+
|
43
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
44
|
+
def metric(kls, key, tags: nil, **args)
|
45
|
+
key = key_with_tags(key, tags)
|
46
|
+
return metrics[key] if metrics[key]
|
47
|
+
|
48
|
+
@lock.synchronize do
|
49
|
+
return metrics[key] if metrics[key]
|
50
|
+
|
51
|
+
metrics[key] =
|
52
|
+
if metrics.length < DISTINCT_LABEL_LIMIT
|
53
|
+
kls.new(key, tags: tags, **args)
|
54
|
+
else
|
55
|
+
unless @label_limit_logged
|
56
|
+
warn(
|
57
|
+
'The limit of %d metricsets has been reached, no new ' \
|
58
|
+
'metricsets will be created.', DISTINCT_LABEL_LIMIT
|
59
|
+
)
|
60
|
+
@label_limit_logged = true
|
61
|
+
end
|
62
|
+
|
63
|
+
NOOP
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
68
|
+
|
69
|
+
# rubocop:disable Metrics/MethodLength
|
70
|
+
def collect
|
71
|
+
return if disabled?
|
72
|
+
|
73
|
+
@lock.synchronize do
|
74
|
+
metrics.each_with_object({}) do |(key, metric), sets|
|
75
|
+
next unless (value = metric.collect)
|
76
|
+
|
77
|
+
# metrics have a key of name and flat array of key-value pairs
|
78
|
+
# eg [name, key, value, key, value]
|
79
|
+
# they can be sent in the same metricset but not if they have
|
80
|
+
# differing tags. So, we split the resulting sets by tags first.
|
81
|
+
name, *tags = key
|
82
|
+
sets[tags] ||= Metricset.new
|
83
|
+
|
84
|
+
# then we set the `samples` value for the metricset
|
85
|
+
set = sets[tags]
|
86
|
+
set.samples[name] = value
|
87
|
+
|
88
|
+
# and finally we copy the tags from the Metric to the Metricset
|
89
|
+
set.merge_tags! metric.tags
|
90
|
+
end.values
|
91
|
+
end
|
92
|
+
end
|
93
|
+
# rubocop:enable Metrics/MethodLength
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def key_with_tags(key, tags)
|
98
|
+
return key unless tags
|
99
|
+
|
100
|
+
tuple = tags.keys.zip(tags.values)
|
101
|
+
tuple.flatten!
|
102
|
+
tuple.unshift(key)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ElasticAPM
|
4
|
+
module Metrics
|
5
|
+
# @api private
|
6
|
+
class SpanScopedSet < Set
|
7
|
+
def collect
|
8
|
+
super.tap do |sets|
|
9
|
+
return unless sets
|
10
|
+
|
11
|
+
sets.each do |set|
|
12
|
+
move_transaction(set)
|
13
|
+
move_span(set)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def move_transaction(set)
|
21
|
+
name = set.tags&.delete(:'transaction.name')
|
22
|
+
type = set.tags&.delete(:'transaction.type')
|
23
|
+
return unless name || type
|
24
|
+
|
25
|
+
set.transaction = { name: name, type: type }
|
26
|
+
set.tags = nil if set.tags.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def move_span(set)
|
30
|
+
type = set.tags&.delete(:'span.type')
|
31
|
+
subtype = set.tags&.delete(:'span.subtype')
|
32
|
+
return unless type
|
33
|
+
|
34
|
+
set.span = { type: type, subtype: subtype }
|
35
|
+
set.tags = nil if set.tags.empty?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ElasticAPM
|
4
|
+
module Metrics
|
5
|
+
# @api private
|
6
|
+
class VMSet < Set
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
def collect
|
10
|
+
read!
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
15
|
+
def read!
|
16
|
+
return if disabled?
|
17
|
+
|
18
|
+
stat = GC.stat
|
19
|
+
|
20
|
+
gauge(:'ruby.gc.count').value = stat[:count]
|
21
|
+
gauge(:'ruby.threads').value = Thread.list.count
|
22
|
+
gauge(:'ruby.heap.slots.live').value = stat[:heap_live_slots]
|
23
|
+
|
24
|
+
gauge(:'ruby.heap.slots.free').value = stat[:heap_free_slots]
|
25
|
+
gauge(:'ruby.heap.allocations.total').value =
|
26
|
+
stat[:total_allocated_objects]
|
27
|
+
|
28
|
+
return unless GC::Profiler.enabled?
|
29
|
+
|
30
|
+
@total_time ||= 0
|
31
|
+
@total_time += GC::Profiler.total_time
|
32
|
+
GC::Profiler.clear
|
33
|
+
gauge(:'ruby.gc.time').value = @total_time
|
34
|
+
rescue TypeError => e
|
35
|
+
error 'VM metrics encountered error: %s', e
|
36
|
+
debug('Backtrace:') { e.backtrace.join("\n") }
|
37
|
+
|
38
|
+
disable!
|
39
|
+
end
|
40
|
+
|
41
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -3,17 +3,44 @@
|
|
3
3
|
module ElasticAPM
|
4
4
|
# @api private
|
5
5
|
class Metricset
|
6
|
-
def initialize(
|
6
|
+
def initialize(
|
7
|
+
timestamp: Util.micros,
|
8
|
+
tags: nil,
|
9
|
+
transaction: nil,
|
10
|
+
span: nil,
|
11
|
+
**samples
|
12
|
+
)
|
7
13
|
@timestamp = timestamp
|
8
|
-
@
|
14
|
+
@tags = tags
|
15
|
+
@transaction = transaction
|
16
|
+
@span = span
|
9
17
|
@samples = samples
|
10
18
|
end
|
11
19
|
|
12
|
-
attr_accessor :timestamp
|
13
|
-
attr_reader :samples
|
20
|
+
attr_accessor :timestamp, :transaction, :span, :tags
|
21
|
+
attr_reader :samples
|
22
|
+
|
23
|
+
def merge_tags!(tags)
|
24
|
+
return unless tags
|
25
|
+
|
26
|
+
@tags ||= {}
|
27
|
+
@tags.merge! tags
|
28
|
+
end
|
29
|
+
|
30
|
+
def tags?
|
31
|
+
tags&.any?
|
32
|
+
end
|
14
33
|
|
15
34
|
def empty?
|
16
35
|
samples.empty?
|
17
36
|
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
"<ElasticAPM::Metricset timestamp:#{timestamp}" \
|
40
|
+
" transaction:#{transaction.inspect}" \
|
41
|
+
" span:#{span.inspect}" \
|
42
|
+
" tags:#{tags.inspect}" \
|
43
|
+
" samples:#{samples.inspect}>"
|
44
|
+
end
|
18
45
|
end
|
19
46
|
end
|
@@ -12,6 +12,8 @@ module ElasticAPM # :nodoc:
|
|
12
12
|
def self.register(name)
|
13
13
|
Normalizers.register(name, self)
|
14
14
|
end
|
15
|
+
|
16
|
+
def backtrace(payload); end
|
15
17
|
end
|
16
18
|
|
17
19
|
def self.register(name, klass)
|
@@ -54,6 +56,10 @@ module ElasticAPM # :nodoc:
|
|
54
56
|
def normalize(transaction, name, payload)
|
55
57
|
self.for(name).normalize(transaction, name, payload)
|
56
58
|
end
|
59
|
+
|
60
|
+
def backtrace(name, payload)
|
61
|
+
self.for(name).backtrace(payload)
|
62
|
+
end
|
57
63
|
end
|
58
64
|
end
|
59
65
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ElasticAPM
|
4
|
+
module Normalizers
|
5
|
+
module Grape
|
6
|
+
# @api private
|
7
|
+
class EndpointRun < Normalizer
|
8
|
+
register 'endpoint_run.grape'
|
9
|
+
|
10
|
+
TYPE = 'app'
|
11
|
+
SUBTYPE = 'resource'
|
12
|
+
|
13
|
+
FRAMEWORK_NAME = 'Grape'
|
14
|
+
|
15
|
+
def normalize(transaction, _name, payload)
|
16
|
+
transaction.name = endpoint(payload[:env])
|
17
|
+
|
18
|
+
if transaction_from_host_app?(transaction)
|
19
|
+
transaction.context.set_service(
|
20
|
+
framework_name: FRAMEWORK_NAME,
|
21
|
+
framework_version: ::Grape::VERSION
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
[transaction.name, TYPE, SUBTYPE, nil, nil]
|
26
|
+
end
|
27
|
+
|
28
|
+
def backtrace(payload)
|
29
|
+
source_location = payload[:endpoint].source.source_location
|
30
|
+
["#{source_location[0]}:#{source_location[1]}"]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def transaction_from_host_app?(transaction)
|
36
|
+
transaction.config.framework_name != FRAMEWORK_NAME
|
37
|
+
end
|
38
|
+
|
39
|
+
def endpoint(env)
|
40
|
+
route_name = env['api.endpoint']&.routes&.first&.pattern&.origin ||
|
41
|
+
env['REQUEST_PATH']
|
42
|
+
[env['REQUEST_METHOD'], route_name].join(' ')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -11,31 +11,42 @@ module ElasticAPM
|
|
11
11
|
|
12
12
|
TYPE = 'db'
|
13
13
|
ACTION = 'sql'
|
14
|
+
SKIP_NAMES = %w[SCHEMA CACHE].freeze
|
15
|
+
UNKNOWN = 'unknown'
|
14
16
|
|
15
17
|
def initialize(*args)
|
16
18
|
super
|
17
19
|
|
18
|
-
@subtype = lookup_adapter || 'unknown'
|
19
20
|
@summarizer = SqlSummarizer.new
|
21
|
+
@adapters = {}
|
20
22
|
end
|
21
23
|
|
22
24
|
def normalize(_transaction, _name, payload)
|
23
|
-
return :skip if
|
25
|
+
return :skip if SKIP_NAMES.include?(payload[:name])
|
24
26
|
|
25
27
|
name = summarize(payload[:sql]) || payload[:name]
|
26
28
|
context =
|
27
29
|
Span::Context.new(db: { statement: payload[:sql], type: 'sql' })
|
28
|
-
[name, TYPE,
|
30
|
+
[name, TYPE, subtype(payload), ACTION, context]
|
29
31
|
end
|
30
32
|
|
31
33
|
private
|
32
34
|
|
35
|
+
def subtype(payload)
|
36
|
+
cached_adapter_name(
|
37
|
+
payload[:connection] ? payload[:connection].adapter_name :
|
38
|
+
::ActiveRecord::Base.connection_config[:adapter]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
33
42
|
def summarize(sql)
|
34
43
|
@summarizer.summarize(sql)
|
35
44
|
end
|
36
45
|
|
37
|
-
def
|
38
|
-
|
46
|
+
def cached_adapter_name(adapter_name)
|
47
|
+
return UNKNOWN if adapter_name.nil? || adapter_name.empty?
|
48
|
+
@adapters[adapter_name] ||
|
49
|
+
(@adapters[adapter_name] = adapter_name.downcase)
|
39
50
|
rescue StandardError
|
40
51
|
nil
|
41
52
|
end
|