elastic-apm 3.10.0 → 3.12.1

Sign up to get free protection for your applications and to get access to all the features.
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