elastic-apm 3.10.0 → 3.12.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.asciidoc +55 -0
  3. data/Gemfile +0 -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 +24 -6
  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 +2 -0
  11. data/lib/elastic_apm/instrumenter.rb +10 -3
  12. data/lib/elastic_apm/metadata.rb +3 -1
  13. data/lib/elastic_apm/metadata/cloud_info.rb +128 -0
  14. data/lib/elastic_apm/middleware.rb +8 -3
  15. data/lib/elastic_apm/span.rb +16 -1
  16. data/lib/elastic_apm/spies/delayed_job.rb +6 -2
  17. data/lib/elastic_apm/spies/elasticsearch.rb +8 -2
  18. data/lib/elastic_apm/spies/faraday.rb +1 -0
  19. data/lib/elastic_apm/spies/http.rb +1 -0
  20. data/lib/elastic_apm/spies/mongo.rb +6 -2
  21. data/lib/elastic_apm/spies/net_http.rb +1 -0
  22. data/lib/elastic_apm/spies/rake.rb +4 -2
  23. data/lib/elastic_apm/spies/resque.rb +4 -2
  24. data/lib/elastic_apm/spies/sequel.rb +10 -1
  25. data/lib/elastic_apm/spies/shoryuken.rb +2 -0
  26. data/lib/elastic_apm/spies/sidekiq.rb +2 -0
  27. data/lib/elastic_apm/spies/sneakers.rb +2 -0
  28. data/lib/elastic_apm/spies/sucker_punch.rb +2 -0
  29. data/lib/elastic_apm/trace_context.rb +1 -1
  30. data/lib/elastic_apm/trace_context/traceparent.rb +2 -4
  31. data/lib/elastic_apm/trace_context/tracestate.rb +112 -9
  32. data/lib/elastic_apm/transaction.rb +39 -6
  33. data/lib/elastic_apm/transport/connection.rb +1 -0
  34. data/lib/elastic_apm/transport/filters/hash_sanitizer.rb +14 -15
  35. data/lib/elastic_apm/transport/filters/secrets_filter.rb +10 -6
  36. data/lib/elastic_apm/transport/serializers.rb +8 -6
  37. data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +43 -3
  38. data/lib/elastic_apm/transport/serializers/span_serializer.rb +3 -1
  39. data/lib/elastic_apm/transport/serializers/transaction_serializer.rb +2 -0
  40. data/lib/elastic_apm/transport/user_agent.rb +3 -3
  41. data/lib/elastic_apm/transport/worker.rb +1 -0
  42. data/lib/elastic_apm/util.rb +2 -0
  43. data/lib/elastic_apm/util/deep_dup.rb +66 -0
  44. data/lib/elastic_apm/util/precision_validator.rb +46 -0
  45. data/lib/elastic_apm/version.rb +1 -1
  46. metadata +7 -3
@@ -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)
@@ -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,
@@ -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
@@ -47,7 +53,7 @@ module ElasticAPM
47
53
  if ElasticAPM.agent.config.capture_elasticsearch_queries
48
54
  unless args[1].nil? || args[1].empty?
49
55
  statement << {
50
- body: ElasticAPM::Spies::ElasticsearchSpy.sanitizer.strip_from!(args[1])
56
+ body: ElasticAPM::Spies::ElasticsearchSpy.sanitizer.strip_from(args[1])
51
57
  }
52
58
  end
53
59
  end
@@ -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