elastic-apm 3.7.0 → 3.11.0

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