elastic-apm 3.7.0 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,6 +42,7 @@ module ElasticAPM
42
42
  option :breakdown_metrics, type: :bool, default: true
43
43
  option :capture_body, type: :string, default: 'off'
44
44
  option :capture_headers, type: :bool, default: true
45
+ option :capture_elasticsearch_queries, type: :bool, default: false
45
46
  option :capture_env, type: :bool, default: true
46
47
  option :central_config, type: :bool, default: true
47
48
  option :current_user_email_method, type: :string, default: 'email'
@@ -78,6 +79,7 @@ module ElasticAPM
78
79
  option :sanitize_field_names, type: :list, default: [], converter: WildcardPatternList.new
79
80
  option :server_ca_cert, type: :string
80
81
  option :service_name, type: :string
82
+ option :service_node_name, type: :string
81
83
  option :service_version, type: :string
82
84
  option :source_lines_error_app_frames, type: :int, default: 5
83
85
  option :source_lines_error_library_frames, type: :int, default: 0
@@ -158,13 +160,12 @@ module ElasticAPM
158
160
  end
159
161
 
160
162
  def replace_options(new_options)
161
- return unless new_options
163
+ return if new_options.nil? || new_options.empty?
162
164
  options_copy = @options.dup
163
165
  new_options.each do |key, value|
164
166
  options_copy.fetch(key.to_sym).set(value)
165
167
  end
166
168
  @options = options_copy
167
- set_log_level(logger)
168
169
  end
169
170
 
170
171
  def app=(app)
@@ -286,15 +287,10 @@ module ElasticAPM
286
287
 
287
288
  def build_logger
288
289
  Logger.new(log_path == '-' ? STDOUT : log_path).tap do |logger|
289
- set_log_level(logger)
290
+ logger.level = log_level
290
291
  end
291
292
  end
292
293
 
293
- def set_log_level(logger)
294
- return unless logger
295
- logger.level = log_level
296
- end
297
-
298
294
  def app_type?(app)
299
295
  if defined?(::Rails::Application) && app.is_a?(::Rails::Application)
300
296
  return :rails
@@ -71,7 +71,7 @@ module ElasticAPM
71
71
  transaction.done 'success'
72
72
  rescue ::Exception => e
73
73
  ElasticAPM.report(e, handled: false)
74
- transaction.done 'error'
74
+ transaction.done 'error' if transaction
75
75
  raise
76
76
  ensure
77
77
  ElasticAPM.end_transaction
@@ -91,7 +91,7 @@ module ElasticAPM
91
91
  def trace_context(call)
92
92
  TraceContext.parse(metadata: call.metadata)
93
93
  rescue TraceContext::InvalidTraceparentHeader
94
- warn "Couldn't parse invalid trace context header: #{header.inspect}"
94
+ warn "Couldn't parse invalid trace context header: #{call.metadata}"
95
95
  nil
96
96
  end
97
97
  end
@@ -30,14 +30,17 @@ module ElasticAPM
30
30
 
31
31
  attr_reader :name, :version
32
32
  end
33
+
33
34
  class Agent < Versioned; end
34
35
  class Framework < Versioned; end
35
36
  class Language < Versioned; end
36
37
  class Runtime < Versioned; end
38
+
37
39
  def initialize(config)
38
40
  @config = config
39
41
 
40
42
  @name = @config.service_name
43
+ @node_name = @config.service_node_name
41
44
  @environment = @config.environment
42
45
  @agent = Agent.new(name: 'ruby', version: VERSION)
43
46
  @framework = Framework.new(
@@ -49,8 +52,8 @@ module ElasticAPM
49
52
  @version = @config.service_version || Util.git_sha
50
53
  end
51
54
 
52
- attr_reader :name, :environment, :agent, :framework, :language, :runtime,
53
- :version
55
+ attr_reader :name, :node_name, :environment, :agent, :framework, :language,
56
+ :runtime, :version
54
57
 
55
58
  private
56
59
 
@@ -39,7 +39,7 @@ module ElasticAPM
39
39
  @span_context
40
40
  end
41
41
 
42
- def set_label(key, val)
42
+ def set_tag(key, val)
43
43
  if elastic_span.is_a?(Transaction)
44
44
  case key.to_s
45
45
  when 'type'
@@ -54,6 +54,8 @@ module ElasticAPM
54
54
  else
55
55
  elastic_span.context.labels[key] = val
56
56
  end
57
+
58
+ self
57
59
  end
58
60
 
59
61
  def set_baggage_item(_key, _value)
@@ -78,18 +80,12 @@ module ElasticAPM
78
80
  ElasticAPM.report_message message
79
81
  end
80
82
  end
81
-
82
83
  # rubocop:enable Lint/UnusedMethodArgument
83
- def finish(clock_end: Util.monotonic_micros, end_time: nil)
84
- return unless (agent = ElasticAPM.agent)
85
84
 
86
- if end_time
87
- warn '[ElasticAPM] DEPRECATED: Setting a custom end time as a ' \
88
- '`Time` is deprecated. Use `clock_end:` and monotonic time instead.'
89
- clock_end = end_time
90
- end
85
+ def finish(end_time: Time.now)
86
+ return unless (agent = ElasticAPM.agent)
91
87
 
92
- elastic_span.done clock_end: clock_end
88
+ elastic_span.done clock_end: Util.micros(end_time)
93
89
 
94
90
  case elastic_span
95
91
  when ElasticAPM::Transaction
@@ -114,6 +110,8 @@ module ElasticAPM
114
110
 
115
111
  # @api private
116
112
  class SpanContext
113
+ extend Forwardable
114
+
117
115
  def initialize(trace_context:, baggage: nil)
118
116
  if baggage
119
117
  ElasticAPM.agent.config.logger.warn(
@@ -125,10 +123,27 @@ module ElasticAPM
125
123
  end
126
124
 
127
125
  attr_accessor :trace_context
126
+ def_delegators :trace_context, :trace_id, :id, :parent_id
127
+
128
+ def self.from_header(header)
129
+ return unless header
130
+
131
+ trace_context = ElasticAPM::TraceContext.parse(header)
132
+ return unless trace_context
133
+
134
+ trace_context.traceparent.id = trace_context.parent_id
135
+ trace_context.traceparent.parent_id = nil
136
+
137
+ from_trace_context(trace_context)
138
+ end
128
139
 
129
140
  def self.from_trace_context(trace_context)
130
141
  new(trace_context: trace_context)
131
142
  end
143
+
144
+ def child
145
+ self.class.from_trace_context(trace_context.child)
146
+ end
132
147
  end
133
148
 
134
149
  # @api private
@@ -210,7 +225,7 @@ module ElasticAPM
210
225
  child_of: nil,
211
226
  references: nil,
212
227
  start_time: Time.now,
213
- labels: {},
228
+ tags: {},
214
229
  ignore_active_scope: false,
215
230
  finish_on_close: true,
216
231
  **
@@ -220,14 +235,14 @@ module ElasticAPM
220
235
  child_of: child_of,
221
236
  references: references,
222
237
  start_time: start_time,
223
- labels: labels,
238
+ tags: tags,
224
239
  ignore_active_scope: ignore_active_scope
225
240
  )
226
241
  scope = scope_manager.activate(span, finish_on_close: finish_on_close)
227
242
 
228
243
  if block_given?
229
244
  begin
230
- yield scope
245
+ return yield scope
231
246
  ensure
232
247
  scope.close
233
248
  end
@@ -243,7 +258,7 @@ module ElasticAPM
243
258
  child_of: nil,
244
259
  references: nil,
245
260
  start_time: Time.now,
246
- labels: {},
261
+ tags: {},
247
262
  ignore_active_scope: false,
248
263
  **
249
264
  )
@@ -280,7 +295,7 @@ module ElasticAPM
280
295
  span_context ||=
281
296
  SpanContext.from_trace_context(elastic_span.trace_context)
282
297
 
283
- labels.each do |key, value|
298
+ tags.each do |key, value|
284
299
  elastic_span.context.labels[key] = value
285
300
  end
286
301
 
@@ -293,7 +308,7 @@ module ElasticAPM
293
308
 
294
309
  def inject(span_context, format, carrier)
295
310
  case format
296
- when ::OpenTracing::FORMAT_RACK
311
+ when ::OpenTracing::FORMAT_RACK, ::OpenTracing::FORMAT_TEXT_MAP
297
312
  carrier['elastic-apm-traceparent'] =
298
313
  span_context.traceparent.to_header
299
314
  else
@@ -304,10 +319,16 @@ module ElasticAPM
304
319
  def extract(format, carrier)
305
320
  case format
306
321
  when ::OpenTracing::FORMAT_RACK
307
- ElasticAPM::TraceContext
308
- .parse(carrier['HTTP_ELASTIC_APM_TRACEPARENT'])
322
+ SpanContext.from_header(
323
+ carrier['HTTP_ELASTIC_APM_TRACEPARENT']
324
+ )
325
+ when ::OpenTracing::FORMAT_TEXT_MAP
326
+ SpanContext.from_header(
327
+ carrier['elastic-apm-traceparent']
328
+ )
309
329
  else
310
- warn 'Only extraction from HTTP headers via Rack is available'
330
+ warn 'Only extraction from HTTP headers via Rack or in ' \
331
+ 'text map format are available'
311
332
  nil
312
333
  end
313
334
  rescue ElasticAPM::TraceContext::InvalidTraceparentHeader
@@ -321,9 +342,12 @@ module ElasticAPM
321
342
  references:,
322
343
  ignore_active_scope:
323
344
  )
324
- context_from_child_of(child_of) ||
325
- context_from_references(references) ||
326
- context_from_active_scope(ignore_active_scope)
345
+ context = context_from_child_of(child_of) ||
346
+ context_from_references(references) ||
347
+ context_from_active_scope(ignore_active_scope)
348
+ return context.child if context&.respond_to?(:child)
349
+
350
+ context
327
351
  end
328
352
 
329
353
  def context_from_child_of(child_of)
@@ -344,7 +368,7 @@ module ElasticAPM
344
368
  def context_from_active_scope(ignore_active_scope)
345
369
  if ignore_active_scope
346
370
  ElasticAPM.agent&.config&.logger&.warn(
347
- 'ignore_active_scope might lead to unexpeced results'
371
+ 'ignore_active_scope might lead to unexpected results'
348
372
  )
349
373
  return
350
374
  end
@@ -80,23 +80,25 @@ module ElasticAPM
80
80
  end
81
81
  end
82
82
 
83
- # @api private
84
- module Kernel
85
- private
83
+ unless ENV['ELASTIC_APM_SKIP_REQUIRE_PATCH'] == '1'
84
+ # @api private
85
+ module Kernel
86
+ private
86
87
 
87
- alias require_without_apm require
88
+ alias require_without_apm require
88
89
 
89
- def require(path)
90
- res = require_without_apm(path)
90
+ def require(path)
91
+ res = require_without_apm(path)
91
92
 
92
- begin
93
- ElasticAPM::Spies.hook_into(path)
94
- rescue ::Exception => e
95
- puts "Failed hooking into '#{path}'. Please report this at " \
96
- 'github.com/elastic/apm-agent-ruby'
97
- puts e.backtrace.join("\n")
98
- end
93
+ begin
94
+ ElasticAPM::Spies.hook_into(path)
95
+ rescue ::Exception => e
96
+ puts "Failed hooking into '#{path}'. Please report this at " \
97
+ 'github.com/elastic/apm-agent-ruby'
98
+ puts e.backtrace.join("\n")
99
+ end
99
100
 
100
- res
101
+ res
102
+ end
101
103
  end
102
104
  end
@@ -26,16 +26,30 @@ module ElasticAPM
26
26
  TYPE = 'db'
27
27
  SUBTYPE = 'elasticsearch'
28
28
 
29
+ def self.sanitizer
30
+ @sanitizer ||= ElasticAPM::Transport::Filters::HashSanitizer.new
31
+ end
32
+
29
33
  def install
30
34
  ::Elasticsearch::Transport::Client.class_eval do
31
35
  alias perform_request_without_apm perform_request
32
36
 
33
37
  def perform_request(method, path, *args, &block)
34
38
  name = format(NAME_FORMAT, method, path)
35
- statement = args[0].is_a?(String) ? args[0] : args[0].to_json
39
+ statement = []
40
+
41
+ statement << { params: args&.[](0) }
42
+
43
+ if ElasticAPM.agent.config.capture_elasticsearch_queries
44
+ unless args[1].nil? || args[1].empty?
45
+ statement << {
46
+ body: ElasticAPM::Spies::ElasticsearchSpy.sanitizer.strip_from!(args[1])
47
+ }
48
+ end
49
+ end
36
50
 
37
51
  context = Span::Context.new(
38
- db: { statement: statement },
52
+ db: { statement: statement.reduce({}, :merge).to_json },
39
53
  destination: {
40
54
  name: SUBTYPE,
41
55
  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
@@ -0,0 +1,77 @@
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
+ 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
36
+
37
+ VALUE_FILTERS = [
38
+ # (probably) credit card number
39
+ /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
40
+ ].freeze
41
+
42
+ attr_accessor :key_filters
43
+
44
+ def initialize
45
+ @key_filters = KEY_FILTERS
46
+ end
47
+
48
+ def strip_from!(obj, key_filters = KEY_FILTERS)
49
+ return unless obj&.is_a?(Hash)
50
+
51
+ obj.each do |k, v|
52
+ if filter_key?(k)
53
+ next obj[k] = FILTERED
54
+ end
55
+
56
+ case v
57
+ when Hash
58
+ strip_from!(v)
59
+ when String
60
+ if filter_value?(v)
61
+ obj[k] = FILTERED
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def filter_key?(key)
68
+ @key_filters.any? { |regex| regex.match(key) }
69
+ end
70
+
71
+ def filter_value?(value)
72
+ VALUE_FILTERS.any? { |regex| regex.match(value) }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end