datadog 2.21.0 → 2.22.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -1
  3. data/ext/LIBDATADOG_DEVELOPMENT.md +60 -0
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/libdatadog_api/ddsketch.c +106 -0
  6. data/ext/libdatadog_api/init.c +3 -0
  7. data/ext/libdatadog_api/library_config.c +35 -27
  8. data/ext/libdatadog_api/process_discovery.c +19 -13
  9. data/ext/libdatadog_extconf_helpers.rb +1 -1
  10. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +26 -0
  11. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +59 -0
  12. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +29 -0
  13. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +26 -0
  14. data/lib/datadog/appsec/api_security/endpoint_collection.rb +10 -0
  15. data/lib/datadog/appsec/assets/waf_rules/README.md +30 -36
  16. data/lib/datadog/appsec/assets/waf_rules/recommended.json +359 -4
  17. data/lib/datadog/appsec/assets/waf_rules/strict.json +43 -2
  18. data/lib/datadog/appsec/compressed_json.rb +1 -1
  19. data/lib/datadog/appsec/configuration/settings.rb +9 -0
  20. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +3 -1
  21. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +3 -2
  22. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +3 -1
  23. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +3 -1
  24. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -4
  25. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +5 -1
  26. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +7 -2
  27. data/lib/datadog/appsec/contrib/rails/patcher.rb +30 -0
  28. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +3 -1
  29. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +10 -4
  30. data/lib/datadog/appsec/event.rb +12 -14
  31. data/lib/datadog/appsec/metrics/collector.rb +19 -3
  32. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +2 -1
  33. data/lib/datadog/appsec/monitor/gateway/watcher.rb +4 -4
  34. data/lib/datadog/appsec/remote.rb +25 -13
  35. data/lib/datadog/appsec/security_engine/result.rb +28 -9
  36. data/lib/datadog/appsec/security_engine/runner.rb +17 -7
  37. data/lib/datadog/appsec/security_event.rb +5 -7
  38. data/lib/datadog/core/configuration/components.rb +14 -6
  39. data/lib/datadog/core/configuration/stable_config.rb +10 -0
  40. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  41. data/lib/datadog/core/configuration.rb +1 -1
  42. data/lib/datadog/core/ddsketch.rb +21 -0
  43. data/lib/datadog/core/environment/yjit.rb +2 -1
  44. data/lib/datadog/core/pin.rb +4 -8
  45. data/lib/datadog/core/process_discovery.rb +4 -2
  46. data/lib/datadog/core/remote/component.rb +4 -6
  47. data/lib/datadog/core/telemetry/component.rb +11 -0
  48. data/lib/datadog/core/telemetry/emitter.rb +6 -6
  49. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +30 -0
  50. data/lib/datadog/core/telemetry/event.rb +1 -0
  51. data/lib/datadog/core/transport/response.rb +4 -1
  52. data/lib/datadog/core/utils/network.rb +19 -0
  53. data/lib/datadog/di/boot.rb +1 -0
  54. data/lib/datadog/di/component.rb +14 -0
  55. data/lib/datadog/di/context.rb +70 -0
  56. data/lib/datadog/di/el/compiler.rb +164 -0
  57. data/lib/datadog/di/el/evaluator.rb +159 -0
  58. data/lib/datadog/di/el/expression.rb +42 -0
  59. data/lib/datadog/di/el.rb +5 -0
  60. data/lib/datadog/di/error.rb +25 -0
  61. data/lib/datadog/di/instrumenter.rb +101 -32
  62. data/lib/datadog/di/probe.rb +35 -15
  63. data/lib/datadog/di/probe_builder.rb +39 -1
  64. data/lib/datadog/di/probe_manager.rb +3 -2
  65. data/lib/datadog/di/probe_notification_builder.rb +50 -51
  66. data/lib/datadog/di/serializer.rb +151 -7
  67. data/lib/datadog/tracing/component.rb +6 -17
  68. data/lib/datadog/tracing/configuration/dynamic.rb +2 -2
  69. data/lib/datadog/tracing/configuration/settings.rb +3 -3
  70. data/lib/datadog/tracing/contrib/component.rb +2 -2
  71. data/lib/datadog/tracing/contrib/graphql/configuration/settings.rb +7 -0
  72. data/lib/datadog/tracing/contrib/graphql/ext.rb +1 -0
  73. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +53 -28
  74. data/lib/datadog/tracing/metadata/ext.rb +8 -0
  75. data/lib/datadog/version.rb +1 -1
  76. metadata +22 -9
  77. data/ext/libdatadog_api/macos_development.md +0 -26
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Lint/AssignmentInCondition
4
+
3
5
  module Datadog
4
6
  module DI
5
7
  # Builds probe status notification and snapshot payloads.
@@ -40,32 +42,17 @@ module Datadog
40
42
 
41
43
  # Duration is in seconds.
42
44
  # path is the actual path of the instrumented file.
43
- def build_executed(probe,
44
- path: nil, rv: nil, duration: nil, caller_locations: nil,
45
- serialized_locals: nil, args: nil, kwargs: nil, target_self: nil,
46
- serialized_entry_args: nil)
47
- build_snapshot(probe, rv: rv, serialized_locals: serialized_locals,
48
- # Actual path of the instrumented file.
49
- path: path,
50
- duration: duration,
51
- # TODO check how many stack frames we should be keeping/sending,
52
- # this should be all frames for enriched probes and no frames for
53
- # non-enriched probes?
54
- caller_locations: caller_locations,
55
- args: args, kwargs: kwargs,
56
- target_self: target_self,
57
- serialized_entry_args: serialized_entry_args)
45
+ def build_executed(context)
46
+ build_snapshot(context)
58
47
  end
59
48
 
60
- def build_snapshot(probe, rv: nil, serialized_locals: nil, path: nil,
61
- # In Ruby everything is a method, therefore we should always have
62
- # a target self. However, if we are not capturing a snapshot,
63
- # there is no need to pass in the target self.
64
- target_self: nil,
65
- duration: nil, caller_locations: nil,
66
- args: nil, kwargs: nil,
67
- serialized_entry_args: nil)
68
- if probe.capture_snapshot? && !target_self
49
+ NANOSECONDS = 10**9
50
+ MILLISECONDS = 1000
51
+
52
+ def build_snapshot(context)
53
+ probe = context.probe
54
+
55
+ if probe.capture_snapshot? && !context.target_self
69
56
  raise ArgumentError, "Asked to build snapshot with snapshot capture but target_self is nil"
70
57
  end
71
58
 
@@ -74,22 +61,14 @@ module Datadog
74
61
  captures = if probe.capture_snapshot?
75
62
  if probe.method?
76
63
  return_arguments = {
77
- "@return": serializer.serialize_value(rv,
64
+ "@return": serializer.serialize_value(context.return_value,
78
65
  depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
79
66
  attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count),
80
- self: serializer.serialize_value(target_self),
67
+ self: serializer.serialize_value(context.target_self),
81
68
  }
82
69
  {
83
70
  entry: {
84
- # standard:disable all
85
- arguments: if serialized_entry_args
86
- serialized_entry_args
87
- else
88
- (args || kwargs) && serializer.serialize_args(args, kwargs, target_self,
89
- depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
90
- attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
91
- end,
92
- # standard:enable all
71
+ arguments: context.serialized_entry_args,
93
72
  },
94
73
  return: {
95
74
  arguments: return_arguments,
@@ -98,10 +77,10 @@ module Datadog
98
77
  }
99
78
  elsif probe.line?
100
79
  {
101
- lines: serialized_locals && {
80
+ lines: (locals = context.serialized_locals) && {
102
81
  probe.line_no => {
103
- locals: serialized_locals,
104
- arguments: {self: serializer.serialize_value(target_self)},
82
+ locals: locals,
83
+ arguments: {self: serializer.serialize_value(context.target_self)},
105
84
  },
106
85
  },
107
86
  }
@@ -110,7 +89,7 @@ module Datadog
110
89
 
111
90
  location = if probe.line?
112
91
  {
113
- file: path,
92
+ file: context.path,
114
93
  lines: [probe.line_no],
115
94
  }
116
95
  elsif probe.method?
@@ -120,17 +99,23 @@ module Datadog
120
99
  }
121
100
  end
122
101
 
123
- stack = if caller_locations
102
+ stack = if caller_locations = context.caller_locations
124
103
  format_caller_locations(caller_locations)
125
104
  end
126
105
 
127
106
  timestamp = timestamp_now
107
+ message = nil
108
+ evaluation_errors = []
109
+ if segments = probe.template_segments
110
+ message, evaluation_errors = evaluate_template(segments, context)
111
+ end
112
+ duration = context.duration
128
113
  {
129
114
  service: settings.service,
130
115
  "debugger.snapshot": {
131
116
  id: SecureRandom.uuid,
132
117
  timestamp: timestamp,
133
- evaluationErrors: [],
118
+ evaluationErrors: evaluation_errors,
134
119
  probe: {
135
120
  id: probe.id,
136
121
  version: 0,
@@ -143,7 +128,7 @@ module Datadog
143
128
  },
144
129
  # In python tracer duration is under debugger.snapshot,
145
130
  # but UI appears to expect it here at top level.
146
- duration: duration ? (duration * 10**9).to_i : 0,
131
+ duration: duration ? (duration * NANOSECONDS).to_i : 0,
147
132
  host: nil,
148
133
  logger: {
149
134
  name: probe.file,
@@ -160,8 +145,7 @@ module Datadog
160
145
  "dd.trace_id": active_trace&.id&.to_s,
161
146
  "dd.span_id": active_span&.id&.to_s,
162
147
  ddsource: 'dd_debugger',
163
- message: probe.template && evaluate_template(probe.template,
164
- duration: duration ? duration * 1000 : 0),
148
+ message: message,
165
149
  timestamp: timestamp,
166
150
  }
167
151
  end
@@ -192,16 +176,29 @@ module Datadog
192
176
  end
193
177
  end
194
178
 
195
- def evaluate_template(template, **vars)
196
- message = template.dup
197
- vars.each do |key, value|
198
- message.gsub!("{@#{key}}") { value.to_s }
199
- end
200
- message
179
+ def evaluate_template(template_segments, context)
180
+ evaluation_errors = []
181
+ message = template_segments.map do |segment|
182
+ case segment
183
+ when String
184
+ segment
185
+ when EL::Expression
186
+ serializer.serialize_value_for_message(segment.evaluate(context))
187
+ else
188
+ raise ArgumentError, "Invalid template segment type: #{segment}"
189
+ end
190
+ rescue => exc
191
+ evaluation_errors << {
192
+ message: "#{exc.class}: #{exc}",
193
+ expr: segment.dsl_expr,
194
+ }
195
+ '[evaluation error]'
196
+ end.join
197
+ [message, evaluation_errors]
201
198
  end
202
199
 
203
200
  def timestamp_now
204
- (Core::Utils::Time.now.to_f * 1000).to_i
201
+ (Core::Utils::Time.now.to_f * MILLISECONDS).to_i
205
202
  end
206
203
 
207
204
  def active_trace
@@ -218,3 +215,5 @@ module Datadog
218
215
  end
219
216
  end
220
217
  end
218
+
219
+ # rubocop:enable Lint/AssignmentInCondition
@@ -64,6 +64,9 @@ module Datadog
64
64
  # a common base class but are all of different classes) or for Mongoid
65
65
  # models (that do not have a common base class at all but include a
66
66
  # standard Mongoid module).
67
+ #
68
+ # Important: these serializers are NOT used in log messages.
69
+ # They are only used for variables that are captured in the snapshots.
67
70
  @@flat_registry = []
68
71
  def self.register(condition: nil, &block)
69
72
  @@flat_registry << {condition: condition, proc: block}
@@ -79,6 +82,18 @@ module Datadog
79
82
  attr_reader :redactor
80
83
  attr_reader :telemetry
81
84
 
85
+ def combine_args(args, kwargs, target_self)
86
+ counter = 0
87
+ combined = args.each_with_object({}) do |value, c|
88
+ counter += 1
89
+ # Conversion to symbol is needed here to put args ahead of
90
+ # kwargs when they are merged below.
91
+ c[:"arg#{counter}"] = value
92
+ end.update(kwargs)
93
+ combined[:self] = target_self
94
+ combined
95
+ end
96
+
82
97
  # Serializes positional and keyword arguments to a method,
83
98
  # as obtained by a method probe.
84
99
  #
@@ -93,13 +108,7 @@ module Datadog
93
108
  def serialize_args(args, kwargs, target_self,
94
109
  depth: settings.dynamic_instrumentation.max_capture_depth,
95
110
  attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count)
96
- counter = 0
97
- combined = args.each_with_object({}) do |value, c|
98
- counter += 1
99
- # Conversion to symbol is needed here to put args ahead of
100
- # kwargs when they are merged below.
101
- c[:"arg#{counter}"] = value
102
- end.update(kwargs).update(self: target_self)
111
+ combined = combine_args(args, kwargs, target_self)
103
112
  serialize_vars(combined, depth: depth, attribute_count: attribute_count)
104
113
  end
105
114
 
@@ -150,6 +159,8 @@ module Datadog
150
159
  end
151
160
 
152
161
  serialized = {type: class_name(cls)}
162
+ # https://github.com/soutaro/steep/issues/1860
163
+ # @type var serialized: untyped
153
164
  case value
154
165
  when NilClass
155
166
  serialized.update(isNull: true)
@@ -261,8 +272,120 @@ module Datadog
261
272
  end
262
273
  end
263
274
 
275
+ # This method is used for serializing arbitrary values into log messages.
276
+ # Because the output is meant to be human-readable, we cannot use
277
+ # the "normal" serialization format which is meant to be machine-readable.
278
+ # Serialize objects with depth of 1 and include the class name.
279
+ #
280
+ # Note that this method does not (currently) utilize the custom
281
+ # serializers that the "normal" serialization logic uses.
282
+ #
283
+ # This serializer differs from the RFC in two ways:
284
+ # 1. We omit the middle of long strings rather than the end,
285
+ # and also the inner entries in arrays/hashes/objects.
286
+ # 2. We use Ruby-ish syntax for hashes and objects.
287
+ #
288
+ # We also use the Ruby-like syntax for symbols, which don't exist
289
+ # in other languages.
290
+ def serialize_value_for_message(value, depth = 1)
291
+ # This method is more verbose than "normal" Ruby code to avoid
292
+ # array allocations.
293
+ case value
294
+ when NilClass
295
+ 'nil'
296
+ when Integer, Float, TrueClass, FalseClass, Time, Date
297
+ value.to_s
298
+ when String
299
+ serialize_string_or_symbol_for_message(value)
300
+ when Symbol
301
+ ':' + serialize_string_or_symbol_for_message(value)
302
+ when Array
303
+ return '...' if depth <= 0
304
+
305
+ max = max_capture_collection_size_for_message
306
+ if value.length > max
307
+ value_ = value[0...max - 1] || []
308
+ value_ << '...'
309
+ value_ << value[-1]
310
+ value = value_
311
+ end
312
+ '[' + value.map do |item|
313
+ serialize_value_for_message(item, depth - 1)
314
+ end.join(', ') + ']'
315
+ when Hash
316
+ return '...' if depth <= 0
317
+
318
+ max = max_capture_collection_size_for_message
319
+ keys = value.keys
320
+ truncated = false
321
+ if value.length > max
322
+ keys_ = keys[0...max - 1] || []
323
+ keys_ << keys[-1]
324
+ keys = keys_
325
+ truncated = true
326
+ end
327
+ serialized = keys.map do |key|
328
+ "#{serialize_value_for_message(key, depth - 1)} => #{serialize_value_for_message(value[key], depth - 1)}"
329
+ end
330
+ if truncated
331
+ serialized[serialized.length] = serialized[serialized.length - 1]
332
+ serialized[serialized.length - 2] = '...'
333
+ end
334
+ "{#{serialized.join(", ")}}"
335
+ else
336
+ return '...' if depth <= 0
337
+
338
+ vars = value.instance_variables
339
+ truncated = false
340
+ max = max_capture_attribute_count_for_message
341
+ if vars.length > max
342
+ vars_ = vars[0...max - 1] || []
343
+ vars_ << vars[-1]
344
+ truncated = true
345
+ vars = vars_
346
+ end
347
+ serialized = vars.map do |var|
348
+ # +var+ here is always the instance variable name which is a
349
+ # symbol, we do not need to run it through our serializer.
350
+ "#{var}=#{serialize_value_for_message(value.send(:instance_variable_get, var), depth - 1)}"
351
+ end
352
+ if truncated
353
+ serialized << serialized.last
354
+ serialized[-2] = '...'
355
+ end
356
+ serialized = if serialized.any?
357
+ ' ' + serialized.join(' ')
358
+ end
359
+ "#<#{class_name(value.class)}#{serialized}>"
360
+ end
361
+ rescue => exc
362
+ telemetry&.report(exc, description: "Error serializing for message")
363
+ # TODO class_name(foo) can also fail, which we don't handle here.
364
+ # Telemetry reporting could potentially also fail?
365
+ "#<#{class_name(value.class)}: serialization error>"
366
+ end
367
+
264
368
  private
265
369
 
370
+ MAX_MESSAGE_COLLECTION_SIZE = 3
371
+ MAX_MESSAGE_ATTRIBUTE_COUNT = 5
372
+
373
+ def max_capture_collection_size_for_message
374
+ max = settings.dynamic_instrumentation.max_capture_collection_size
375
+ if max > MAX_MESSAGE_COLLECTION_SIZE
376
+ max = MAX_MESSAGE_COLLECTION_SIZE
377
+ end
378
+ max
379
+ end
380
+
381
+ def max_capture_attribute_count_for_message
382
+ max = settings.dynamic_instrumentation.max_capture_attribute_count
383
+ if max > MAX_MESSAGE_ATTRIBUTE_COUNT
384
+ max = MAX_MESSAGE_ATTRIBUTE_COUNT
385
+ end
386
+ max
387
+ end
388
+
266
389
  # Returns the name for the specified class object.
267
390
  #
268
391
  # Ruby can have nameless classes, e.g. Class.new is a class object
@@ -273,6 +396,27 @@ module Datadog
273
396
  # and we don't want to invoke user code.
274
397
  cls.name || "[Unnamed class]"
275
398
  end
399
+
400
+ def serialize_string_or_symbol_for_message(value)
401
+ max = settings.dynamic_instrumentation.max_capture_string_length
402
+ if max > 100
403
+ max = 100
404
+ end
405
+ value = value.to_s
406
+ if (length = value.length) > max
407
+ if max < 5
408
+ value[0...max]
409
+ else
410
+ upper = length - max / 2 + 1
411
+ if max % 2 == 0
412
+ upper += 1
413
+ end
414
+ value[0...max / 2 - 1] + '...' + value[upper...length]
415
+ end
416
+ else
417
+ value
418
+ end
419
+ end
276
420
  end
277
421
  end
278
422
  end
@@ -12,16 +12,7 @@ module Datadog
12
12
  module Tracing
13
13
  # Tracing component
14
14
  module Component
15
- # Methods that interact with component instance fields.
16
- module InstanceMethods
17
- # Hot-swaps with a new sampler.
18
- # This operation acquires the Components lock to ensure
19
- # there is no concurrent modification of the sampler.
20
- def reconfigure_live_sampler
21
- sampler = self.class.build_sampler(Datadog.configuration)
22
- Datadog.send(:safely_synchronize) { tracer.sampler.sampler = sampler }
23
- end
24
- end
15
+ module_function
25
16
 
26
17
  def build_tracer(settings, agent_settings, logger:)
27
18
  # If a custom tracer has been provided, use it instead.
@@ -156,11 +147,6 @@ module Datadog
156
147
  Tracing::Sampling::Span::Sampler.new(rules || [])
157
148
  end
158
149
 
159
- # Configure non-privileged components.
160
- def configure_tracing(settings)
161
- Datadog::Tracing::Contrib::Component.configure(settings)
162
- end
163
-
164
150
  # Sampler wrapper component, to allow for hot-swapping
165
151
  # the sampler instance used by the tracer.
166
152
  # Swapping samplers happens during Dynamic Configuration.
@@ -182,8 +168,7 @@ module Datadog
182
168
  end
183
169
  end
184
170
 
185
- private
186
-
171
+ # @api private
187
172
  def build_tracer_tags(settings)
188
173
  settings.tags.dup.tap do |tags|
189
174
  tags[Core::Environment::Ext::TAG_ENV] = settings.env unless settings.env.nil?
@@ -193,6 +178,7 @@ module Datadog
193
178
 
194
179
  # Build a post-sampler that limits the rate of traces to one per `seconds`.
195
180
  # E.g.: `build_rate_limit_post_sampler(seconds: 60)` will limit the rate to one trace per minute.
181
+ # @api private
196
182
  def build_rate_limit_post_sampler(seconds:)
197
183
  Tracing::Sampling::RuleSampler.new(
198
184
  rate_limiter: Datadog::Core::TokenBucket.new(1.0 / seconds, 1.0),
@@ -200,11 +186,13 @@ module Datadog
200
186
  )
201
187
  end
202
188
 
189
+ # @api private
203
190
  def build_test_mode_trace_flush(settings)
204
191
  # If context flush behavior is provided, use it instead.
205
192
  settings.tracing.test_mode.trace_flush || build_trace_flush(settings)
206
193
  end
207
194
 
195
+ # @api private
208
196
  def build_test_mode_sampler
209
197
  # Do not sample any spans for tests; all must be preserved.
210
198
  # Set priority sampler to ensure the agent doesn't drop any traces.
@@ -214,6 +202,7 @@ module Datadog
214
202
  )
215
203
  end
216
204
 
205
+ # @api private
217
206
  def build_test_mode_writer(settings, agent_settings)
218
207
  writer_options = settings.tracing.test_mode.writer_options || {}
219
208
 
@@ -41,7 +41,7 @@ module Datadog
41
41
  # Ensures sampler is rebuilt and new configuration is applied
42
42
  def call(tracing_sampling_rate)
43
43
  super
44
- Datadog.send(:components).reconfigure_live_sampler
44
+ Datadog.send(:components).reconfigure_sampler
45
45
  end
46
46
 
47
47
  protected
@@ -79,7 +79,7 @@ module Datadog
79
79
  end
80
80
 
81
81
  super
82
- Datadog.send(:components).reconfigure_live_sampler
82
+ Datadog.send(:components).reconfigure_sampler
83
83
  end
84
84
 
85
85
  protected
@@ -96,7 +96,7 @@ module Datadog
96
96
  # Note: Alias (DD_TRACE_PROPAGATION_STYLE) defined in supported-configurations.json
97
97
  o.env Configuration::Ext::Distributed::ENV_PROPAGATION_STYLE
98
98
  o.default []
99
- o.after_set do |styles|
99
+ o.after_set do |styles, _, precedence|
100
100
  next if styles.empty?
101
101
 
102
102
  # Make values case-insensitive
@@ -110,8 +110,8 @@ module Datadog
110
110
  false
111
111
  end
112
112
  end
113
- set_option(:propagation_style_extract, styles)
114
- set_option(:propagation_style_inject, styles)
113
+ set_option(:propagation_style_extract, styles, precedence: precedence)
114
+ set_option(:propagation_style_inject, styles, precedence: precedence)
115
115
  end
116
116
  end
117
117
 
@@ -9,13 +9,13 @@ module Datadog
9
9
  # Register a callback to be invoked when components are reconfigured.
10
10
  # @param name [String] the name of the integration
11
11
  # @param callback [Proc] the callback to invoke
12
- # @yieldparam config [Datadog::Configuration] the configuration to pass to callbacks
12
+ # @yieldparam config [Datadog::Core::Configuration::Settings] the configuration to pass to callbacks
13
13
  def register(name, &callback)
14
14
  @registry[name] = callback
15
15
  end
16
16
 
17
17
  # Invoke all registered callbacks with the given configuration.
18
- # @param config [Datadog::Configuration] the configuration to pass to callbacks
18
+ # @param config [Datadog::Core::Configuration::Settings] the configuration to pass to callbacks
19
19
  def configure(config)
20
20
  @registry.each do |name, callback|
21
21
  callback.call(config)
@@ -58,6 +58,13 @@ module Datadog
58
58
  o.default []
59
59
  o.env_parser { |v| ErrorExtensionEnvParser.call(v) }
60
60
  end
61
+
62
+ # Surface GraphQL errors in Error Tracking.
63
+ option :error_tracking do |o|
64
+ o.env Ext::ENV_ERROR_TRACKING
65
+ o.type :bool
66
+ o.default false
67
+ end
61
68
  end
62
69
  end
63
70
  end
@@ -13,6 +13,7 @@ module Datadog
13
13
  ENV_ANALYTICS_SAMPLE_RATE = 'DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE'
14
14
  ENV_WITH_UNIFIED_TRACER = 'DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER'
15
15
  ENV_ERROR_EXTENSIONS = 'DD_TRACE_GRAPHQL_ERROR_EXTENSIONS'
16
+ ENV_ERROR_TRACKING = 'DD_TRACE_GRAPHQL_ERROR_TRACKING'
16
17
  SERVICE_NAME = 'graphql'
17
18
  TAG_COMPONENT = 'graphql'
18
19
 
@@ -11,11 +11,45 @@ module Datadog
11
11
  # which is required to use features such as API Catalog.
12
12
  # DEV-3.0: This tracer should be the default one in the next major version.
13
13
  module UnifiedTrace
14
+ include ::GraphQL::Tracing::PlatformTrace
15
+
14
16
  def initialize(*args, **kwargs)
15
17
  @has_prepare_span = respond_to?(:prepare_span)
18
+
19
+ # Cache configuration values to avoid repeated lookups
20
+ config = Datadog.configuration.tracing[:graphql]
21
+ @service_name = config[:service_name]
22
+ @analytics_enabled = config[:analytics_enabled]
23
+ @analytics_sample_rate = config[:analytics_sample_rate]
24
+ @error_extensions_config = config[:error_extensions]
25
+
26
+ load_error_event_attributes(config[:error_tracking])
27
+
16
28
  super
17
29
  end
18
30
 
31
+ def load_error_event_attributes(error_tracking)
32
+ if error_tracking
33
+ @event_name = Tracing::Metadata::Ext::Errors::EVENT_NAME
34
+ @message_key = Tracing::Metadata::Ext::Errors::ATTRIBUTE_MESSAGE
35
+ @type_key = Tracing::Metadata::Ext::Errors::ATTRIBUTE_TYPE
36
+ @stacktrace_key = Tracing::Metadata::Ext::Errors::ATTRIBUTE_STACKTRACE
37
+ @locations_key = 'graphql.error.locations'
38
+ @path_key = 'graphql.error.path'
39
+ @extensions_key = 'graphql.error.extensions.'
40
+ else
41
+ @event_name = Ext::EVENT_QUERY_ERROR
42
+ @message_key = 'message'
43
+ @type_key = 'type'
44
+ @stacktrace_key = 'stacktrace'
45
+ @locations_key = 'locations'
46
+ @path_key = 'path'
47
+ @extensions_key = 'extensions.'
48
+ end
49
+ end
50
+
51
+ private :load_error_event_attributes
52
+
19
53
  def lex(*args, query_string:, **kwargs)
20
54
  trace(proc { super }, 'lex', query_string, query_string: query_string)
21
55
  end
@@ -56,7 +90,7 @@ module Datadog
56
90
  span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_SERVER)
57
91
 
58
92
  span.set_tag('graphql.source', query.query_string)
59
- span.set_tag('graphql.operation.type', query.selected_operation.operation_type)
93
+ span.set_tag('graphql.operation.type', query.selected_operation&.operation_type)
60
94
  if query.selected_operation_name
61
95
  span.set_tag(
62
96
  'graphql.operation.name',
@@ -130,8 +164,6 @@ module Datadog
130
164
  resolve_type_span(proc { super }, 'resolve_type_lazy', **kwargs)
131
165
  end
132
166
 
133
- include ::GraphQL::Tracing::PlatformTrace
134
-
135
167
  def platform_field_key(field, *args, **kwargs)
136
168
  field.path
137
169
  end
@@ -156,16 +188,14 @@ module Datadog
156
188
  # @param kwargs [Hash] the arguments to pass to `prepare_span`
157
189
  # @yield [Span] the block to run before the trace, same as the `before` parameter
158
190
  def trace(callable, trace_key, resource, before = nil, after = nil, **kwargs, &before_block)
159
- config = Datadog.configuration.tracing[:graphql]
160
-
161
191
  Tracing.trace(
162
192
  "graphql.#{trace_key}",
163
193
  type: 'graphql',
164
194
  resource: resource,
165
- service: config[:service_name]
195
+ service: @service_name
166
196
  ) do |span|
167
- if Contrib::Analytics.enabled?(config[:analytics_enabled])
168
- Contrib::Analytics.set_sample_rate(span, config[:analytics_sample_rate])
197
+ if Contrib::Analytics.enabled?(@analytics_enabled)
198
+ Contrib::Analytics.set_sample_rate(span, @analytics_sample_rate)
169
199
  end
170
200
 
171
201
  # A sanity check for us.
@@ -198,34 +228,29 @@ module Datadog
198
228
  end
199
229
 
200
230
  def operation_resource(operation)
201
- if operation.name
231
+ if operation&.name
202
232
  "#{operation.operation_type} #{operation.name}"
203
233
  else
204
- "anonymous"
234
+ 'anonymous'
205
235
  end
206
236
  end
207
237
 
208
238
  # Create a Span Event for each error that occurs at query level.
209
- #
210
- # These are represented in the Datadog App as special GraphQL errors,
211
- # given their event name `dd.graphql.query.error`.
212
239
  def add_query_error_events(span, errors)
213
- capture_extensions = Datadog.configuration.tracing[:graphql][:error_extensions]
214
240
  errors.each do |error|
215
- extensions = if !capture_extensions.empty? && (extensions = error.extensions)
241
+ attributes = if !@error_extensions_config.empty? && (extensions = error.extensions)
216
242
  # Capture extensions, ensuring all values are primitives
217
243
  extensions.each_with_object({}) do |(key, value), hash|
218
- next unless capture_extensions.include?(key.to_s)
244
+ next unless @error_extensions_config.include?(key.to_s)
219
245
 
220
246
  value = case value
221
247
  when TrueClass, FalseClass, Integer, Float
222
248
  value
223
249
  else
224
- # Stringify anything that is not a boolean or a number
225
250
  value.to_s
226
251
  end
227
252
 
228
- hash["extensions.#{key}"] = value
253
+ hash[@extensions_key + key.to_s] = value
229
254
  end
230
255
  else
231
256
  {}
@@ -235,16 +260,16 @@ module Datadog
235
260
  # This is an unwritten contract in the `graphql` library.
236
261
  # See for an example: https://github.com/rmosolgo/graphql-ruby/blob/0afa241775e5a113863766cce126214dee093464/lib/graphql/execution_error.rb#L32
237
262
  graphql_error = error.to_h
238
- error = Core::Error.build_from(error)
239
-
240
- span.span_events << Datadog::Tracing::SpanEvent.new(
241
- Ext::EVENT_QUERY_ERROR,
242
- attributes: extensions.merge!(
243
- message: graphql_error['message'],
244
- type: error.type,
245
- stacktrace: error.backtrace,
246
- locations: serialize_error_locations(graphql_error['locations']),
247
- path: graphql_error['path'],
263
+ parsed_error = Core::Error.build_from(error)
264
+
265
+ span.span_events << SpanEvent.new(
266
+ @event_name,
267
+ attributes: attributes.merge!(
268
+ @type_key => parsed_error.type,
269
+ @stacktrace_key => parsed_error.backtrace,
270
+ @message_key => graphql_error['message'],
271
+ @locations_key => serialize_error_locations(graphql_error['locations']),
272
+ @path_key => graphql_error['path'],
248
273
  )
249
274
  )
250
275
  end