elastic-apm 3.10.1 → 3.13.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/labeler-config.yml +3 -0
  3. data/.github/workflows/labeler.yml +16 -0
  4. data/.rubocop.yml +33 -4
  5. data/CHANGELOG.asciidoc +67 -0
  6. data/Gemfile +5 -6
  7. data/Rakefile +10 -10
  8. data/docs/api.asciidoc +2 -1
  9. data/docs/configuration.asciidoc +4 -3
  10. data/lib/elastic_apm.rb +14 -2
  11. data/lib/elastic_apm/central_config.rb +7 -2
  12. data/lib/elastic_apm/config.rb +51 -19
  13. data/lib/elastic_apm/config/log_level_map.rb +47 -0
  14. data/lib/elastic_apm/config/options.rb +2 -1
  15. data/lib/elastic_apm/config/round_float.rb +31 -0
  16. data/lib/elastic_apm/config/wildcard_pattern_list.rb +2 -0
  17. data/lib/elastic_apm/context.rb +2 -8
  18. data/lib/elastic_apm/graphql.rb +2 -0
  19. data/lib/elastic_apm/grpc.rb +3 -3
  20. data/lib/elastic_apm/instrumenter.rb +11 -4
  21. data/lib/elastic_apm/metadata.rb +3 -1
  22. data/lib/elastic_apm/metadata/cloud_info.rb +130 -0
  23. data/lib/elastic_apm/metadata/service_info.rb +2 -2
  24. data/lib/elastic_apm/metadata/system_info/container_info.rb +16 -5
  25. data/lib/elastic_apm/metrics.rb +1 -0
  26. data/lib/elastic_apm/metrics/cpu_mem_set.rb +1 -0
  27. data/lib/elastic_apm/middleware.rb +8 -3
  28. data/lib/elastic_apm/opentracing.rb +2 -1
  29. data/lib/elastic_apm/span.rb +14 -0
  30. data/lib/elastic_apm/span/context/db.rb +1 -1
  31. data/lib/elastic_apm/spies/delayed_job.rb +8 -2
  32. data/lib/elastic_apm/spies/dynamo_db.rb +8 -1
  33. data/lib/elastic_apm/spies/elasticsearch.rb +10 -2
  34. data/lib/elastic_apm/spies/faraday.rb +5 -2
  35. data/lib/elastic_apm/spies/http.rb +1 -0
  36. data/lib/elastic_apm/spies/mongo.rb +6 -2
  37. data/lib/elastic_apm/spies/net_http.rb +3 -0
  38. data/lib/elastic_apm/spies/rake.rb +4 -2
  39. data/lib/elastic_apm/spies/resque.rb +4 -2
  40. data/lib/elastic_apm/spies/sequel.rb +12 -1
  41. data/lib/elastic_apm/spies/shoryuken.rb +2 -0
  42. data/lib/elastic_apm/spies/sidekiq.rb +2 -0
  43. data/lib/elastic_apm/spies/sneakers.rb +2 -0
  44. data/lib/elastic_apm/spies/sucker_punch.rb +2 -0
  45. data/lib/elastic_apm/sql/signature.rb +4 -2
  46. data/lib/elastic_apm/sql/tokenizer.rb +2 -2
  47. data/lib/elastic_apm/stacktrace/frame.rb +1 -0
  48. data/lib/elastic_apm/stacktrace_builder.rb +2 -4
  49. data/lib/elastic_apm/trace_context.rb +1 -3
  50. data/lib/elastic_apm/trace_context/traceparent.rb +2 -6
  51. data/lib/elastic_apm/trace_context/tracestate.rb +114 -9
  52. data/lib/elastic_apm/transaction.rb +41 -7
  53. data/lib/elastic_apm/transport/connection.rb +2 -1
  54. data/lib/elastic_apm/transport/filters/hash_sanitizer.rb +16 -16
  55. data/lib/elastic_apm/transport/filters/secrets_filter.rb +35 -12
  56. data/lib/elastic_apm/transport/serializers.rb +9 -6
  57. data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +44 -4
  58. data/lib/elastic_apm/transport/serializers/span_serializer.rb +3 -3
  59. data/lib/elastic_apm/transport/serializers/transaction_serializer.rb +2 -0
  60. data/lib/elastic_apm/transport/user_agent.rb +3 -3
  61. data/lib/elastic_apm/transport/worker.rb +1 -0
  62. data/lib/elastic_apm/util.rb +2 -0
  63. data/lib/elastic_apm/util/deep_dup.rb +65 -0
  64. data/lib/elastic_apm/util/precision_validator.rb +46 -0
  65. data/lib/elastic_apm/version.rb +1 -1
  66. metadata +10 -3
@@ -27,14 +27,12 @@ module ElasticAPM
27
27
  TRACE_ID_LENGTH = 16
28
28
  ID_LENGTH = 8
29
29
 
30
- # rubocop:disable Metrics/ParameterLists
31
30
  def initialize(
32
31
  version: VERSION,
33
32
  trace_id: nil,
34
33
  span_id: nil,
35
34
  id: nil,
36
- recorded: true,
37
- tracestate: nil
35
+ recorded: true
38
36
  )
39
37
  @version = version
40
38
  @trace_id = trace_id || hex(TRACE_ID_LENGTH)
@@ -42,11 +40,9 @@ module ElasticAPM
42
40
  @parent_id = span_id
43
41
  @id = id || hex(ID_LENGTH)
44
42
  @recorded = recorded
45
- @tracestate = tracestate
46
43
  end
47
- # rubocop:enable Metrics/ParameterLists
48
44
 
49
- attr_accessor :version, :id, :trace_id, :parent_id, :recorded, :tracestate
45
+ attr_accessor :version, :id, :trace_id, :parent_id, :recorded
50
46
 
51
47
  alias :recorded? :recorded
52
48
 
@@ -17,26 +17,131 @@
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
+ # @api private
41
+ class EsEntry
42
+ ASSIGN = ':'
43
+ SPLIT = ';'
44
+
45
+ SHORT_TO_LONG = { 's' => 'sample_rate' }.freeze
46
+ LONG_TO_SHORT = { 'sample_rate' => 's' }.freeze
47
+
48
+ def initialize(values = nil)
49
+ parse(values)
50
+ end
51
+
52
+ attr_reader :sample_rate
53
+
54
+ def key
55
+ 'es'
56
+ end
57
+
58
+ def value
59
+ LONG_TO_SHORT.map do |l, s|
60
+ "#{s}#{ASSIGN}#{send(l)}"
61
+ end.join(SPLIT)
62
+ end
63
+
64
+ def empty?
65
+ !sample_rate
66
+ end
67
+
68
+ def sample_rate=(val)
69
+ @sample_rate = Util::PrecisionValidator.validate(
70
+ val, precision: 4, minimum: 0.0001
71
+ )
72
+ end
73
+
74
+ def to_s
75
+ return nil if empty?
76
+
77
+ "es=#{value}"
78
+ end
79
+
80
+ private
81
+
82
+ def parse(values)
83
+ return unless values
84
+
85
+ values.split(SPLIT).map do |kv|
86
+ k, v = kv.split(ASSIGN)
87
+ next unless SHORT_TO_LONG.key?(k)
88
+ send("#{SHORT_TO_LONG[k]}=", v)
89
+ end
90
+ end
91
+ end
92
+
93
+ extend Forwardable
94
+
95
+ def initialize(entries: {}, sample_rate: nil)
96
+ @entries = entries
97
+
98
+ self.sample_rate = sample_rate if sample_rate
99
+ end
100
+
101
+ attr_accessor :entries
102
+
103
+ def_delegators :es_entry, :sample_rate, :sample_rate=
29
104
 
30
105
  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"))
106
+ entries =
107
+ split_by_nl_and_comma(header)
108
+ .each_with_object({}) do |entry, hsh|
109
+ k, v = entry.split('=')
110
+
111
+ hsh[k] =
112
+ case k
113
+ when 'es' then EsEntry.new(v)
114
+ else Entry.new(k, v)
115
+ end
116
+ end
117
+
118
+ new(entries: entries)
36
119
  end
37
120
 
38
121
  def to_header
39
- values.join(',')
122
+ return "" unless entries.any?
123
+
124
+ entries.values.map(&:to_s).join(',')
125
+ end
126
+
127
+ private
128
+
129
+ def es_entry
130
+ # lazy generate this so we only add it if necessary
131
+ entries['es'] ||= EsEntry.new
132
+ end
133
+
134
+ class << self
135
+ private
136
+
137
+ def split_by_nl_and_comma(str)
138
+ # HTTP allows multiple headers with the same name, eg. multiple
139
+ # Set-Cookie headers per response.
140
+ # Rack handles this by joining the headers under the same key,
141
+ # separated by newlines.
142
+ # See https://www.rubydoc.info/github/rack/rack/file/SPEC
143
+ String(str).split("\n").map { |s| s.split(',') }.flatten
144
+ end
40
145
  end
41
146
  end
42
147
  end
@@ -20,6 +20,17 @@
20
20
  module ElasticAPM
21
21
  # @api private
22
22
  class Transaction
23
+ # @api private
24
+ class Outcome
25
+ FAILURE = "failure"
26
+ SUCCESS = "success"
27
+ UNKNOWN = "unknown"
28
+
29
+ def self.from_http_status(code)
30
+ code.to_i >= 500 ? FAILURE : SUCCESS
31
+ end
32
+ end
33
+
23
34
  extend Forwardable
24
35
  include ChildDurations::Methods
25
36
 
@@ -33,9 +44,10 @@ module ElasticAPM
33
44
  def initialize(
34
45
  name = nil,
35
46
  type = nil,
47
+ config:,
36
48
  sampled: true,
49
+ sample_rate: 1,
37
50
  context: nil,
38
- config:,
39
51
  trace_context: nil
40
52
  )
41
53
  @name = name
@@ -52,13 +64,21 @@ module ElasticAPM
52
64
  @default_labels = config.default_labels
53
65
 
54
66
  @sampled = sampled
67
+ @sample_rate = sample_rate
55
68
 
56
69
  @context = context || Context.new # TODO: Lazy generate this?
57
70
  if @default_labels
58
71
  Util.reverse_merge!(@context.labels, @default_labels)
59
72
  end
60
73
 
61
- @trace_context = trace_context || TraceContext.new(recorded: sampled)
74
+ unless (@trace_context = trace_context)
75
+ @trace_context = TraceContext.new(
76
+ traceparent: TraceContext::Traceparent.new(recorded: sampled),
77
+ tracestate: TraceContext::Tracestate.new(
78
+ sample_rate: sampled ? sample_rate : 0
79
+ )
80
+ )
81
+ end
62
82
 
63
83
  @started_spans = 0
64
84
  @dropped_spans = 0
@@ -67,12 +87,26 @@ module ElasticAPM
67
87
  end
68
88
  # rubocop:enable Metrics/ParameterLists
69
89
 
70
- attr_accessor :name, :type, :result
90
+ attr_accessor :name, :type, :result, :outcome
91
+
92
+ attr_reader(
93
+ :breakdown_metrics,
94
+ :collect_metrics,
95
+ :context,
96
+ :dropped_spans,
97
+ :duration,
98
+ :framework_name,
99
+ :notifications,
100
+ :self_time,
101
+ :sample_rate,
102
+ :span_frames_min_duration,
103
+ :started_spans,
104
+ :timestamp,
105
+ :trace_context,
106
+ :transaction_max_spans
107
+ )
71
108
 
72
- attr_reader :context, :duration, :started_spans, :dropped_spans,
73
- :timestamp, :trace_context, :notifications, :self_time,
74
- :span_frames_min_duration, :collect_metrics, :breakdown_metrics,
75
- :framework_name, :transaction_max_spans
109
+ alias :collect_metrics? :collect_metrics
76
110
 
77
111
  def sampled?
78
112
  @sampled
@@ -42,11 +42,12 @@ module ElasticAPM
42
42
  Metadata.new(config)
43
43
  )
44
44
  )
45
- @url = config.server_url + '/intake/v2/events'
45
+ @url = "#{config.server_url}/intake/v2/events"
46
46
  @mutex = Mutex.new
47
47
  end
48
48
 
49
49
  attr_reader :http
50
+
50
51
  def write(str)
51
52
  return false if @config.disable_send
52
53
 
@@ -17,36 +17,36 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
+ require 'elastic_apm/util/deep_dup'
21
+
20
22
  module ElasticAPM
21
23
  module Transport
22
24
  module Filters
25
+ # @api private
23
26
  class HashSanitizer
24
27
  FILTERED = '[FILTERED]'
25
28
 
26
- KEY_FILTERS = [
27
- /passw(or)?d/i,
28
- /auth/i,
29
- /^pw$/,
30
- /secret/i,
31
- /token/i,
32
- /api[-._]?key/i,
33
- /session[-._]?id/i,
34
- /(set[-_])?cookie/i
35
- ].freeze
29
+ # DEPRECATED: Remove these additions in next major version
30
+ LEGACY_KEY_FILTERS = [/cookie/i, /auth/i].freeze
36
31
 
32
+ # DEPRECATED: Remove this check in next major version
37
33
  VALUE_FILTERS = [
38
34
  # (probably) credit card number
39
35
  /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
40
36
  ].freeze
41
37
 
42
- attr_accessor :key_filters
38
+ def initialize(key_patterns:)
39
+ @key_patterns = key_patterns + LEGACY_KEY_FILTERS
40
+ end
41
+
42
+ attr_accessor :key_patterns
43
43
 
44
- def initialize
45
- @key_filters = KEY_FILTERS
44
+ def strip_from(obj)
45
+ strip_from!(Util::DeepDup.dup(obj))
46
46
  end
47
47
 
48
- def strip_from!(obj, key_filters = KEY_FILTERS)
49
- return unless obj&.is_a?(Hash)
48
+ def strip_from!(obj)
49
+ return unless obj.is_a?(Hash)
50
50
 
51
51
  obj.each do |k, v|
52
52
  if filter_key?(k)
@@ -65,7 +65,7 @@ module ElasticAPM
65
65
  end
66
66
 
67
67
  def filter_key?(key)
68
- @key_filters.any? { |regex| regex.match(key) }
68
+ @key_patterns.any? { |regex| regex.match(key) }
69
69
  end
70
70
 
71
71
  def filter_value?(value)
@@ -26,21 +26,44 @@ module ElasticAPM
26
26
  class SecretsFilter
27
27
  def initialize(config)
28
28
  @config = config
29
- @sanitizer = HashSanitizer.new
30
- @sanitizer.key_filters += config.custom_key_filters +
31
- config.sanitize_field_names
29
+ @sanitizer =
30
+ HashSanitizer.new(
31
+ key_patterns: config.custom_key_filters +
32
+ config.sanitize_field_names
33
+ )
32
34
  end
33
35
 
34
36
  def call(payload)
35
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :headers)
36
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :env)
37
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :cookies)
38
- @sanitizer.strip_from! payload.dig(:transaction, :context, :response, :headers)
39
- @sanitizer.strip_from! payload.dig(:error, :context, :request, :headers)
40
- @sanitizer.strip_from! payload.dig(:error, :context, :request, :cookies)
41
- @sanitizer.strip_from! payload.dig(:error, :context, :response, :headers)
42
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :body)
43
-
37
+ @sanitizer.strip_from!(
38
+ payload.dig(:transaction, :context, :request, :body)
39
+ )
40
+ @sanitizer.strip_from!(
41
+ payload.dig(:transaction, :context, :request, :cookies)
42
+ )
43
+ @sanitizer.strip_from!(
44
+ payload.dig(:transaction, :context, :request, :env)
45
+ )
46
+ @sanitizer.strip_from!(
47
+ payload.dig(:transaction, :context, :request, :headers)
48
+ )
49
+ @sanitizer.strip_from!(
50
+ payload.dig(:transaction, :context, :response, :headers)
51
+ )
52
+ @sanitizer.strip_from!(
53
+ payload.dig(:error, :context, :request, :body)
54
+ )
55
+ @sanitizer.strip_from!(
56
+ payload.dig(:error, :context, :request, :cookies)
57
+ )
58
+ @sanitizer.strip_from!(
59
+ payload.dig(:error, :context, :request, :env)
60
+ )
61
+ @sanitizer.strip_from!(
62
+ payload.dig(:error, :context, :request, :headers)
63
+ )
64
+ @sanitizer.strip_from!(
65
+ payload.dig(:error, :context, :response, :headers)
66
+ )
44
67
  payload
45
68
  end
46
69
  end
@@ -45,18 +45,20 @@ module ElasticAPM
45
45
  def keyword_object(hash)
46
46
  return unless hash
47
47
 
48
- hash.tap do |h|
49
- h.each { |k, v| hash[k] = keyword_field(v) }
48
+ hash.each do |k, v|
49
+ hash[k] =
50
+ case v
51
+ when Hash then keyword_object(v)
52
+ else keyword_field(v)
53
+ end
50
54
  end
51
55
  end
52
56
 
53
57
  def mixed_object(hash)
54
58
  return unless hash
55
59
 
56
- hash.tap do |h|
57
- h.each do |k, v|
58
- hash[k] = v.is_a?(String) ? keyword_field(v) : v
59
- end
60
+ hash.each do |k, v|
61
+ hash[k] = v.is_a?(String) ? keyword_field(v) : v
60
62
  end
61
63
  end
62
64
  end
@@ -72,6 +74,7 @@ module ElasticAPM
72
74
  end
73
75
 
74
76
  attr_reader :transaction, :span, :error, :metadata, :metricset
77
+
75
78
  def serialize(resource)
76
79
  case resource
77
80
  when Transaction
@@ -23,14 +23,19 @@ module ElasticAPM
23
23
  # @api private
24
24
  class MetadataSerializer < Serializer
25
25
  def build(metadata)
26
- {
27
- metadata: {
26
+ base =
27
+ {
28
28
  service: build_service(metadata.service),
29
29
  process: build_process(metadata.process),
30
30
  system: build_system(metadata.system),
31
31
  labels: build_labels(metadata.labels)
32
32
  }
33
- }
33
+
34
+ if metadata.cloud.provider
35
+ base[:cloud] = build_cloud(metadata.cloud)
36
+ end
37
+
38
+ { metadata: base }
34
39
  end
35
40
 
36
41
  private
@@ -59,7 +64,7 @@ module ElasticAPM
59
64
  }
60
65
  }
61
66
 
62
- if node_name = service.node_name
67
+ if (node_name = service.node_name)
63
68
  base[:node] = { name: keyword_field(node_name) }
64
69
  end
65
70
 
@@ -83,9 +88,44 @@ module ElasticAPM
83
88
  }
84
89
  end
85
90
 
91
+ def build_cloud(cloud)
92
+ strip_nulls!(
93
+ provider: cloud.provider,
94
+ account: {
95
+ id: keyword_field(cloud.account_id),
96
+ name: keyword_field(cloud.account_name)
97
+ },
98
+ availability_zone: keyword_field(cloud.availability_zone),
99
+ instance: {
100
+ id: keyword_field(cloud.instance_id),
101
+ name: keyword_field(cloud.instance_name)
102
+ },
103
+ machine: { type: keyword_field(cloud.machine_type) },
104
+ project: {
105
+ id: keyword_field(cloud.project_id),
106
+ name: keyword_field(cloud.project_name)
107
+ },
108
+ region: keyword_field(cloud.region)
109
+ )
110
+ end
111
+
86
112
  def build_labels(labels)
87
113
  keyword_object(labels)
88
114
  end
115
+
116
+ # A bug in APM Server 7.9 disallows null values in `cloud`
117
+ def strip_nulls!(hash)
118
+ hash.each_key do |key|
119
+ case value = hash[key]
120
+ when Hash
121
+ strip_nulls!(value)
122
+ hash.delete(key) if value.empty?
123
+ when nil then hash.delete(key)
124
+ end
125
+ end
126
+
127
+ hash
128
+ end
89
129
  end
90
130
  end
91
131
  end