elastic-apm 3.1.0 → 3.2.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.
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