elastic-apm 3.7.0 → 3.11.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/Jenkinsfile +139 -96
  3. data/.ci/packer_cache.sh +12 -10
  4. data/.rspec +0 -1
  5. data/CHANGELOG.asciidoc +80 -0
  6. data/Gemfile +4 -3
  7. data/bin/run-tests +4 -1
  8. data/docker-compose.yml +2 -0
  9. data/docs/configuration.asciidoc +38 -0
  10. data/docs/debugging.asciidoc +14 -0
  11. data/docs/supported-technologies.asciidoc +2 -1
  12. data/lib/elastic_apm/config.rb +36 -13
  13. data/lib/elastic_apm/config/options.rb +2 -1
  14. data/lib/elastic_apm/config/round_float.rb +31 -0
  15. data/lib/elastic_apm/config/wildcard_pattern_list.rb +13 -1
  16. data/lib/elastic_apm/context_builder.rb +1 -1
  17. data/lib/elastic_apm/grpc.rb +2 -2
  18. data/lib/elastic_apm/instrumenter.rb +10 -3
  19. data/lib/elastic_apm/metadata.rb +3 -1
  20. data/lib/elastic_apm/metadata/cloud_info.rb +128 -0
  21. data/lib/elastic_apm/metadata/service_info.rb +5 -2
  22. data/lib/elastic_apm/metadata/system_info.rb +5 -3
  23. data/lib/elastic_apm/metadata/system_info/container_info.rb +28 -4
  24. data/lib/elastic_apm/middleware.rb +8 -2
  25. data/lib/elastic_apm/opentracing.rb +47 -23
  26. data/lib/elastic_apm/span.rb +7 -3
  27. data/lib/elastic_apm/spies.rb +16 -14
  28. data/lib/elastic_apm/spies/delayed_job.rb +4 -2
  29. data/lib/elastic_apm/spies/dynamo_db.rb +58 -0
  30. data/lib/elastic_apm/spies/elasticsearch.rb +26 -2
  31. data/lib/elastic_apm/spies/mongo.rb +1 -1
  32. data/lib/elastic_apm/spies/net_http.rb +6 -2
  33. data/lib/elastic_apm/spies/sequel.rb +1 -1
  34. data/lib/elastic_apm/trace_context.rb +1 -1
  35. data/lib/elastic_apm/trace_context/traceparent.rb +2 -4
  36. data/lib/elastic_apm/trace_context/tracestate.rb +112 -9
  37. data/lib/elastic_apm/transaction.rb +26 -5
  38. data/lib/elastic_apm/transport/connection.rb +1 -0
  39. data/lib/elastic_apm/transport/filters/hash_sanitizer.rb +70 -0
  40. data/lib/elastic_apm/transport/filters/secrets_filter.rb +14 -56
  41. data/lib/elastic_apm/transport/serializers.rb +8 -6
  42. data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +56 -23
  43. data/lib/elastic_apm/transport/serializers/span_serializer.rb +2 -1
  44. data/lib/elastic_apm/transport/serializers/transaction_serializer.rb +1 -0
  45. data/lib/elastic_apm/transport/user_agent.rb +3 -3
  46. data/lib/elastic_apm/transport/worker.rb +5 -0
  47. data/lib/elastic_apm/util.rb +2 -0
  48. data/lib/elastic_apm/util/precision_validator.rb +46 -0
  49. data/lib/elastic_apm/version.rb +1 -1
  50. metadata +12 -8
  51. data/.ci/downstreamTests.groovy +0 -192
@@ -26,16 +26,40 @@ module ElasticAPM
26
26
  TYPE = 'db'
27
27
  SUBTYPE = 'elasticsearch'
28
28
 
29
+ def self.sanitizer
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
37
+ end
38
+
29
39
  def install
30
40
  ::Elasticsearch::Transport::Client.class_eval do
31
41
  alias perform_request_without_apm perform_request
32
42
 
33
43
  def perform_request(method, path, *args, &block)
44
+ unless ElasticAPM.current_transaction
45
+ return perform_request_without_apm(method, path, *args, &block)
46
+ end
47
+
34
48
  name = format(NAME_FORMAT, method, path)
35
- statement = args[0].is_a?(String) ? args[0] : args[0].to_json
49
+ statement = []
50
+
51
+ statement << { params: args&.[](0) }
52
+
53
+ if ElasticAPM.agent.config.capture_elasticsearch_queries
54
+ unless args[1].nil? || args[1].empty?
55
+ statement << {
56
+ body: ElasticAPM::Spies::ElasticsearchSpy.sanitizer.strip_from!(args[1])
57
+ }
58
+ end
59
+ end
36
60
 
37
61
  context = Span::Context.new(
38
- db: { statement: statement },
62
+ db: { statement: statement.reduce({}, :merge).to_json },
39
63
  destination: {
40
64
  name: SUBTYPE,
41
65
  resource: SUBTYPE,
@@ -85,8 +85,8 @@ module ElasticAPM
85
85
  end
86
86
 
87
87
  def pop_event(event)
88
- return unless (curr = ElasticAPM.current_span)
89
88
  span = @events.delete(event.operation_id)
89
+ return unless (curr = ElasticAPM.current_span)
90
90
 
91
91
  curr == span && ElasticAPM.end_span
92
92
  end
@@ -64,8 +64,12 @@ module ElasticAPM
64
64
  method = req.method.to_s.upcase
65
65
  path, query = req.path.split('?')
66
66
 
67
- cls = use_ssl? ? URI::HTTPS : URI::HTTP
68
- uri = cls.build([nil, host, port, path, query, nil])
67
+ url = use_ssl? ? +'https://' : +'http://'
68
+ url << host
69
+ url << ":#{port}" if port
70
+ url << path
71
+ url << "?#{query}" if query
72
+ uri = URI(url)
69
73
 
70
74
  destination =
71
75
  ElasticAPM::Span::Context::Destination.from_uri(uri)
@@ -28,7 +28,7 @@ module ElasticAPM
28
28
  ACTION = 'query'
29
29
 
30
30
  def self.summarizer
31
- @summarizer = Sql.summarizer
31
+ @summarizer ||= Sql.summarizer
32
32
  end
33
33
 
34
34
  def install
@@ -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
@@ -34,6 +34,7 @@ module ElasticAPM
34
34
  name = nil,
35
35
  type = nil,
36
36
  sampled: true,
37
+ sample_rate: 1,
37
38
  context: nil,
38
39
  config:,
39
40
  trace_context: nil
@@ -52,13 +53,19 @@ module ElasticAPM
52
53
  @default_labels = config.default_labels
53
54
 
54
55
  @sampled = sampled
56
+ @sample_rate = sample_rate
55
57
 
56
58
  @context = context || Context.new # TODO: Lazy generate this?
57
59
  if @default_labels
58
60
  Util.reverse_merge!(@context.labels, @default_labels)
59
61
  end
60
62
 
61
- @trace_context = trace_context || TraceContext.new(recorded: sampled)
63
+ unless (@trace_context = trace_context)
64
+ @trace_context = TraceContext.new(
65
+ traceparent: TraceContext::Traceparent.new(recorded: sampled),
66
+ tracestate: TraceContext::Tracestate.new(sample_rate: sampled ? sample_rate : 0)
67
+ )
68
+ end
62
69
 
63
70
  @started_spans = 0
64
71
  @dropped_spans = 0
@@ -69,10 +76,24 @@ module ElasticAPM
69
76
 
70
77
  attr_accessor :name, :type, :result
71
78
 
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
79
+ attr_reader(
80
+ :breakdown_metrics,
81
+ :collect_metrics,
82
+ :context,
83
+ :dropped_spans,
84
+ :duration,
85
+ :framework_name,
86
+ :notifications,
87
+ :self_time,
88
+ :sample_rate,
89
+ :span_frames_min_duration,
90
+ :started_spans,
91
+ :timestamp,
92
+ :trace_context,
93
+ :transaction_max_spans
94
+ )
95
+
96
+ alias :collect_metrics? :collect_metrics
76
97
 
77
98
  def sampled?
78
99
  @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
 
@@ -0,0 +1,70 @@
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 Transport
22
+ module Filters
23
+ class HashSanitizer
24
+ FILTERED = '[FILTERED]'
25
+
26
+ # DEPRECATED: Remove these additions in next major version
27
+ LEGACY_KEY_FILTERS = [/cookie/i, /auth/i].freeze
28
+
29
+ # DEPRECATED: Remove this check in next major version
30
+ VALUE_FILTERS = [
31
+ # (probably) credit card number
32
+ /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
33
+ ].freeze
34
+
35
+ def initialize(key_patterns:)
36
+ @key_patterns = key_patterns + LEGACY_KEY_FILTERS
37
+ end
38
+
39
+ attr_accessor :key_patterns
40
+
41
+ def strip_from!(obj)
42
+ return unless obj&.is_a?(Hash)
43
+
44
+ obj.each do |k, v|
45
+ if filter_key?(k)
46
+ next obj[k] = FILTERED
47
+ end
48
+
49
+ case v
50
+ when Hash
51
+ strip_from!(v)
52
+ when String
53
+ if filter_value?(v)
54
+ obj[k] = FILTERED
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def filter_key?(key)
61
+ @key_patterns.any? { |regex| regex.match(key) }
62
+ end
63
+
64
+ def filter_value?(value)
65
+ VALUE_FILTERS.any? { |regex| regex.match(value) }
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -17,75 +17,33 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
+ require 'elastic_apm/transport/filters/hash_sanitizer'
21
+
20
22
  module ElasticAPM
21
23
  module Transport
22
24
  module Filters
23
25
  # @api private
24
26
  class SecretsFilter
25
- FILTERED = '[FILTERED]'
26
-
27
- KEY_FILTERS = [
28
- /passw(or)?d/i,
29
- /auth/i,
30
- /^pw$/,
31
- /secret/i,
32
- /token/i,
33
- /api[-._]?key/i,
34
- /session[-._]?id/i,
35
- /(set[-_])?cookie/i
36
- ].freeze
37
-
38
- VALUE_FILTERS = [
39
- # (probably) credit card number
40
- /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
41
- ].freeze
42
-
43
27
  def initialize(config)
44
28
  @config = config
45
- @key_filters =
46
- KEY_FILTERS +
47
- config.custom_key_filters +
48
- config.sanitize_field_names
29
+ @sanitizer =
30
+ HashSanitizer.new(
31
+ key_patterns: config.custom_key_filters + config.sanitize_field_names
32
+ )
49
33
  end
50
34
 
51
35
  def call(payload)
52
- strip_from! payload.dig(:transaction, :context, :request, :headers)
53
- strip_from! payload.dig(:transaction, :context, :request, :env)
54
- strip_from! payload.dig(:transaction, :context, :request, :cookies)
55
- strip_from! payload.dig(:transaction, :context, :response, :headers)
56
- strip_from! payload.dig(:error, :context, :request, :headers)
57
- strip_from! payload.dig(:error, :context, :response, :headers)
58
- strip_from! payload.dig(:transaction, :context, :request, :body)
36
+ @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :body)
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)
40
+ @sanitizer.strip_from! payload.dig(:transaction, :context, :response, :headers)
41
+ @sanitizer.strip_from! payload.dig(:error, :context, :request, :cookies)
42
+ @sanitizer.strip_from! payload.dig(:error, :context, :request, :headers)
43
+ @sanitizer.strip_from! payload.dig(:error, :context, :response, :headers)
59
44
 
60
45
  payload
61
46
  end
62
-
63
- def strip_from!(obj)
64
- return unless obj&.is_a?(Hash)
65
-
66
- obj.each do |k, v|
67
- if filter_key?(k)
68
- next obj[k] = FILTERED
69
- end
70
-
71
- case v
72
- when Hash
73
- strip_from!(v)
74
- when String
75
- if filter_value?(v)
76
- obj[k] = FILTERED
77
- end
78
- end
79
- end
80
- end
81
-
82
- def filter_key?(key)
83
- @key_filters.any? { |regex| regex.match(key) }
84
- end
85
-
86
- def filter_value?(value)
87
- VALUE_FILTERS.any? { |regex| regex.match(value) }
88
- end
89
47
  end
90
48
  end
91
49
  end