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
@@ -20,6 +20,18 @@
20
20
  module ElasticAPM
21
21
  # @api private
22
22
  class Transaction
23
+
24
+ # @api private
25
+ class Outcome
26
+ FAILURE = "failure"
27
+ SUCCESS = "success"
28
+ UNKNOWN = "unknown"
29
+
30
+ def self.from_http_status(code)
31
+ code.to_i >= 500 ? FAILURE : SUCCESS
32
+ end
33
+ end
34
+
23
35
  extend Forwardable
24
36
  include ChildDurations::Methods
25
37
 
@@ -34,6 +46,7 @@ module ElasticAPM
34
46
  name = nil,
35
47
  type = nil,
36
48
  sampled: true,
49
+ sample_rate: 1,
37
50
  context: nil,
38
51
  config:,
39
52
  trace_context: nil
@@ -52,13 +65,19 @@ module ElasticAPM
52
65
  @default_labels = config.default_labels
53
66
 
54
67
  @sampled = sampled
68
+ @sample_rate = sample_rate
55
69
 
56
70
  @context = context || Context.new # TODO: Lazy generate this?
57
71
  if @default_labels
58
72
  Util.reverse_merge!(@context.labels, @default_labels)
59
73
  end
60
74
 
61
- @trace_context = trace_context || TraceContext.new(recorded: sampled)
75
+ unless (@trace_context = trace_context)
76
+ @trace_context = TraceContext.new(
77
+ traceparent: TraceContext::Traceparent.new(recorded: sampled),
78
+ tracestate: TraceContext::Tracestate.new(sample_rate: sampled ? sample_rate : 0)
79
+ )
80
+ end
62
81
 
63
82
  @started_spans = 0
64
83
  @dropped_spans = 0
@@ -67,12 +86,26 @@ module ElasticAPM
67
86
  end
68
87
  # rubocop:enable Metrics/ParameterLists
69
88
 
70
- attr_accessor :name, :type, :result
89
+ attr_accessor :name, :type, :result, :outcome
90
+
91
+ attr_reader(
92
+ :breakdown_metrics,
93
+ :collect_metrics,
94
+ :context,
95
+ :dropped_spans,
96
+ :duration,
97
+ :framework_name,
98
+ :notifications,
99
+ :self_time,
100
+ :sample_rate,
101
+ :span_frames_min_duration,
102
+ :started_spans,
103
+ :timestamp,
104
+ :trace_context,
105
+ :transaction_max_spans
106
+ )
71
107
 
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
108
+ alias :collect_metrics? :collect_metrics
76
109
 
77
110
  def sampled?
78
111
  @sampled
@@ -47,6 +47,7 @@ module ElasticAPM
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,35 +17,34 @@
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
23
25
  class HashSanitizer
24
26
  FILTERED = '[FILTERED]'
25
27
 
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
28
+ # DEPRECATED: Remove these additions in next major version
29
+ LEGACY_KEY_FILTERS = [/cookie/i, /auth/i].freeze
36
30
 
31
+ # DEPRECATED: Remove this check in next major version
37
32
  VALUE_FILTERS = [
38
33
  # (probably) credit card number
39
34
  /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
40
35
  ].freeze
41
36
 
42
- attr_accessor :key_filters
37
+ def initialize(key_patterns:)
38
+ @key_patterns = key_patterns + LEGACY_KEY_FILTERS
39
+ end
40
+
41
+ attr_accessor :key_patterns
43
42
 
44
- def initialize
45
- @key_filters = KEY_FILTERS
43
+ def strip_from(obj)
44
+ strip_from!(Util::DeepDup.dup(obj))
46
45
  end
47
46
 
48
- def strip_from!(obj, key_filters = KEY_FILTERS)
47
+ def strip_from!(obj)
49
48
  return unless obj&.is_a?(Hash)
50
49
 
51
50
  obj.each do |k, v|
@@ -65,7 +64,7 @@ module ElasticAPM
65
64
  end
66
65
 
67
66
  def filter_key?(key)
68
- @key_filters.any? { |regex| regex.match(key) }
67
+ @key_patterns.any? { |regex| regex.match(key) }
69
68
  end
70
69
 
71
70
  def filter_value?(value)
@@ -26,19 +26,23 @@ 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 + config.sanitize_field_names
32
+ )
32
33
  end
33
34
 
34
35
  def call(payload)
35
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :headers)
36
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :env)
36
+ @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :body)
37
37
  @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :cookies)
38
+ @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :env)
39
+ @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :headers)
38
40
  @sanitizer.strip_from! payload.dig(:transaction, :context, :response, :headers)
41
+ @sanitizer.strip_from! payload.dig(:error, :context, :request, :body)
42
+ @sanitizer.strip_from! payload.dig(:error, :context, :request, :cookies)
43
+ @sanitizer.strip_from! payload.dig(:error, :context, :request, :env)
39
44
  @sanitizer.strip_from! payload.dig(:error, :context, :request, :headers)
40
45
  @sanitizer.strip_from! payload.dig(:error, :context, :response, :headers)
41
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :body)
42
46
 
43
47
  payload
44
48
  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
@@ -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
@@ -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.keys.each 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
@@ -42,7 +42,9 @@ module ElasticAPM
42
42
  context: context_serializer.build(span.context),
43
43
  stacktrace: span.stacktrace.to_a,
44
44
  timestamp: span.timestamp,
45
- trace_id: span.trace_id
45
+ trace_id: span.trace_id,
46
+ sample_rate: span.sample_rate,
47
+ outcome: keyword_field(span.outcome)
46
48
  }
47
49
  }
48
50
  end
@@ -35,9 +35,11 @@ module ElasticAPM
35
35
  name: keyword_field(transaction.name),
36
36
  type: keyword_field(transaction.type),
37
37
  result: keyword_field(transaction.result.to_s),
38
+ outcome: keyword_field(transaction.outcome),
38
39
  duration: ms(transaction.duration),
39
40
  timestamp: transaction.timestamp,
40
41
  sampled: transaction.sampled?,
42
+ sample_rate: transaction.sample_rate,
41
43
  context: context_serializer.build(transaction.context),
42
44
  span_count: {
43
45
  started: transaction.started_spans,
@@ -32,14 +32,14 @@ module ElasticAPM
32
32
  private
33
33
 
34
34
  def build(config)
35
- metadata = Metadata.new(config)
35
+ service = Metadata::ServiceInfo.new(config)
36
36
 
37
37
  [
38
38
  "elastic-apm-ruby/#{VERSION}",
39
39
  HTTP::Request::USER_AGENT,
40
40
  [
41
- metadata.service.runtime.name,
42
- metadata.service.runtime.version
41
+ service.runtime.name,
42
+ service.runtime.version
43
43
  ].join('/')
44
44
  ].join(' ')
45
45
  end
@@ -53,6 +53,7 @@ module ElasticAPM
53
53
  end
54
54
 
55
55
  attr_reader :queue, :filters, :name, :connection, :serializers
56
+
56
57
  def work_forever
57
58
  while (msg = queue.pop)
58
59
  case msg
@@ -46,6 +46,8 @@ module ElasticAPM
46
46
 
47
47
  def self.truncate(value, max_length: 1024)
48
48
  return unless value
49
+
50
+ value = String(value)
49
51
  return value if value.length <= max_length
50
52
 
51
53
  value[0...(max_length - 1)] + '…'
@@ -0,0 +1,66 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ module ElasticAPM
21
+ module Util
22
+ # @api private
23
+ #
24
+ # Makes a deep copy of an Array or Hash
25
+ # NB: Not guaranteed to work well with complex objects, only simple Hash,
26
+ # Array, String, Number, etc…
27
+ class DeepDup
28
+ def initialize(obj)
29
+ @obj = obj
30
+ end
31
+
32
+ def dup
33
+ deep_dup(@obj)
34
+ end
35
+
36
+ def self.dup(obj)
37
+ new(obj).dup
38
+ end
39
+
40
+ private
41
+
42
+ def deep_dup(obj)
43
+ case obj
44
+ when Hash then hash(obj)
45
+ when Array then array(obj)
46
+ else obj.dup
47
+ end
48
+ end
49
+
50
+ def array(arr)
51
+ arr.map(&method(:deep_dup))
52
+ end
53
+
54
+ def hash(hsh)
55
+ result = hsh.dup
56
+
57
+ hsh.each_pair do |key, value|
58
+ result[key] = deep_dup(value)
59
+ end
60
+
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,46 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ module ElasticAPM
21
+ module Util
22
+ # @api private
23
+ # Rounds half away from zero.
24
+ # If `minimum` is provided, and the value rounds to 0 (but was not zero to
25
+ # begin with), use the minimum instead.
26
+ module PrecisionValidator
27
+ extend self
28
+
29
+ def validate(value, precision: 0, minimum: nil)
30
+ float = Float(value)
31
+ return nil unless (0.0..1.0).cover?(float)
32
+ return float if float == 0
33
+
34
+ multiplier = Float(10**precision)
35
+ rounded = (float * multiplier + 0.5).floor / multiplier
36
+ if rounded == 0 && minimum
37
+ minimum
38
+ else
39
+ rounded
40
+ end
41
+ rescue ArgumentError
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ end