elastic-apm 3.9.0 → 3.12.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.asciidoc +66 -0
  3. data/Gemfile +1 -3
  4. data/docs/api.asciidoc +2 -1
  5. data/docs/configuration.asciidoc +1 -0
  6. data/lib/elastic_apm.rb +14 -2
  7. data/lib/elastic_apm/config.rb +31 -5
  8. data/lib/elastic_apm/config/options.rb +2 -1
  9. data/lib/elastic_apm/config/round_float.rb +31 -0
  10. data/lib/elastic_apm/config/wildcard_pattern_list.rb +13 -1
  11. data/lib/elastic_apm/context_builder.rb +1 -1
  12. data/lib/elastic_apm/instrumenter.rb +10 -3
  13. data/lib/elastic_apm/metadata.rb +3 -1
  14. data/lib/elastic_apm/metadata/cloud_info.rb +128 -0
  15. data/lib/elastic_apm/metadata/system_info.rb +5 -3
  16. data/lib/elastic_apm/metadata/system_info/container_info.rb +28 -4
  17. data/lib/elastic_apm/middleware.rb +16 -5
  18. data/lib/elastic_apm/span.rb +19 -3
  19. data/lib/elastic_apm/spies/delayed_job.rb +6 -2
  20. data/lib/elastic_apm/spies/elasticsearch.rb +7 -1
  21. data/lib/elastic_apm/spies/faraday.rb +1 -0
  22. data/lib/elastic_apm/spies/http.rb +1 -0
  23. data/lib/elastic_apm/spies/mongo.rb +6 -2
  24. data/lib/elastic_apm/spies/net_http.rb +1 -0
  25. data/lib/elastic_apm/spies/rake.rb +4 -2
  26. data/lib/elastic_apm/spies/resque.rb +4 -2
  27. data/lib/elastic_apm/spies/sequel.rb +10 -1
  28. data/lib/elastic_apm/spies/shoryuken.rb +2 -0
  29. data/lib/elastic_apm/spies/sidekiq.rb +2 -0
  30. data/lib/elastic_apm/spies/sneakers.rb +2 -0
  31. data/lib/elastic_apm/spies/sucker_punch.rb +2 -0
  32. data/lib/elastic_apm/trace_context.rb +1 -1
  33. data/lib/elastic_apm/trace_context/traceparent.rb +2 -4
  34. data/lib/elastic_apm/trace_context/tracestate.rb +112 -9
  35. data/lib/elastic_apm/transaction.rb +39 -6
  36. data/lib/elastic_apm/transport/connection.rb +1 -0
  37. data/lib/elastic_apm/transport/filters/hash_sanitizer.rb +9 -16
  38. data/lib/elastic_apm/transport/filters/secrets_filter.rb +8 -6
  39. data/lib/elastic_apm/transport/serializers.rb +8 -6
  40. data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +43 -3
  41. data/lib/elastic_apm/transport/serializers/span_serializer.rb +3 -1
  42. data/lib/elastic_apm/transport/serializers/transaction_serializer.rb +2 -0
  43. data/lib/elastic_apm/transport/user_agent.rb +3 -3
  44. data/lib/elastic_apm/transport/worker.rb +5 -0
  45. data/lib/elastic_apm/util.rb +2 -0
  46. data/lib/elastic_apm/util/precision_validator.rb +46 -0
  47. data/lib/elastic_apm/version.rb +1 -1
  48. metadata +6 -3
@@ -24,7 +24,7 @@ module ElasticAPM
24
24
  def initialize(config)
25
25
  @config = config
26
26
 
27
- @hostname = @config.hostname || `hostname`.chomp
27
+ @hostname = @config.hostname || self.class.system_hostname
28
28
  @architecture = gem_platform.cpu
29
29
  @platform = gem_platform.os
30
30
 
@@ -35,11 +35,13 @@ module ElasticAPM
35
35
 
36
36
  attr_reader :hostname, :architecture, :platform, :container, :kubernetes
37
37
 
38
- private
39
-
40
38
  def gem_platform
41
39
  @gem_platform ||= Gem::Platform.local
42
40
  end
41
+
42
+ def self.system_hostname
43
+ @system_hostname ||= `hostname`.chomp
44
+ end
43
45
  end
44
46
  end
45
47
  end
@@ -81,8 +81,14 @@ module ElasticAPM
81
81
  ENV.fetch('KUBERNETES_POD_UID', kubernetes_pod_uid)
82
82
  end
83
83
 
84
- CONTAINER_ID_REGEX = /^[0-9A-Fa-f]{64}$/.freeze
85
- KUBEPODS_REGEX = %r{(?:^/kubepods/[^/]+/pod([^/]+)$)|(?:^/kubepods\.slice/kubepods-[^/]+\.slice/kubepods-[^/]+-pod([^/]+)\.slice$)}.freeze # rubocop:disable Metrics/LineLength
84
+ CONTAINER_ID_REGEXES = [
85
+ %r{^[[:xdigit:]]{64}$},
86
+ %r{^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4,}$}
87
+ ]
88
+ KUBEPODS_REGEXES = [
89
+ %r{(?:^/kubepods[^\s]*/pod([^/]+)$)},
90
+ %r{(?:^/kubepods\.slice/kubepods-[^/]+\.slice/kubepods-[^/]+-pod([^/]+)\.slice$)}
91
+ ]
86
92
  SYSTEMD_SCOPE_SUFFIX = '.scope'
87
93
 
88
94
  # rubocop:disable Metrics/PerceivedComplexity
@@ -118,18 +124,36 @@ module ElasticAPM
118
124
  end
119
125
  end
120
126
 
121
- if (kubepods_match = KUBEPODS_REGEX.match(directory))
127
+ if (kubepods_match = match_kubepods(directory))
122
128
  pod_id = kubepods_match[1] || kubepods_match[2]
123
129
 
124
130
  self.container_id = container_id
125
131
  self.kubernetes_pod_uid = pod_id
126
- elsif CONTAINER_ID_REGEX.match(container_id)
132
+ elsif match_container(container_id)
127
133
  self.container_id = container_id
128
134
  end
129
135
  end
130
136
  end
131
137
  # rubocop:enable Metrics/PerceivedComplexity
132
138
  # rubocop:enable Metrics/CyclomaticComplexity
139
+
140
+ def match_kubepods(directory)
141
+ KUBEPODS_REGEXES.each do |r|
142
+ next unless (match = r.match(directory))
143
+ return match
144
+ end
145
+
146
+ nil
147
+ end
148
+
149
+ def match_container(container_id)
150
+ CONTAINER_ID_REGEXES.each do |r|
151
+ next unless (match = r.match(container_id))
152
+ return match
153
+ end
154
+
155
+ nil
156
+ end
133
157
  end
134
158
  end
135
159
  end
@@ -41,9 +41,14 @@ module ElasticAPM
41
41
  ElasticAPM.report(e, context: context, handled: false)
42
42
  raise
43
43
  ensure
44
- if resp && transaction
45
- status, headers, _body = resp
46
- transaction.add_response(status, headers: headers.dup)
44
+ if transaction
45
+ if resp
46
+ status, headers, _body = resp
47
+ transaction.add_response(status, headers: headers.dup)
48
+ transaction&.outcome = Transaction::Outcome.from_http_status(status)
49
+ else
50
+ transaction&.outcome = Transaction::Outcome::FAILURE
51
+ end
47
52
  end
48
53
 
49
54
  ElasticAPM.end_transaction http_result(status)
@@ -59,9 +64,15 @@ module ElasticAPM
59
64
  end
60
65
 
61
66
  def path_ignored?(env)
62
- config.ignore_url_patterns.any? do |r|
63
- env['PATH_INFO'].match r
67
+ return true if config.ignore_url_patterns.any? do |r|
68
+ r.match(env['PATH_INFO'])
64
69
  end
70
+
71
+ return true if config.transaction_ignore_urls.any? do |r|
72
+ r.match(env['PATH_INFO'])
73
+ end
74
+
75
+ false
65
76
  end
66
77
 
67
78
  def start_transaction(env)
@@ -25,6 +25,17 @@ module ElasticAPM
25
25
  extend Forwardable
26
26
  include ChildDurations::Methods
27
27
 
28
+ # @api private
29
+ class Outcome
30
+ FAILURE = "failure"
31
+ SUCCESS = "success"
32
+ UNKNOWN = "unknown"
33
+
34
+ def self.from_http_status(code)
35
+ code.to_i >= 400 ? FAILURE : SUCCESS
36
+ end
37
+ end
38
+
28
39
  DEFAULT_TYPE = 'custom'
29
40
 
30
41
  # rubocop:disable Metrics/ParameterLists
@@ -38,7 +49,8 @@ module ElasticAPM
38
49
  action: nil,
39
50
  context: nil,
40
51
  stacktrace_builder: nil,
41
- sync: nil
52
+ sync: nil,
53
+ sample_rate: nil
42
54
  )
43
55
  @name = name
44
56
 
@@ -53,6 +65,7 @@ module ElasticAPM
53
65
  @transaction = transaction
54
66
  @parent = parent
55
67
  @trace_context = trace_context || parent.trace_context.child
68
+ @sample_rate = transaction.sample_rate
56
69
 
57
70
  @context = context || Span::Context.new(sync: sync)
58
71
  @stacktrace_builder = stacktrace_builder
@@ -65,6 +78,7 @@ module ElasticAPM
65
78
  :action,
66
79
  :name,
67
80
  :original_backtrace,
81
+ :outcome,
68
82
  :subtype,
69
83
  :trace_context,
70
84
  :type
@@ -73,6 +87,7 @@ module ElasticAPM
73
87
  :context,
74
88
  :duration,
75
89
  :parent,
90
+ :sample_rate,
76
91
  :self_time,
77
92
  :stacktrace,
78
93
  :timestamp,
@@ -97,11 +112,12 @@ module ElasticAPM
97
112
 
98
113
  def done(clock_end: Util.monotonic_micros)
99
114
  stop clock_end
115
+ self
116
+ end
100
117
 
118
+ def prepare_for_serialization!
101
119
  build_stacktrace! if should_build_stacktrace?
102
120
  self.original_backtrace = nil # release original
103
-
104
- self
105
121
  end
106
122
 
107
123
  def stopped?
@@ -41,10 +41,12 @@ module ElasticAPM
41
41
  job_name = name_from_payload(job.payload_object)
42
42
  transaction = ElasticAPM.start_transaction(job_name, TYPE)
43
43
  job.invoke_job_without_apm(*args, &block)
44
- transaction.done 'success'
44
+ transaction&.done 'success'
45
+ transaction&.outcome = Transaction::Outcome::SUCCESS
45
46
  rescue ::Exception => e
46
47
  ElasticAPM.report(e, handled: false)
47
- transaction.done 'error'
48
+ transaction&.done 'error'
49
+ transaction&.outcome = Transaction::Outcome::FAILURE
48
50
  raise
49
51
  ensure
50
52
  ElasticAPM.end_transaction
@@ -53,6 +55,8 @@ module ElasticAPM
53
55
  def self.name_from_payload(payload_object)
54
56
  if payload_object.is_a?(::Delayed::PerformableMethod)
55
57
  performable_method_name(payload_object)
58
+ elsif payload_object.class.name == 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'
59
+ payload_object.job_data['job_class']
56
60
  else
57
61
  payload_object.class.name
58
62
  end
@@ -27,7 +27,13 @@ module ElasticAPM
27
27
  SUBTYPE = 'elasticsearch'
28
28
 
29
29
  def self.sanitizer
30
- @sanitizer ||= ElasticAPM::Transport::Filters::HashSanitizer.new
30
+ @sanitizer ||=
31
+ begin
32
+ config = ElasticAPM.agent.config
33
+ ElasticAPM::Transport::Filters::HashSanitizer.new(
34
+ key_patterns: config.custom_key_filters + config.sanitize_field_names
35
+ )
36
+ end
31
37
  end
32
38
 
33
39
  def install
@@ -89,6 +89,7 @@ module ElasticAPM
89
89
  http.status_code = result.status.to_s
90
90
  end
91
91
 
92
+ span&.outcome = Span::Outcome.from_http_status(result.status)
92
93
  result
93
94
  end
94
95
  end
@@ -61,6 +61,7 @@ module ElasticAPM
61
61
  http.status_code = result.status.to_s
62
62
  end
63
63
 
64
+ span&.outcome = Span::Outcome.from_http_status(result.status)
64
65
  result
65
66
  end
66
67
  end
@@ -44,11 +44,15 @@ module ElasticAPM
44
44
  end
45
45
 
46
46
  def failed(event)
47
- pop_event(event)
47
+ span = pop_event(event)
48
+ span&.outcome = Span::Outcome::FAILURE
49
+ span
48
50
  end
49
51
 
50
52
  def succeeded(event)
51
- pop_event(event)
53
+ span = pop_event(event)
54
+ span&.outcome = Span::Outcome::SUCCESS
55
+ span
52
56
  end
53
57
 
54
58
  private
@@ -96,6 +96,7 @@ module ElasticAPM
96
96
  http.status_code = result.code
97
97
  end
98
98
 
99
+ span&.outcome = Span::Outcome.from_http_status(result.code)
99
100
  result
100
101
  end
101
102
  end
@@ -39,9 +39,11 @@ module ElasticAPM
39
39
  begin
40
40
  result = execute_without_apm(*args)
41
41
 
42
- transaction.result = 'success' if transaction
42
+ transaction&.result = 'success'
43
+ transaction&.outcome = Transaction::Outcome::SUCCESS
43
44
  rescue StandardError => e
44
- transaction.result = 'error' if transaction
45
+ transaction&.result = 'error'
46
+ transaction&.outcome = Transaction::Outcome::FAILURE
45
47
  ElasticAPM.report(e)
46
48
 
47
49
  raise
@@ -36,10 +36,12 @@ module ElasticAPM
36
36
  name = @payload && @payload['class']&.to_s
37
37
  transaction = ElasticAPM.start_transaction(name, TYPE)
38
38
  perform_without_elastic_apm
39
- transaction.done 'success'
39
+ transaction&.done 'success'
40
+ transaction&.outcome = Transaction::Outcome::SUCCESS
40
41
  rescue ::Exception => e
41
42
  ElasticAPM.report(e, handled: false)
42
- transaction.done 'error' if transaction
43
+ transaction&.done 'error'
44
+ transaction&.outcome = Transaction::Outcome::FAILURE
43
45
  raise
44
46
  ensure
45
47
  ElasticAPM.end_transaction
@@ -61,7 +61,12 @@ module ElasticAPM
61
61
  action: ACTION,
62
62
  context: context
63
63
  )
64
- yield.tap do |result|
64
+ log_connection_yield_without_apm(
65
+ sql,
66
+ connection,
67
+ args,
68
+ &block
69
+ ).tap do |result|
65
70
  if name =~ /^(UPDATE|DELETE)/
66
71
  if connection.respond_to?(:changes)
67
72
  span.context.db.rows_affected = connection.changes
@@ -70,7 +75,11 @@ module ElasticAPM
70
75
  end
71
76
  end
72
77
  end
78
+ rescue
79
+ span&.outcome = Span::Outcome::FAILURE
80
+ raise
73
81
  ensure
82
+ span&.outcome ||= Span::Outcome::SUCCESS
74
83
  ElasticAPM.end_span
75
84
  end
76
85
  end
@@ -37,9 +37,11 @@ module ElasticAPM
37
37
  yield
38
38
 
39
39
  transaction&.done :success
40
+ transaction&.outcome = Transaction::Outcome::SUCCESS
40
41
  rescue ::Exception => e
41
42
  ElasticAPM.report(e, handled: false)
42
43
  transaction&.done :error
44
+ transaction&.outcome = Transaction::Outcome::FAILURE
43
45
  raise
44
46
  ensure
45
47
  ElasticAPM.end_transaction
@@ -35,9 +35,11 @@ module ElasticAPM
35
35
  yield
36
36
 
37
37
  transaction&.done :success
38
+ transaction&.outcome = Transaction::Outcome::SUCCESS
38
39
  rescue ::Exception => e
39
40
  ElasticAPM.report(e, handled: false)
40
41
  transaction&.done :error
42
+ transaction&.outcome = Transaction::Outcome::FAILURE
41
43
  raise
42
44
  ensure
43
45
  ElasticAPM.end_transaction
@@ -57,11 +57,13 @@ module ElasticAPM
57
57
 
58
58
  res = @app.call(deserialized_msg, delivery_info, metadata, handler)
59
59
  transaction&.done(:success)
60
+ transaction&.outcome = Transaction::Outcome::SUCCESS
60
61
 
61
62
  res
62
63
  rescue ::Exception => e
63
64
  ElasticAPM.report(e, handled: false)
64
65
  transaction&.done(:error)
66
+ transaction&.outcome = Transaction::Outcome::FAILURE
65
67
  raise
66
68
  ensure
67
69
  ElasticAPM.end_transaction
@@ -35,12 +35,14 @@ module ElasticAPM
35
35
  transaction = ElasticAPM.start_transaction(name, TYPE)
36
36
  __run_perform_without_elastic_apm(*args)
37
37
  transaction.done 'success'
38
+ transaction&.outcome = Transaction::Outcome::SUCCESS
38
39
  rescue ::Exception => e
39
40
  # Note that SuckerPunch by default doesn't raise the errors from
40
41
  # the user-defined JobClass#perform method as it uses an error
41
42
  # handler, accessed via `SuckerPunch.exception_handler`.
42
43
  ElasticAPM.report(e, handled: false)
43
44
  transaction.done 'error'
45
+ transaction&.outcome = Transaction::Outcome::FAILURE
44
46
  raise
45
47
  ensure
46
48
  ElasticAPM.end_transaction
@@ -33,7 +33,7 @@ module ElasticAPM
33
33
  **legacy_traceparent_attrs
34
34
  )
35
35
  @traceparent = traceparent || Traceparent.new(**legacy_traceparent_attrs)
36
- @tracestate = tracestate
36
+ @tracestate = tracestate || Tracestate.new
37
37
  end
38
38
 
39
39
  attr_accessor :traceparent, :tracestate
@@ -33,8 +33,7 @@ module ElasticAPM
33
33
  trace_id: nil,
34
34
  span_id: nil,
35
35
  id: nil,
36
- recorded: true,
37
- tracestate: nil
36
+ recorded: true
38
37
  )
39
38
  @version = version
40
39
  @trace_id = trace_id || hex(TRACE_ID_LENGTH)
@@ -42,11 +41,10 @@ module ElasticAPM
42
41
  @parent_id = span_id
43
42
  @id = id || hex(ID_LENGTH)
44
43
  @recorded = recorded
45
- @tracestate = tracestate
46
44
  end
47
45
  # rubocop:enable Metrics/ParameterLists
48
46
 
49
- attr_accessor :version, :id, :trace_id, :parent_id, :recorded, :tracestate
47
+ attr_accessor :version, :id, :trace_id, :parent_id, :recorded
50
48
 
51
49
  alias :recorded? :recorded
52
50
 
@@ -17,26 +17,129 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
+ require 'elastic_apm/util/precision_validator'
21
+
20
22
  module ElasticAPM
21
23
  class TraceContext
22
24
  # @api private
23
25
  class Tracestate
24
- def initialize(values = [])
25
- @values = values
26
+ # @api private
27
+ class Entry
28
+ def initialize(key, value)
29
+ @key = key
30
+ @value = value
31
+ end
32
+
33
+ attr_reader :key, :value
34
+
35
+ def to_s
36
+ "#{key}=#{value}"
37
+ end
26
38
  end
27
39
 
28
- attr_accessor :values
40
+ class EsEntry
41
+ ASSIGN = ':'
42
+ SPLIT = ';'
43
+
44
+ SHORT_TO_LONG = { 's' => 'sample_rate' }
45
+ LONG_TO_SHORT = { 'sample_rate' => 's' }
46
+
47
+ def initialize(values = nil)
48
+ parse(values)
49
+ end
50
+
51
+ attr_reader :sample_rate
52
+
53
+ def key
54
+ 'es'
55
+ end
56
+
57
+ def value
58
+ LONG_TO_SHORT.map do |l, s|
59
+ "#{s}#{ASSIGN}#{send(l)}"
60
+ end.join(SPLIT)
61
+ end
62
+
63
+ def empty?
64
+ !sample_rate
65
+ end
66
+
67
+ def sample_rate=(val)
68
+ @sample_rate = Util::PrecisionValidator.validate(
69
+ val, precision: 4, minimum: 0.0001
70
+ )
71
+ end
72
+
73
+ def to_s
74
+ return nil if empty?
75
+
76
+ "es=#{value}"
77
+ end
78
+
79
+ private
80
+
81
+ def parse(values)
82
+ return unless values
83
+
84
+ values.split(SPLIT).map do |kv|
85
+ k, v = kv.split(ASSIGN)
86
+ next unless SHORT_TO_LONG.keys.include?(k)
87
+ send("#{SHORT_TO_LONG[k]}=", v)
88
+ end
89
+ end
90
+ end
91
+
92
+ extend Forwardable
93
+
94
+ def initialize(entries: {}, sample_rate: nil)
95
+ @entries = entries
96
+
97
+ self.sample_rate = sample_rate if sample_rate
98
+ end
99
+
100
+ attr_accessor :entries
101
+
102
+ def_delegators :es_entry, :sample_rate, :sample_rate=
29
103
 
30
104
  def self.parse(header)
31
- # HTTP allows multiple headers with the same name, eg. multiple
32
- # Set-Cookie headers per response.
33
- # Rack handles this by joining the headers under the same key, separated
34
- # by newlines, see https://www.rubydoc.info/github/rack/rack/file/SPEC
35
- new(String(header).split("\n"))
105
+ entries =
106
+ split_by_nl_and_comma(header)
107
+ .each_with_object({}) do |entry, hsh|
108
+ k, v = entry.split('=')
109
+
110
+ hsh[k] =
111
+ case k
112
+ when 'es' then EsEntry.new(v)
113
+ else Entry.new(k, v)
114
+ end
115
+ end
116
+
117
+ new(entries: entries)
36
118
  end
37
119
 
38
120
  def to_header
39
- values.join(',')
121
+ return "" unless entries.any?
122
+
123
+ entries.values.map(&:to_s).join(',')
124
+ end
125
+
126
+ private
127
+
128
+ def es_entry
129
+ # lazy generate this so we only add it if necessary
130
+ entries['es'] ||= EsEntry.new
131
+ end
132
+
133
+ class << self
134
+ private
135
+
136
+ def split_by_nl_and_comma(str)
137
+ # HTTP allows multiple headers with the same name, eg. multiple
138
+ # Set-Cookie headers per response.
139
+ # Rack handles this by joining the headers under the same key, separated
140
+ # by newlines, see https://www.rubydoc.info/github/rack/rack/file/SPEC
141
+ String(str).split("\n").map { |s| s.split(',') }.flatten
142
+ end
40
143
  end
41
144
  end
42
145
  end