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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/.jenkins_exclude.yml +47 -0
  3. data/.ci/.jenkins_framework.yml +4 -0
  4. data/.ci/.jenkins_master_framework.yml +1 -0
  5. data/.ci/.jenkins_ruby.yml +1 -0
  6. data/.ci/downstreamTests.groovy +1 -1
  7. data/.gitignore +2 -1
  8. data/.rspec +1 -0
  9. data/CHANGELOG.asciidoc +24 -0
  10. data/Dockerfile +43 -0
  11. data/Gemfile +34 -15
  12. data/README.md +30 -1
  13. data/bin/dev +54 -0
  14. data/bin/run-tests +27 -0
  15. data/docker-compose.yml +32 -0
  16. data/docs/api.asciidoc +13 -2
  17. data/docs/configuration.asciidoc +30 -0
  18. data/docs/getting-started-rack.asciidoc +24 -0
  19. data/docs/release-notes.asciidoc +1 -1
  20. data/lib/elastic_apm.rb +12 -1
  21. data/lib/elastic_apm/agent.rb +15 -3
  22. data/lib/elastic_apm/central_config.rb +39 -19
  23. data/lib/elastic_apm/child_durations.rb +42 -0
  24. data/lib/elastic_apm/config.rb +27 -11
  25. data/lib/elastic_apm/context/request/socket.rb +1 -1
  26. data/lib/elastic_apm/context_builder.rb +1 -1
  27. data/lib/elastic_apm/error.rb +10 -0
  28. data/lib/elastic_apm/error/exception.rb +7 -0
  29. data/lib/elastic_apm/grape.rb +48 -0
  30. data/lib/elastic_apm/instrumenter.rb +77 -4
  31. data/lib/elastic_apm/logging.rb +0 -2
  32. data/lib/elastic_apm/metrics.rb +39 -26
  33. data/lib/elastic_apm/metrics/breakdown_set.rb +14 -0
  34. data/lib/elastic_apm/metrics/{cpu_mem.rb → cpu_mem_set.rb} +62 -54
  35. data/lib/elastic_apm/metrics/metric.rb +117 -0
  36. data/lib/elastic_apm/metrics/set.rb +106 -0
  37. data/lib/elastic_apm/metrics/span_scoped_set.rb +39 -0
  38. data/lib/elastic_apm/metrics/transaction_set.rb +11 -0
  39. data/lib/elastic_apm/metrics/vm_set.rb +44 -0
  40. data/lib/elastic_apm/metricset.rb +31 -4
  41. data/lib/elastic_apm/normalizers.rb +6 -0
  42. data/lib/elastic_apm/normalizers/grape.rb +5 -0
  43. data/lib/elastic_apm/normalizers/grape/endpoint_run.rb +47 -0
  44. data/lib/elastic_apm/normalizers/rails/active_record.rb +16 -5
  45. data/lib/elastic_apm/opentracing.rb +4 -4
  46. data/lib/elastic_apm/rails.rb +12 -2
  47. data/lib/elastic_apm/railtie.rb +1 -5
  48. data/lib/elastic_apm/sinatra.rb +1 -1
  49. data/lib/elastic_apm/span.rb +15 -10
  50. data/lib/elastic_apm/spies.rb +0 -1
  51. data/lib/elastic_apm/sql_summarizer.rb +8 -6
  52. data/lib/elastic_apm/subscriber.rb +4 -1
  53. data/lib/elastic_apm/transaction.rb +6 -6
  54. data/lib/elastic_apm/transport/base.rb +7 -0
  55. data/lib/elastic_apm/transport/connection.rb +11 -69
  56. data/lib/elastic_apm/transport/connection/http.rb +43 -35
  57. data/lib/elastic_apm/transport/connection/proxy_pipe.rb +0 -3
  58. data/lib/elastic_apm/transport/headers.rb +62 -0
  59. data/lib/elastic_apm/transport/serializers.rb +0 -2
  60. data/lib/elastic_apm/transport/serializers/metricset_serializer.rb +19 -6
  61. data/lib/elastic_apm/transport/serializers/span_serializer.rb +3 -3
  62. data/lib/elastic_apm/transport/user_agent.rb +31 -0
  63. data/lib/elastic_apm/transport/worker.rb +1 -2
  64. data/lib/elastic_apm/version.rb +1 -1
  65. metadata +20 -6
  66. data/lib/elastic_apm/metrics/vm.rb +0 -60
  67. 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ module Metrics
5
+ # @api private
6
+ class TransactionSet < SpanScopedSet
7
+ end
8
+ end
9
+ end
10
+
11
+
@@ -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(timestamp: Util.micros, labels: nil, **samples)
6
+ def initialize(
7
+ timestamp: Util.micros,
8
+ tags: nil,
9
+ transaction: nil,
10
+ span: nil,
11
+ **samples
12
+ )
7
13
  @timestamp = timestamp
8
- @labels = labels
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, :labels
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[endpoint_run].each do |lib|
4
+ require "elastic_apm/normalizers/grape/#{lib}"
5
+ 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 %w[SCHEMA CACHE].include?(payload[:name])
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, @subtype, ACTION, context]
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 lookup_adapter
38
- ::ActiveRecord::Base.connection.adapter_name.downcase
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