elastic-apm 3.8.0 → 3.11.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.asciidoc +72 -0
  3. data/Gemfile +2 -3
  4. data/docs/configuration.asciidoc +1 -0
  5. data/docs/debugging.asciidoc +14 -0
  6. data/docs/supported-technologies.asciidoc +2 -1
  7. data/lib/elastic_apm/config.rb +32 -5
  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 +13 -1
  11. data/lib/elastic_apm/context_builder.rb +1 -1
  12. data/lib/elastic_apm/instrumenter.rb +10 -3
  13. data/lib/elastic_apm/metadata.rb +3 -1
  14. data/lib/elastic_apm/metadata/cloud_info.rb +128 -0
  15. data/lib/elastic_apm/metadata/system_info.rb +5 -3
  16. data/lib/elastic_apm/metadata/system_info/container_info.rb +28 -4
  17. data/lib/elastic_apm/middleware.rb +8 -2
  18. data/lib/elastic_apm/span.rb +7 -3
  19. data/lib/elastic_apm/spies/delayed_job.rb +4 -2
  20. data/lib/elastic_apm/spies/dynamo_db.rb +58 -0
  21. data/lib/elastic_apm/spies/elasticsearch.rb +11 -1
  22. data/lib/elastic_apm/trace_context.rb +1 -1
  23. data/lib/elastic_apm/trace_context/traceparent.rb +2 -4
  24. data/lib/elastic_apm/trace_context/tracestate.rb +112 -9
  25. data/lib/elastic_apm/transaction.rb +26 -5
  26. data/lib/elastic_apm/transport/connection.rb +1 -0
  27. data/lib/elastic_apm/transport/filters/hash_sanitizer.rb +9 -16
  28. data/lib/elastic_apm/transport/filters/secrets_filter.rb +8 -6
  29. data/lib/elastic_apm/transport/serializers.rb +8 -6
  30. data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +43 -3
  31. data/lib/elastic_apm/transport/serializers/span_serializer.rb +2 -1
  32. data/lib/elastic_apm/transport/serializers/transaction_serializer.rb +1 -0
  33. data/lib/elastic_apm/transport/user_agent.rb +3 -3
  34. data/lib/elastic_apm/transport/worker.rb +5 -0
  35. data/lib/elastic_apm/util.rb +2 -0
  36. data/lib/elastic_apm/util/precision_validator.rb +46 -0
  37. data/lib/elastic_apm/version.rb +1 -1
  38. metadata +6 -2
@@ -24,7 +24,7 @@ module ElasticAPM
24
24
  def initialize(config)
25
25
  @config = config
26
26
 
27
- @hostname = @config.hostname || `hostname`.chomp
27
+ @hostname = @config.hostname || self.class.system_hostname
28
28
  @architecture = gem_platform.cpu
29
29
  @platform = gem_platform.os
30
30
 
@@ -35,11 +35,13 @@ module ElasticAPM
35
35
 
36
36
  attr_reader :hostname, :architecture, :platform, :container, :kubernetes
37
37
 
38
- private
39
-
40
38
  def gem_platform
41
39
  @gem_platform ||= Gem::Platform.local
42
40
  end
41
+
42
+ def self.system_hostname
43
+ @system_hostname ||= `hostname`.chomp
44
+ end
43
45
  end
44
46
  end
45
47
  end
@@ -81,8 +81,14 @@ module ElasticAPM
81
81
  ENV.fetch('KUBERNETES_POD_UID', kubernetes_pod_uid)
82
82
  end
83
83
 
84
- CONTAINER_ID_REGEX = /^[0-9A-Fa-f]{64}$/.freeze
85
- KUBEPODS_REGEX = %r{(?:^/kubepods/[^/]+/pod([^/]+)$)|(?:^/kubepods\.slice/kubepods-[^/]+\.slice/kubepods-[^/]+-pod([^/]+)\.slice$)}.freeze # rubocop:disable Metrics/LineLength
84
+ CONTAINER_ID_REGEXES = [
85
+ %r{^[[:xdigit:]]{64}$},
86
+ %r{^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4,}$}
87
+ ]
88
+ KUBEPODS_REGEXES = [
89
+ %r{(?:^/kubepods[^\s]*/pod([^/]+)$)},
90
+ %r{(?:^/kubepods\.slice/kubepods-[^/]+\.slice/kubepods-[^/]+-pod([^/]+)\.slice$)}
91
+ ]
86
92
  SYSTEMD_SCOPE_SUFFIX = '.scope'
87
93
 
88
94
  # rubocop:disable Metrics/PerceivedComplexity
@@ -118,18 +124,36 @@ module ElasticAPM
118
124
  end
119
125
  end
120
126
 
121
- if (kubepods_match = KUBEPODS_REGEX.match(directory))
127
+ if (kubepods_match = match_kubepods(directory))
122
128
  pod_id = kubepods_match[1] || kubepods_match[2]
123
129
 
124
130
  self.container_id = container_id
125
131
  self.kubernetes_pod_uid = pod_id
126
- elsif CONTAINER_ID_REGEX.match(container_id)
132
+ elsif match_container(container_id)
127
133
  self.container_id = container_id
128
134
  end
129
135
  end
130
136
  end
131
137
  # rubocop:enable Metrics/PerceivedComplexity
132
138
  # rubocop:enable Metrics/CyclomaticComplexity
139
+
140
+ def match_kubepods(directory)
141
+ KUBEPODS_REGEXES.each do |r|
142
+ next unless (match = r.match(directory))
143
+ return match
144
+ end
145
+
146
+ nil
147
+ end
148
+
149
+ def match_container(container_id)
150
+ CONTAINER_ID_REGEXES.each do |r|
151
+ next unless (match = r.match(container_id))
152
+ return match
153
+ end
154
+
155
+ nil
156
+ end
133
157
  end
134
158
  end
135
159
  end
@@ -59,9 +59,15 @@ module ElasticAPM
59
59
  end
60
60
 
61
61
  def path_ignored?(env)
62
- config.ignore_url_patterns.any? do |r|
63
- env['PATH_INFO'].match r
62
+ return true if config.ignore_url_patterns.any? do |r|
63
+ r.match(env['PATH_INFO'])
64
64
  end
65
+
66
+ return true if config.transaction_ignore_urls.any? do |r|
67
+ r.match(env['PATH_INFO'])
68
+ end
69
+
70
+ false
65
71
  end
66
72
 
67
73
  def start_transaction(env)
@@ -38,7 +38,8 @@ module ElasticAPM
38
38
  action: nil,
39
39
  context: nil,
40
40
  stacktrace_builder: nil,
41
- sync: nil
41
+ sync: nil,
42
+ sample_rate: nil
42
43
  )
43
44
  @name = name
44
45
 
@@ -53,6 +54,7 @@ module ElasticAPM
53
54
  @transaction = transaction
54
55
  @parent = parent
55
56
  @trace_context = trace_context || parent.trace_context.child
57
+ @sample_rate = transaction.sample_rate
56
58
 
57
59
  @context = context || Span::Context.new(sync: sync)
58
60
  @stacktrace_builder = stacktrace_builder
@@ -73,6 +75,7 @@ module ElasticAPM
73
75
  :context,
74
76
  :duration,
75
77
  :parent,
78
+ :sample_rate,
76
79
  :self_time,
77
80
  :stacktrace,
78
81
  :timestamp,
@@ -97,11 +100,12 @@ module ElasticAPM
97
100
 
98
101
  def done(clock_end: Util.monotonic_micros)
99
102
  stop clock_end
103
+ self
104
+ end
100
105
 
106
+ def prepare_for_serialization!
101
107
  build_stacktrace! if should_build_stacktrace?
102
108
  self.original_backtrace = nil # release original
103
-
104
- self
105
109
  end
106
110
 
107
111
  def stopped?
@@ -41,10 +41,10 @@ module ElasticAPM
41
41
  job_name = name_from_payload(job.payload_object)
42
42
  transaction = ElasticAPM.start_transaction(job_name, TYPE)
43
43
  job.invoke_job_without_apm(*args, &block)
44
- transaction.done 'success'
44
+ transaction&.done 'success'
45
45
  rescue ::Exception => e
46
46
  ElasticAPM.report(e, handled: false)
47
- transaction.done 'error'
47
+ transaction&.done 'error'
48
48
  raise
49
49
  ensure
50
50
  ElasticAPM.end_transaction
@@ -53,6 +53,8 @@ module ElasticAPM
53
53
  def self.name_from_payload(payload_object)
54
54
  if payload_object.is_a?(::Delayed::PerformableMethod)
55
55
  performable_method_name(payload_object)
56
+ elsif payload_object.class.name == 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'
57
+ payload_object.job_data['job_class']
56
58
  else
57
59
  payload_object.class.name
58
60
  end
@@ -0,0 +1,58 @@
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
+ # @api private
22
+ module Spies
23
+ # @api private
24
+ class DynamoDBSpy
25
+ def self.without_net_http
26
+ return yield unless defined?(NetHTTPSpy)
27
+
28
+ ElasticAPM::Spies::NetHTTPSpy.disable_in do
29
+ yield
30
+ end
31
+ end
32
+
33
+ def install
34
+ ::Aws::DynamoDB::Client.class_eval do
35
+ # Alias all available operations
36
+ api.operation_names.each do |operation_name|
37
+ alias :"#{operation_name}_without_apm" :"#{operation_name}"
38
+
39
+ define_method(operation_name) do |params = {}, options = {}|
40
+ ElasticAPM.with_span(operation_name, 'db', subtype: 'dynamodb', action: operation_name) do
41
+ ElasticAPM::Spies::DynamoDBSpy.without_net_http do
42
+ original_method = method("#{operation_name}_without_apm")
43
+ original_method.call(params, options)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ register(
53
+ 'Aws::DynamoDB::Client',
54
+ 'aws-sdk-dynamodb',
55
+ DynamoDBSpy.new
56
+ )
57
+ end
58
+ end
@@ -27,7 +27,13 @@ module ElasticAPM
27
27
  SUBTYPE = 'elasticsearch'
28
28
 
29
29
  def self.sanitizer
30
- @sanitizer ||= ElasticAPM::Transport::Filters::HashSanitizer.new
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
31
37
  end
32
38
 
33
39
  def install
@@ -35,6 +41,10 @@ module ElasticAPM
35
41
  alias perform_request_without_apm perform_request
36
42
 
37
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
+
38
48
  name = format(NAME_FORMAT, method, path)
39
49
  statement = []
40
50
 
@@ -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