datadog 2.45.0 → 2.46.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/CHANGELOG.md +25 -2
  3. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +4 -0
  4. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
  5. data/ext/datadog_profiling_native_extension/profiling.c +6 -0
  6. data/ext/libdatadog_api/di.c +30 -5
  7. data/ext/libdatadog_api/extconf.rb +2 -0
  8. data/ext/libdatadog_api/otel_thread_context.c +20 -14
  9. data/ext/libdatadog_api/process_discovery.c +15 -1
  10. data/ext/libdatadog_api/trace_exporter.c +105 -25
  11. data/ext/libdatadog_extconf_helpers.rb +1 -1
  12. data/lib/datadog/core/configuration/components.rb +29 -8
  13. data/lib/datadog/core/configuration/supported_configurations.rb +5 -2
  14. data/lib/datadog/core/process_discovery.rb +5 -1
  15. data/lib/datadog/core/remote/client/capabilities.rb +34 -29
  16. data/lib/datadog/core/remote/component.rb +28 -12
  17. data/lib/datadog/core/remote/dispatcher.rb +13 -3
  18. data/lib/datadog/di/boot.rb +1 -0
  19. data/lib/datadog/di/capture_expression_evaluator/result.rb +30 -0
  20. data/lib/datadog/di/capture_expression_evaluator.rb +39 -9
  21. data/lib/datadog/di/component.rb +38 -3
  22. data/lib/datadog/di/configuration.rb +53 -2
  23. data/lib/datadog/di/context.rb +7 -2
  24. data/lib/datadog/di/contrib/active_record.rb +8 -2
  25. data/lib/datadog/di/evaluation_failure.rb +28 -0
  26. data/lib/datadog/di/guardrails_telemetry/reason.rb +87 -0
  27. data/lib/datadog/di/guardrails_telemetry.rb +203 -0
  28. data/lib/datadog/di/instrumenter.rb +65 -15
  29. data/lib/datadog/di/logger.rb +30 -8
  30. data/lib/datadog/di/probe_manager.rb +48 -5
  31. data/lib/datadog/di/probe_notification_builder.rb +28 -26
  32. data/lib/datadog/di/probe_notifier_worker.rb +56 -6
  33. data/lib/datadog/di/serializer.rb +130 -54
  34. data/lib/datadog/di/snapshot_event.rb +38 -0
  35. data/lib/datadog/di/transport/http.rb +13 -3
  36. data/lib/datadog/di/transport/input.rb +32 -7
  37. data/lib/datadog/open_feature/activation.rb +268 -0
  38. data/lib/datadog/open_feature/component.rb +31 -15
  39. data/lib/datadog/open_feature/provider.rb +282 -5
  40. data/lib/datadog/open_feature.rb +31 -1
  41. data/lib/datadog/tracing/component.rb +14 -0
  42. data/lib/datadog/tracing/configuration/settings.rb +3 -1
  43. data/lib/datadog/tracing/otel_thread_context.rb +3 -5
  44. data/lib/datadog/tracing/transport/http/telemetry.rb +94 -0
  45. data/lib/datadog/tracing/transport/http/traces.rb +2 -0
  46. data/lib/datadog/tracing/transport/native/telemetry.rb +17 -0
  47. data/lib/datadog/tracing/transport/native.rb +16 -1
  48. data/lib/datadog/tracing/transport/telemetry.rb +62 -0
  49. data/lib/datadog/tracing/transport/traces.rb +82 -15
  50. data/lib/datadog/version.rb +1 -1
  51. metadata +16 -7
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
3
4
  require_relative "redactor"
5
+ require_relative "guardrails_telemetry"
6
+ require_relative "../core/utils/time"
4
7
 
5
8
  module Datadog
6
9
  module DI
7
- # Serializes captured snapshot to primitive types, which are subsequently
10
+ # Serializes captured snapshots to primitive types, which are subsequently
8
11
  # serialized to JSON and sent to the backend.
9
12
  #
10
- # This class performs actual removal of sensitive values from the
11
- # snapshots. It uses Redactor to determine which values are sensitive
12
- # and need to be removed.
13
+ # Sensitive values are removed from the snapshots.
13
14
  #
14
15
  # Serializer normally ought not to invoke user (application) code,
15
16
  # to guarantee predictable performance. However, objects like ActiveRecord
@@ -19,7 +20,7 @@ module Datadog
19
20
  # thus they won't be captured at all). To accommodate complex objects,
20
21
  # there is an extension mechanism implemented permitting registration
21
22
  # of serializer callbacks for arbitrary types. Applications and libraries
22
- # definining such serializer callbacks should be very careful to
23
+ # defining such serializer callbacks should be very careful to
23
24
  # have predictable performance and avoid exceptions and infinite loops
24
25
  # and other such issues.
25
26
  #
@@ -36,25 +37,21 @@ module Datadog
36
37
  # efficient but there would be additional overhead from passing this
37
38
  # parameter all the time and the API would get more complex.
38
39
  #
39
- # Note: "self" cannot be used as a parameter name in Ruby, therefore
40
- # there should never be a conflict between instance variable
41
- # serialization and method parameters.
42
- #
43
40
  # @api private
44
41
  class Serializer
45
- # Exception classes that should never be caught during serialization.
46
- # These represent fatal conditions (signals, interrupts, system exit)
47
- # that must propagate to the caller. NoMemoryError is deliberately
48
- # excluded (unlike DI::FATAL_EXCEPTION_CLASSES): serialization of large
49
- # or deeply recursive objects can exhaust memory, and the serializer must
50
- # return a safe stub rather than tear the process down.
42
+ # Exception classes that must propagate to the caller during
43
+ # serialization: fatal conditions (signals, interrupts, system exit).
44
+ # NoMemoryError is deliberately excluded (unlike
45
+ # DI::FATAL_EXCEPTION_CLASSES): serialization of large or deeply
46
+ # recursive objects can exhaust memory, and the serializer must return
47
+ # a safe stub rather than terminate the process.
51
48
  SERIALIZABLE_FATAL_EXCEPTION_CLASSES = [SystemExit, SignalException].freeze
52
49
 
53
50
  # Placeholder emitted by #serialize_value_for_message in place of a value
54
- # whose identifier or type matches the redaction configuration. This
55
- # mirrors, for the human-readable log-probe message path, the
56
- # notCapturedReason: "redactedIdent"/"redactedType" gating that
57
- # #serialize_value applies on the snapshot path.
51
+ # whose identifier or type matches the redaction configuration. The
52
+ # message-path counterpart of the +notCapturedReason+
53
+ # "redactedIdent"/"redactedType" markers that #serialize_value produces
54
+ # on the snapshot path.
58
55
  REDACTED_VALUE_FOR_MESSAGE = "[redacted]"
59
56
 
60
57
  # Third-party library integration / custom serializers.
@@ -103,16 +100,42 @@ module Datadog
103
100
  @@flat_registry << {condition: condition, proc: block}
104
101
  end
105
102
 
106
- def initialize(settings, redactor, telemetry: nil)
103
+ # Initializes the serializer.
104
+ #
105
+ # @param settings [Datadog::Core::Configuration::Settings] tracer settings
106
+ # @param redactor [Redactor] redactor gating captured values
107
+ # @param logger [DI::Logger] logger for contained serialization failures
108
+ # @param guardrails_telemetry [GuardrailsTelemetry] emitter for the canonical capture-incomplete metric
109
+ # @param telemetry [Datadog::Core::Telemetry::Component, nil] component serializer errors are reported through
110
+ # @return [void]
111
+ def initialize(settings, redactor, logger:, guardrails_telemetry:, telemetry: nil)
107
112
  @settings = settings
108
113
  @redactor = redactor
114
+ @logger = logger
115
+ @guardrails_telemetry = guardrails_telemetry
109
116
  @telemetry = telemetry
110
117
  end
111
118
 
112
119
  attr_reader :settings
113
120
  attr_reader :redactor
121
+ # The logger.
122
+ # @return [DI::Logger]
123
+ attr_reader :logger
124
+ # The guardrails metric emitter.
125
+ # @return [GuardrailsTelemetry]
126
+ attr_reader :guardrails_telemetry
114
127
  attr_reader :telemetry
115
128
 
129
+ # Emits the canonical capture-incomplete metric for a capture limit
130
+ # enforced while serializing a snapshot, with the snapshot event
131
+ # type.
132
+ #
133
+ # @param reason [String] a GuardrailsTelemetry::Reason constant naming the enforced limit
134
+ # @return [void]
135
+ def record_capture_incomplete(reason)
136
+ guardrails_telemetry.capture_incomplete(reason: reason, event_type: GuardrailsTelemetry::EVENT_TYPE_SNAPSHOT)
137
+ end
138
+
116
139
  def combine_args(args, kwargs, target_self)
117
140
  counter = 0
118
141
  combined = args.each_with_object({}) do |value, c|
@@ -133,31 +156,39 @@ module Datadog
133
156
  # arguments to keyword arguments ("arg1", "arg2", ...) and ensure
134
157
  # the positional arguments are listed first.
135
158
  #
136
- # Instance variables are technically a hash just like kwargs,
137
- # we take them as a separate parameter to avoid a hash merge
138
- # in upstream code.
159
+ # @param args [Array] positional arguments passed to the probed method
160
+ # @param kwargs [Hash{Symbol => Object}] keyword arguments passed to the probed method
161
+ # @param target_self [any] receiver of the probed method invocation
162
+ # @param deadline [Float] absolute capture deadline in monotonic clock seconds
163
+ # @return [Hash{Symbol => Hash}] serialized arguments keyed by argument name
139
164
  def serialize_args(args, kwargs, target_self,
165
+ deadline:,
140
166
  depth: settings.dynamic_instrumentation.max_capture_depth,
141
167
  attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count,
142
168
  length: nil,
143
169
  collection_size: nil)
144
170
  combined = combine_args(args, kwargs, target_self)
145
171
  serialize_vars(combined, depth: depth, attribute_count: attribute_count,
146
- length: length, collection_size: collection_size)
172
+ length: length, collection_size: collection_size, deadline: deadline,)
147
173
  end
148
174
 
149
175
  # Serializes variables captured by a line probe.
150
176
  #
151
177
  # These are normally local variables that exist on a particular line
152
178
  # of executed code.
179
+ #
180
+ # @param vars [Hash{Symbol => Object}] variables to serialize, keyed by variable name
181
+ # @param deadline [Float] absolute capture deadline in monotonic clock seconds
182
+ # @return [Hash{Symbol => Hash}] serialized variables keyed by variable name
153
183
  def serialize_vars(vars,
184
+ deadline:,
154
185
  depth: settings.dynamic_instrumentation.max_capture_depth,
155
186
  attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count,
156
187
  length: nil,
157
188
  collection_size: nil)
158
189
  vars.each_with_object({}) do |(k, v), agg|
159
190
  agg[k] = serialize_value(v, name: k, depth: depth, attribute_count: attribute_count,
160
- length: length, collection_size: collection_size)
191
+ length: length, collection_size: collection_size, deadline: deadline,)
161
192
  end
162
193
  end
163
194
 
@@ -173,7 +204,13 @@ module Datadog
173
204
  # (integers, strings, arrays, hashes).
174
205
  #
175
206
  # Respects string length, collection size and traversal depth limits.
176
- def serialize_value(value, name: nil,
207
+ #
208
+ # @param value [any] value to serialize
209
+ # @param name [Symbol, String, nil] name the value is bound to, used for redaction
210
+ # @param deadline [Float] absolute capture deadline in monotonic clock seconds
211
+ # @return [Hash{Symbol => Object}] serialized value; the {type:, notCapturedReason: "timeout"} stub when the deadline is exceeded
212
+ def serialize_value(value, deadline:,
213
+ name: nil,
177
214
  depth: settings.dynamic_instrumentation.max_capture_depth,
178
215
  attribute_count: nil,
179
216
  length: nil,
@@ -182,6 +219,12 @@ module Datadog
182
219
  attribute_count ||= settings.dynamic_instrumentation.max_capture_attribute_count
183
220
  cls = type || value.class
184
221
  begin
222
+ if deadline_exceeded?(deadline)
223
+ telemetry&.inc(DI::TELEMETRY_NAMESPACE, "serialized_values_skipped_by_timeout", 1)
224
+ record_capture_incomplete(GuardrailsTelemetry::Reason::CAPTURE_TIMEOUT)
225
+ return {type: class_name(cls), notCapturedReason: "timeout"}
226
+ end
227
+
185
228
  if redactor.redact_type?(value)
186
229
  return {type: class_name(cls), notCapturedReason: "redactedType"}
187
230
  end
@@ -213,7 +256,7 @@ module Datadog
213
256
 
214
257
  if condition_result
215
258
  serializer_proc = entry.fetch(:proc)
216
- return serializer_proc.call(self, value, name: nil, depth: depth)
259
+ return serializer_proc.call(self, value, name: nil, depth: depth, deadline: deadline)
217
260
  end
218
261
  end
219
262
  end
@@ -227,8 +270,6 @@ module Datadog
227
270
  when Integer, Float, TrueClass, FalseClass
228
271
  serialized.update(value: value.to_s)
229
272
  when Time
230
- # This path also handles DateTime values although they do not need
231
- # to be explicitly added to the case statement.
232
273
  serialized.update(value: value.iso8601)
233
274
  when Date
234
275
  serialized.update(value: value.to_s)
@@ -246,27 +287,24 @@ module Datadog
246
287
  end
247
288
 
248
289
  # Handle binary strings and invalid UTF-8 by escaping to JSON-safe format.
249
- # See escape_binary_string for details on the escaping format.
250
- #
251
- # Truncate binary data BEFORE escaping to avoid cutting mid-escape-sequence.
252
290
  # For regular strings, the limit is applied to string length in characters.
253
291
  length ||= settings.dynamic_instrumentation.max_capture_string_length
254
292
  max = length
255
293
 
256
294
  if value.encoding == Encoding::BINARY || !value.valid_encoding?
257
- # Truncate binary data BEFORE escaping to avoid cutting mid-escape-sequence
295
+ # Truncate binary data before escaping to avoid cutting mid-escape-sequence
258
296
  # For invalid encodings, use bytesize instead of length to avoid encoding errors
259
297
  original_size = value.bytesize
260
298
  if original_size > max
261
299
  serialized.update(truncated: true, size: original_size)
300
+ record_capture_incomplete(GuardrailsTelemetry::Reason::STRING_LENGTH)
262
301
  value = value.byteslice(0...max)
263
302
  end
264
303
  value = escape_binary_string(value) # steep:ignore ArgumentTypeMismatch
265
- false # Already converted to a new string
266
304
  else
267
- # Truncate non-binary strings
268
305
  if value.length > max
269
306
  serialized.update(truncated: true, size: value.length)
307
+ record_capture_incomplete(GuardrailsTelemetry::Reason::STRING_LENGTH)
270
308
  value = value[0...max]
271
309
  need_dup = false
272
310
  end
@@ -278,23 +316,32 @@ module Datadog
278
316
  when Array
279
317
  if depth <= 0
280
318
  serialized.update(notCapturedReason: "depth")
319
+ record_capture_incomplete(GuardrailsTelemetry::Reason::DEPTH)
281
320
  else
282
321
  collection_size ||= settings.dynamic_instrumentation.max_capture_collection_size
283
322
  max = collection_size
284
323
  if max != 0 && value.length > max
285
324
  serialized.update(notCapturedReason: "collectionSize", size: value.length)
325
+ record_capture_incomplete(GuardrailsTelemetry::Reason::COLLECTION_SIZE)
286
326
  # same steep failure with array slices.
287
327
  # https://github.com/soutaro/steep/issues/1219
288
328
  value = value[0...max] || []
289
329
  end
290
- entries = value.map do |elt|
291
- serialize_value(elt, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count)
330
+ entries = []
331
+ value.each do |elt|
332
+ entry = serialize_value(elt, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count, deadline: deadline)
333
+ entries << entry
334
+ if entry[:notCapturedReason] == "timeout"
335
+ serialized.update(notCapturedReason: "timeout")
336
+ break
337
+ end
292
338
  end
293
339
  serialized.update(elements: entries)
294
340
  end
295
341
  when Hash
296
342
  if depth <= 0
297
343
  serialized.update(notCapturedReason: "depth")
344
+ record_capture_incomplete(GuardrailsTelemetry::Reason::DEPTH)
298
345
  else
299
346
  collection_size ||= settings.dynamic_instrumentation.max_capture_collection_size
300
347
  max = collection_size
@@ -303,17 +350,28 @@ module Datadog
303
350
  value.each do |k, v|
304
351
  if max != 0 && cur >= max
305
352
  serialized.update(notCapturedReason: "collectionSize", size: value.length)
353
+ record_capture_incomplete(GuardrailsTelemetry::Reason::COLLECTION_SIZE)
306
354
  break
307
355
  end
308
356
  cur += 1
309
- entries << [serialize_value(k, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count),
310
- serialize_value(v, name: k, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count)]
357
+ key_entry = serialize_value(k, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count, deadline: deadline)
358
+ if key_entry[:notCapturedReason] == "timeout"
359
+ serialized.update(notCapturedReason: "timeout")
360
+ break
361
+ end
362
+ value_entry = serialize_value(v, name: k, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count, deadline: deadline)
363
+ entries << [key_entry, value_entry]
364
+ if value_entry[:notCapturedReason] == "timeout"
365
+ serialized.update(notCapturedReason: "timeout")
366
+ break
367
+ end
311
368
  end
312
369
  serialized.update(entries: entries)
313
370
  end
314
371
  else
315
372
  if depth <= 0
316
373
  serialized.update(notCapturedReason: "depth")
374
+ record_capture_incomplete(GuardrailsTelemetry::Reason::DEPTH)
317
375
  else
318
376
  fields = {}
319
377
  cur = 0
@@ -334,40 +392,42 @@ module Datadog
334
392
  #
335
393
  # For consistency, we should have some kind of stable order of
336
394
  # instance variables on all supported Ruby runtimes, so that the UI
337
- # stays consistent. Given that initial implementation of Ruby DI
338
- # does not support JRuby, we don't handle JRuby's lack of ordering
339
- # of #instance_variables here, but if JRuby is supported in the
340
- # future this may need to be addressed.
395
+ # stays consistent. Ruby DI does not support JRuby, so JRuby's lack
396
+ # of ordering of #instance_variables is not handled here.
341
397
  ivars = value.instance_variables
342
398
 
343
399
  ivars.each do |ivar|
344
400
  if cur >= attribute_count
345
401
  serialized.update(notCapturedReason: "fieldCount", fields: fields)
402
+ record_capture_incomplete(GuardrailsTelemetry::Reason::FIELD_COUNT)
346
403
  break
347
404
  end
348
405
  cur += 1
349
- fields[ivar] = serialize_value(value.instance_variable_get(ivar), name: ivar, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count)
406
+ entry = serialize_value(value.instance_variable_get(ivar), name: ivar, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count, deadline: deadline)
407
+ fields[ivar] = entry
408
+ if entry[:notCapturedReason] == "timeout"
409
+ serialized.update(notCapturedReason: "timeout")
410
+ break
411
+ end
350
412
  end
351
413
  serialized.update(fields: fields)
352
414
  end
353
415
  end
354
416
  serialized
355
417
  rescue Exception => exc # standard:disable Lint/RescueException
356
- # Re-raise fatal exceptions that should not be caught
357
- # (signals, interrupts, system exit)
358
418
  raise if SERIALIZABLE_FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
359
419
 
360
- # Catch all other exceptions including SystemStackError and NoMemoryError.
361
- # These inherit from Exception (not StandardError) but can occur during
362
- # serialization (e.g., infinite recursion in custom serializers, memory
363
- # exhaustion from large objects) and should return a safe structure
364
- # rather than propagating to the transport layer.
420
+ # Catch all remaining exceptions: SystemStackError and NoMemoryError can
421
+ # occur during serialization (e.g. a custom serializer recursing
422
+ # infinitely) and produce the safe stub.
423
+ record_capture_incomplete(GuardrailsTelemetry::Reason::RUNTIME_ERROR)
424
+ logger.debug { "di: error serializing value of #{class_name(cls)}: #{exc.class}: #{exc.message}" }
365
425
  telemetry&.report(exc, description: "Error serializing")
366
426
  {type: class_name(cls), notSerializedReason: exc.to_s}
367
427
  end
368
428
  end
369
429
 
370
- # This method is used for serializing arbitrary values into log messages.
430
+ # Serializes a value for interpolation into a log-probe message.
371
431
  # Because the output is meant to be human-readable, we cannot use
372
432
  # the "normal" serialization format which is meant to be machine-readable.
373
433
  # Serialize objects with depth of 1 and include the class name.
@@ -386,6 +446,8 @@ module Datadog
386
446
  # +name+, when given, is the identifier the template expression
387
447
  # references at its top level; a redacted identifier yields the
388
448
  # redaction placeholder, mirroring #serialize_value on the snapshot path.
449
+ # Message rendering is capped by depth (1) and
450
+ # MAX_MESSAGE_COLLECTION_SIZE / MAX_MESSAGE_ATTRIBUTE_COUNT.
389
451
  def serialize_value_for_message(value, depth: 1, name: nil)
390
452
  # This method is more verbose than "normal" Ruby code to avoid
391
453
  # array allocations.
@@ -475,13 +537,27 @@ module Datadog
475
537
  raise if SERIALIZABLE_FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
476
538
 
477
539
  telemetry&.report(exc, description: "Error serializing for message")
478
- # TODO class_name(foo) can also fail, which we don't handle here.
479
- # Telemetry reporting could potentially also fail?
540
+ # TODO handle class_name raising here as well.
480
541
  "#<#{class_name(value.class)}: serialization error>"
481
542
  end
482
543
 
544
+ # Computes the absolute monotonic deadline (in seconds) for a capture
545
+ # point from max_time_to_serialize_ms. Resolve once per capture point and
546
+ # share across all values serialized on the built-in paths so the
547
+ # budget is not exceeded.
548
+ #
549
+ # @return [Float] absolute monotonic clock reading, in seconds, at
550
+ # which the capture budget is exhausted
551
+ def serialization_deadline
552
+ Core::Utils::Time.get_time + settings.dynamic_instrumentation.max_time_to_serialize_ms / 1000.0
553
+ end
554
+
483
555
  private
484
556
 
557
+ def deadline_exceeded?(deadline)
558
+ Core::Utils::Time.get_time >= deadline
559
+ end
560
+
485
561
  MAX_MESSAGE_COLLECTION_SIZE = 3
486
562
  MAX_MESSAGE_ATTRIBUTE_COUNT = 5
487
563
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # A snapshot payload queued for sending to the agent, paired with
6
+ # the guardrails event type of the probe that produced it, so drop
7
+ # metrics can be tagged with the probe's classification.
8
+ # @api private
9
+ class SnapshotEvent
10
+ # Initializes the event.
11
+ #
12
+ # @param payload [Hash] the snapshot payload to send to the agent
13
+ # @param event_type [String] one of GuardrailsTelemetry::EVENT_TYPE_SNAPSHOT and EVENT_TYPE_LOG
14
+ # @return [void]
15
+ def initialize(payload:, event_type:)
16
+ @payload = payload
17
+ @event_type = event_type
18
+ end
19
+
20
+ # The snapshot payload to send to the agent.
21
+ # @return [Hash]
22
+ attr_reader :payload
23
+
24
+ # The guardrails event type of the probe that produced the payload.
25
+ # @return [String] one of GuardrailsTelemetry::EVENT_TYPE_SNAPSHOT and EVENT_TYPE_LOG
26
+ attr_reader :event_type
27
+
28
+ # Compares two events by payload and event type.
29
+ #
30
+ # @param other [Object] the object to compare with
31
+ # @return [Boolean] whether the other object is an event with the
32
+ # same payload and event type
33
+ def ==(other)
34
+ other.is_a?(SnapshotEvent) && other.payload == payload && other.event_type == event_type
35
+ end
36
+ end
37
+ end
38
+ end
@@ -47,11 +47,20 @@ module Datadog
47
47
  end.to_transport(DI::Transport::Diagnostics::Transport)
48
48
  end
49
49
 
50
- # Builds a new Transport::HTTP::Client with default settings
51
- # Pass a block to override any settings.
50
+ # Builds the snapshot input transport. Pass a block to override
51
+ # any transport settings.
52
+ #
53
+ # @param agent_settings [Datadog::Core::Configuration::AgentSettings] agent connection settings
54
+ # @param logger [DI::Logger] logger for transport diagnostics
55
+ # @param headers [Hash{String => String}, nil] headers to send with every request
56
+ # @param telemetry [Datadog::Core::Telemetry::Component, nil] component transport errors are reported through
57
+ # @param guardrails_telemetry [GuardrailsTelemetry] emitter for the canonical guardrails drop metric
58
+ # @return [DI::Transport::Input::Transport]
59
+ # @yield [builder] block overriding the default transport builder settings
52
60
  def self.input(
53
61
  agent_settings:,
54
62
  logger:,
63
+ guardrails_telemetry:,
55
64
  headers: nil,
56
65
  telemetry: nil
57
66
  )
@@ -72,7 +81,8 @@ module Datadog
72
81
  builder.to_api_instances,
73
82
  builder.default_api,
74
83
  logger: logger,
75
- telemetry: telemetry
84
+ telemetry: telemetry,
85
+ guardrails_telemetry: guardrails_telemetry,
76
86
  )
77
87
  end
78
88
  end
@@ -8,6 +8,7 @@ require_relative "../../core/transport/request"
8
8
  require_relative "../../core/transport/transport"
9
9
  require_relative "../error"
10
10
  require_relative "../fatal_exceptions"
11
+ require_relative "../guardrails_telemetry"
11
12
  require_relative "http/input"
12
13
 
13
14
  module Datadog
@@ -26,10 +27,22 @@ module Datadog
26
27
 
27
28
  class Transport < Core::Transport::Transport
28
29
  attr_reader :telemetry
30
+ # The guardrails metric emitter.
31
+ # @return [GuardrailsTelemetry]
32
+ attr_reader :guardrails_telemetry
29
33
 
30
- def initialize(apis, default_api, logger:, telemetry: nil)
34
+ # Initializes the transport.
35
+ #
36
+ # @param apis [Datadog::Core::Transport::HTTP::API::Map] API instances by name
37
+ # @param default_api [String] name of the API requests are sent through when unspecified
38
+ # @param logger [Datadog::Core::Logger] logger for transport diagnostics
39
+ # @param guardrails_telemetry [GuardrailsTelemetry] emitter for the canonical payload-drop metric
40
+ # @param telemetry [Datadog::Core::Telemetry::Component, nil] component transport errors are reported through
41
+ # @return [void]
42
+ def initialize(apis, default_api, logger:, guardrails_telemetry:, telemetry: nil)
31
43
  super(apis, default_api, logger: logger)
32
44
  @telemetry = telemetry
45
+ @guardrails_telemetry = guardrails_telemetry
33
46
  end
34
47
 
35
48
  # The limit on an individual snapshot payload, aka "log line",
@@ -62,19 +75,24 @@ module Datadog
62
75
  # Large snapshots (> 1MB) are dropped. Batches are split into chunks
63
76
  # of ~2MB each to avoid large network requests.
64
77
  #
65
- # @param payload [Array<Hash>] Array of snapshot payloads
78
+ # @param payload [Array<SnapshotEvent>] the snapshot events to send, each paired with its probe's guardrails event type
66
79
  # @param tags [Hash] Tags to send with the snapshots
67
80
  # @param on_serialization_error [Proc] Called with (probe_id, exception)
68
81
  # when a snapshot fails to serialize.
82
+ # @return [Array<SnapshotEvent>] the events that were sent
69
83
  def send_input(payload, tags, on_serialization_error:)
70
84
  serialized_tags = Core::TagBuilder.serialize_tags(tags)
71
85
 
72
86
  # Serialize each snapshot individually to isolate failures
73
87
  encoded_snapshots = []
74
- payload.each do |snapshot|
75
- encoded = encoder.encode(snapshot)
76
- if encoded.length > MAX_SERIALIZED_SNAPSHOT_SIZE
77
- logger.debug { "di: dropping too big snapshot" }
88
+ dropped_events = []
89
+ payload.each do |event|
90
+ encoded = encoder.encode(event.payload)
91
+ if encoded.bytesize > MAX_SERIALIZED_SNAPSHOT_SIZE
92
+ logger.debug do
93
+ "di: dropping too big snapshot (#{GuardrailsTelemetry::Reason::PAYLOAD_TOO_LARGE})"
94
+ end
95
+ dropped_events << [event, encoded.bytesize]
78
96
  next
79
97
  end
80
98
  encoded_snapshots << encoded
@@ -82,7 +100,7 @@ module Datadog
82
100
  Datadog::DI.reraise_if_fatal(exc)
83
101
  # Serialization failed for this snapshot - report via callback
84
102
  # This catches JSON::GeneratorError, Encoding errors, TypeError, etc.
85
- probe_id = snapshot.dig(:debugger, :snapshot, :probe, :id)
103
+ probe_id = event.payload.dig(:debugger, :snapshot, :probe, :id)
86
104
  logger.debug { "di: JSON encoding failed for snapshot (probe #{probe_id}): #{exc.class}: #{exc.message}" }
87
105
  telemetry&.report(exc, description: "JSON encoding failed for snapshot")
88
106
 
@@ -97,6 +115,13 @@ module Datadog
97
115
  end
98
116
  end
99
117
 
118
+ dropped_events.each do |event, bytes|
119
+ guardrails_telemetry.dropped(
120
+ reason: GuardrailsTelemetry::Reason::PAYLOAD_TOO_LARGE,
121
+ event_type: event.event_type, bytes: bytes,
122
+ )
123
+ end
124
+
100
125
  return payload if encoded_snapshots.empty?
101
126
 
102
127
  Datadog::Core::Chunker.chunk_by_size(