datadog 2.41.0 → 2.42.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  8. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  9. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  10. data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
  11. data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
  12. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  20. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  21. data/ext/libdatadog_api/di.c +10 -0
  22. data/ext/libdatadog_api/extconf.rb +3 -0
  23. data/ext/libdatadog_api/init.c +2 -0
  24. data/ext/libdatadog_api/otel_thread_context.c +232 -0
  25. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  26. data/ext/libdatadog_extconf_helpers.rb +1 -1
  27. data/lib/datadog/appsec/assets/blocked.html +1 -108
  28. data/lib/datadog/core/configuration/components.rb +1 -0
  29. data/lib/datadog/core/crashtracking/component.rb +5 -1
  30. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  31. data/lib/datadog/data_streams/processor.rb +31 -0
  32. data/lib/datadog/di/instrumenter.rb +41 -1
  33. data/lib/datadog/di/logger.rb +2 -2
  34. data/lib/datadog/di/probe.rb +9 -1
  35. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  36. data/lib/datadog/di/remote.rb +3 -3
  37. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  38. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  39. data/lib/datadog/open_feature/ext.rb +19 -0
  40. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  41. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  42. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  43. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  44. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  45. data/lib/datadog/open_feature/provider.rb +11 -2
  46. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  47. data/lib/datadog/opentelemetry/trace.rb +3 -0
  48. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  49. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  50. data/lib/datadog/profiling/component.rb +8 -16
  51. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  52. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  53. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  54. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  55. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  56. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  57. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  58. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  59. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  60. data/lib/datadog/tracing/otel_thread_context.rb +30 -0
  61. data/lib/datadog/tracing/remote.rb +195 -27
  62. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  63. data/lib/datadog/tracing/trace_digest.rb +22 -4
  64. data/lib/datadog/tracing/trace_operation.rb +22 -10
  65. data/lib/datadog/tracing/tracer.rb +5 -5
  66. data/lib/datadog/version.rb +1 -1
  67. metadata +14 -8
  68. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+
3
5
  require_relative "aggregator"
6
+ require_relative "../ext"
4
7
  require_relative "../../core/encoding"
5
8
  require_relative "../../core/evp"
6
9
  require_relative "../../core/utils/time"
@@ -33,11 +36,23 @@ module Datadog
33
36
  ROWS_DROPPED_METRIC = "flagevaluation.rows.dropped"
34
37
  ROWS_DEGRADED_METRIC = "flagevaluation.rows.degraded"
35
38
  PAYLOAD_SPLITS_METRIC = "flagevaluation.payload.splits"
39
+ CONTEXT_TRUNCATED_METRIC = "flagevaluation.context.truncated"
36
40
 
37
41
  REASON_QUEUE_OVERFLOW = "queue_overflow"
38
42
  REASON_DEGRADED_CAP = "degraded_cap"
39
43
  REASON_CARDINALITY_CAP = "cardinality_cap"
40
44
  REASON_PAYLOAD_LIMIT = "payload_limit"
45
+ REASON_PRE_QUEUE_OVERFLOW = "pre_queue_overflow"
46
+ REASON_SERIALIZATION_ERROR = "serialization_error"
47
+ REASON_SNAPSHOT_ERROR = "snapshot_error"
48
+
49
+ # Must equal OpenFeature::SDK::EvaluationContext::TARGETING_KEY. Duplicated as a
50
+ # literal rather than referenced because the SDK is an optional dependency and
51
+ # this file loads without it. If the two drift, the targeting key stops being
52
+ # excluded from the context snapshot and lands in context.evaluation as raw PII,
53
+ # so a spec asserts the equality.
54
+ TARGETING_KEY_FIELD = "targeting_key"
55
+ TARGETING_KEY_HASH_PREFIX = "sha256_"
41
56
 
42
57
  # Service context fields for the batch wrapper.
43
58
  attr_reader :service_context
@@ -56,6 +71,9 @@ module Datadog
56
71
  @stop_cond = ConditionVariable.new
57
72
  @stopped = false
58
73
  @dropped_queue_overflow = 0
74
+ @dropped_pre_queue_overflow = 0
75
+ @context_truncated_counts = Hash.new(0)
76
+ @context_snapshot_error_logged = false
59
77
 
60
78
  self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_RESTART
61
79
 
@@ -64,27 +82,37 @@ module Datadog
64
82
  end
65
83
 
66
84
  # Non-blocking enqueue from the finally hook. Drops + counts on overflow.
67
- # Context flattening/pruning runs in the background writer, not on the caller eval thread.
68
- def enqueue(**event)
85
+ def enqueue(
86
+ flag_key:, eval_time_ms:, variant: nil, allocation_key: nil, error_message: nil,
87
+ runtime_default: nil, targeting_key: nil, attrs: nil, observe_full_evaluation_data: false,
88
+ **_event
89
+ )
69
90
  start_background_thread if forked?
70
91
 
71
- attrs = event[:attrs]
72
- attrs = attrs.is_a?(Hash) ? snapshot_context_value(attrs, {}, 0) || {} : {}
92
+ # Avoid snapshot work when the queue is already full.
93
+ if @queue.size >= QUEUE_SIZE
94
+ @stop_mutex.synchronize { @dropped_pre_queue_overflow += 1 }
95
+ return
96
+ end
97
+
98
+ observe_full_evaluation_data = observe_full_evaluation_data == true
99
+ attrs = observe_full_evaluation_data ? snapshot_context(attrs) : nil
73
100
  bounded_event = {
74
- flag_key: event[:flag_key],
75
- variant: event[:variant],
76
- allocation_key: event[:allocation_key],
77
- error_message: event[:error_message],
78
- runtime_default: event[:runtime_default],
79
- targeting_key: event[:targeting_key],
80
- eval_time_ms: event[:eval_time_ms],
101
+ flag_key: snapshot_string(flag_key),
102
+ variant: snapshot_string(variant),
103
+ allocation_key: snapshot_string(allocation_key),
104
+ error_message: normalized_error_code(error_message),
105
+ runtime_default: runtime_default,
106
+ targeting_key: snapshot_targeting_key(targeting_key),
107
+ eval_time_ms: snapshot_integer(eval_time_ms),
81
108
  attrs: attrs,
109
+ observe_full_evaluation_data: observe_full_evaluation_data,
82
110
  }
83
111
  @queue.push(bounded_event, true)
112
+ @stop_mutex.synchronize { @stop_cond.signal }
84
113
  start_background_thread unless running?
85
114
  rescue ThreadError
86
- # Queue full drop and count (best-effort, same as Go: drop-and-count). The count is
87
- # emitted on the next flush so backpressure is observable, not silently lost.
115
+ # Report queue backpressure on the next flush.
88
116
  @stop_mutex.synchronize { @dropped_queue_overflow += 1 }
89
117
  end
90
118
 
@@ -111,45 +139,81 @@ module Datadog
111
139
  @stop_cond = ConditionVariable.new
112
140
  @stopped = false
113
141
  @dropped_queue_overflow = 0
142
+ @dropped_pre_queue_overflow = 0
143
+ @context_truncated_counts = Hash.new(0)
144
+ @context_snapshot_error_logged = false
114
145
  end
115
146
 
116
147
  private
117
148
 
149
+ def snapshot_context(attrs)
150
+ return {} unless attrs.is_a?(Hash)
151
+
152
+ snapshot, reasons = Aggregator.bounded_context_snapshot(attrs, excluded_key: TARGETING_KEY_FIELD)
153
+ unless reasons.empty?
154
+ @stop_mutex.synchronize { reasons.each { |reason| @context_truncated_counts[reason] += 1 } }
155
+ end
156
+ snapshot
157
+ rescue => e
158
+ # Context handling runs in the finally hook and must not interrupt flag evaluation.
159
+ should_log = false
160
+ @stop_mutex.synchronize do
161
+ @context_truncated_counts[REASON_SNAPSHOT_ERROR] += 1
162
+ unless @context_snapshot_error_logged
163
+ @context_snapshot_error_logged = true
164
+ should_log = true
165
+ end
166
+ end
167
+ @logger.debug { "OpenFeature EVP: context snapshot error: #{e.class}" } if should_log
168
+ {}
169
+ end
170
+
118
171
  def build_service_context
119
172
  config = Datadog.configuration
120
- ctx = {"service" => config.service.to_s}
121
- ctx["env"] = config.env if config.env && !config.env.empty?
122
- ctx["version"] = config.version if config.version && !config.version.empty?
173
+ ctx = {"service" => snapshot_string(config.service) || ""}
174
+ env = snapshot_string(config.env)
175
+ version = snapshot_string(config.version)
176
+ ctx["env"] = env if env && !env.empty?
177
+ ctx["version"] = version if version && !version.empty?
123
178
  ctx
124
179
  end
125
180
 
126
- def snapshot_context_value(value, seen, depth)
127
- return if depth > Aggregator::MAX_CONTEXT_DEPTH
181
+ def snapshot_string(value)
182
+ return unless value
128
183
 
129
- case value
130
- when Hash
131
- object_id = value.object_id
132
- return if seen[object_id]
184
+ string = value.is_a?(String) ? value : value.to_s
185
+ String.new(string).encode(Encoding::UTF_8, invalid: :replace, undef: :replace).freeze
186
+ rescue
187
+ # Unsupported caller values must not break flag evaluation.
188
+ nil
189
+ end
133
190
 
134
- seen[object_id] = true
135
- value.each_with_object({}) do |(key, child_value), snapshot|
136
- snapshot[key.is_a?(String) ? key.dup : key] = snapshot_context_value(child_value, seen, depth + 1)
137
- end.tap { seen.delete(object_id) }
138
- when Array
139
- object_id = value.object_id
140
- return if seen[object_id]
191
+ def snapshot_targeting_key(value)
192
+ return unless value.is_a?(String)
141
193
 
142
- seen[object_id] = true
143
- value.map { |child_value| snapshot_context_value(child_value, seen, depth + 1) }.tap { seen.delete(object_id) }
144
- when String
145
- value.dup
146
- else
147
- begin
148
- value.dup
149
- rescue TypeError
150
- value
151
- end
152
- end
194
+ string = String.new(value)
195
+ return unless string.valid_encoding?
196
+
197
+ string.encode!(Encoding::UTF_8)
198
+ string.freeze
199
+ rescue EncodingError
200
+ nil
201
+ end
202
+
203
+ def snapshot_integer(value)
204
+ integer = value.to_i
205
+ integer.is_a?(Integer) ? integer : 0
206
+ rescue
207
+ 0
208
+ end
209
+
210
+ def normalized_error_code(value)
211
+ error_code = snapshot_string(value)
212
+ # Preserve absence so evaluations without errors omit the error field.
213
+ return error_code if !error_code || error_code.empty?
214
+ return error_code if Ext::STANDARD_ERROR_CODES.include?(error_code)
215
+
216
+ Ext::GENERAL
153
217
  end
154
218
 
155
219
  def start_background_thread
@@ -189,7 +253,7 @@ module Datadog
189
253
 
190
254
  def wait_for_next_cycle
191
255
  @stop_mutex.synchronize do
192
- return if @stopped
256
+ return if @stopped || !@queue.empty?
193
257
 
194
258
  @stop_cond.wait(@stop_mutex, DRAIN_INTERVAL_SECONDS)
195
259
  end
@@ -215,9 +279,10 @@ module Datadog
215
279
  allocation_key: event[:allocation_key],
216
280
  targeting_key: event[:targeting_key],
217
281
  eval_time_ms: event[:eval_time_ms].to_i,
218
- attrs: event[:attrs].is_a?(Hash) ? event[:attrs] : {},
282
+ attrs: event[:attrs],
219
283
  error_message: event[:error_message],
220
284
  runtime_default: event[:runtime_default],
285
+ observe_full_evaluation_data: event[:observe_full_evaluation_data],
221
286
  )
222
287
  drained += 1
223
288
  rescue ThreadError
@@ -230,12 +295,17 @@ module Datadog
230
295
  def flush_once
231
296
  snapshot = @aggregator.flush_and_reset
232
297
  dropped_overflow = snapshot[:dropped_degraded_overflow].to_i
233
- dropped_queue = take_dropped_queue_overflow
298
+ dropped_queue = read_and_reset_dropped_queue_overflow
299
+ dropped_pre_queue = read_and_reset_dropped_pre_queue_overflow
300
+ context_truncated_counts = read_and_reset_context_truncated_counts
234
301
 
235
- emit_drop_counts(dropped_queue, dropped_overflow)
302
+ emit_drop_counts(dropped_queue, dropped_overflow, dropped_pre_queue)
303
+ context_truncated_counts.each do |reason, count|
304
+ record_telemetry_count(CONTEXT_TRUNCATED_METRIC, count, reason: reason)
305
+ end
236
306
 
237
307
  events = build_events(snapshot)
238
- emit_degraded_counts(snapshot[:degraded].values.sum { |entry| entry[:count].to_i })
308
+ record_telemetry_count(ROWS_DEGRADED_METRIC, snapshot[:degraded].values.sum { |entry| entry[:count].to_i }, reason: REASON_CARDINALITY_CAP)
239
309
  return if events.empty?
240
310
 
241
311
  send_payload_batches(events)
@@ -243,8 +313,7 @@ module Datadog
243
313
  @logger.debug { "OpenFeature EVP: flush error: #{e.class}: #{e.message}" }
244
314
  end
245
315
 
246
- # Read-and-reset the queue-overflow drop counter under the stop mutex.
247
- def take_dropped_queue_overflow
316
+ def read_and_reset_dropped_queue_overflow
248
317
  @stop_mutex.synchronize do
249
318
  count = @dropped_queue_overflow
250
319
  @dropped_queue_overflow = 0
@@ -252,32 +321,45 @@ module Datadog
252
321
  end
253
322
  end
254
323
 
324
+ def read_and_reset_dropped_pre_queue_overflow
325
+ @stop_mutex.synchronize do
326
+ count = @dropped_pre_queue_overflow
327
+ @dropped_pre_queue_overflow = 0
328
+ count
329
+ end
330
+ end
331
+
332
+ def read_and_reset_context_truncated_counts
333
+ @stop_mutex.synchronize do
334
+ counts = @context_truncated_counts
335
+ @context_truncated_counts = Hash.new(0)
336
+ counts
337
+ end
338
+ end
339
+
255
340
  # Emit (log) the observable drop counts so backpressure is never silently lost.
256
341
  # Σ(emitted tier counts + these drops) == evaluations processed.
257
- def emit_drop_counts(dropped_queue, dropped_overflow)
258
- return if dropped_queue.zero? && dropped_overflow.zero?
342
+ def emit_drop_counts(dropped_queue, dropped_overflow, dropped_pre_queue)
343
+ return if dropped_queue.zero? && dropped_overflow.zero? && dropped_pre_queue.zero?
259
344
 
345
+ record_telemetry_count(ROWS_DROPPED_METRIC, dropped_pre_queue, reason: REASON_PRE_QUEUE_OVERFLOW)
260
346
  record_telemetry_count(ROWS_DROPPED_METRIC, dropped_queue, reason: REASON_QUEUE_OVERFLOW)
261
347
  record_telemetry_count(ROWS_DROPPED_METRIC, dropped_overflow, reason: REASON_DEGRADED_CAP)
262
348
 
263
349
  @logger.debug do
264
350
  "OpenFeature EVP: dropped events " \
351
+ "pre_queue_overflow=#{dropped_pre_queue} " \
265
352
  "queue_overflow=#{dropped_queue} degraded_overflow=#{dropped_overflow}"
266
353
  end
267
354
  end
268
355
 
269
- def emit_degraded_counts(degraded_count)
270
- record_telemetry_count(ROWS_DEGRADED_METRIC, degraded_count, reason: REASON_CARDINALITY_CAP)
271
- end
272
-
273
356
  # Build flagEvaluationEvent list from aggregation snapshot.
274
- # Full-tier entries include all optional fields; degraded entries omit targeting_key + context.
275
357
  def build_events(snapshot)
276
358
  flush_time_ms = (Core::Utils::Time.now.to_f * 1000).to_i
277
359
  events = []
278
360
 
279
361
  snapshot[:full].each do |key, entry|
280
- flag_key, variant, allocation_key, _runtime_default, _error_message, targeting_key, _ctx_key = key
362
+ flag_key, variant, allocation_key, _runtime_default, _error_message, targeting_key, _ctx_key, _observe_full_evaluation_data = key
281
363
  event = build_event(
282
364
  flag_key: flag_key, variant: variant, allocation_key: allocation_key,
283
365
  targeting_key: targeting_key, entry: entry, flush_time_ms: flush_time_ms, tier: :full,
@@ -285,8 +367,9 @@ module Datadog
285
367
  events << event
286
368
  end
287
369
 
370
+ # Degraded rows omit targeting_key and context, so the policy is not a key dimension.
288
371
  snapshot[:degraded].each do |key, entry|
289
- flag_key, variant, allocation_key, _runtime_default, _error_message = key
372
+ flag_key, variant, allocation_key, _runtime_default, _error_dimension = key
290
373
  event = build_event(
291
374
  flag_key: flag_key, variant: variant, allocation_key: allocation_key,
292
375
  targeting_key: nil, entry: entry, flush_time_ms: flush_time_ms, tier: :degraded,
@@ -297,8 +380,11 @@ module Datadog
297
380
  events
298
381
  end
299
382
 
300
- def build_event(flag_key:, variant:, allocation_key:, targeting_key:, entry:, flush_time_ms:, tier:)
301
- # @type var event: ::Hash[::String, untyped]
383
+ def build_event(
384
+ flag_key:, variant:, allocation_key:, targeting_key:, entry:, flush_time_ms:, tier:
385
+ )
386
+ observe_full_evaluation_data = entry[:observe_full_evaluation_data]
387
+ # @type var event: ::Hash[::String, any]
302
388
  event = {
303
389
  "timestamp" => flush_time_ms,
304
390
  "flag" => {"key" => flag_key},
@@ -308,18 +394,30 @@ module Datadog
308
394
  }
309
395
 
310
396
  event["runtime_default_used"] = true if entry[:runtime_default]
311
- event["error"] = {"message" => entry[:error_message]} if entry[:error_message] && !entry[:error_message].empty?
397
+
398
+ # EVP uses the normalized code as error.message and an aggregation dimension.
399
+ error_message = entry[:error_message]
400
+ if error_message && !error_message.empty?
401
+ event["error"] = {"message" => error_message}
402
+ end
312
403
 
313
404
  # variant + allocation are present in both tiers (omitempty per schema).
314
405
  event["variant"] = {"key" => variant} if variant && !variant.empty?
315
406
  event["allocation"] = {"key" => allocation_key} if allocation_key && !allocation_key.empty?
316
407
 
317
- # Full-tier additionally carries targeting_key + the pruned evaluation context;
408
+ # Full-tier additionally carries targeting_key and the truncated evaluation context;
318
409
  # the degraded tier omits both.
319
410
  if tier == :full
320
- event["targeting_key"] = targeting_key if targeting_key && !targeting_key.empty?
411
+ unless targeting_key.nil?
412
+ event["targeting_key"] =
413
+ if targeting_key.empty? || observe_full_evaluation_data
414
+ targeting_key
415
+ else
416
+ prefixed_targeting_key_digest(targeting_key)
417
+ end
418
+ end
321
419
 
322
- if entry[:context_attrs] && !entry[:context_attrs].empty?
420
+ if observe_full_evaluation_data && entry[:context_attrs] && !entry[:context_attrs].empty?
323
421
  event["context"] = {"evaluation" => entry[:context_attrs]}
324
422
  end
325
423
  end
@@ -327,6 +425,14 @@ module Datadog
327
425
  event
328
426
  end
329
427
 
428
+ # Encode to UTF-8 first so Ruby encodings do not change the digest.
429
+ # Distinct from SpanEnrichmentHook::Codec.hash_targeting_key, which emits a
430
+ # bare digest for a different wire format on the span track.
431
+ def prefixed_targeting_key_digest(targeting_key)
432
+ utf8 = targeting_key.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
433
+ TARGETING_KEY_HASH_PREFIX + Digest::SHA256.hexdigest(utf8)
434
+ end
435
+
330
436
  def send_payload_batches(events)
331
437
  context_json = Core::Encoding::JSONEncoder.encode(@service_context)
332
438
  payload_prefix = "{\"context\":#{context_json},\"flagEvaluations\":["
@@ -336,11 +442,19 @@ module Datadog
336
442
  batch = []
337
443
  batch_size = base_payload_size
338
444
  dropped_oversized = 0
445
+ dropped_serialization = 0
339
446
  payload_limit_degraded = 0
340
447
  payload_splits = 0
341
448
 
342
449
  events.each do |event|
343
- encoded_event = encoded_event_for_payload(event, base_payload_size)
450
+ begin
451
+ encoded_event = encoded_event_for_payload(event, base_payload_size)
452
+ rescue => e
453
+ # Keep one malformed event from discarding the complete flush snapshot.
454
+ dropped_serialization += event_count(event)
455
+ @logger.debug { "OpenFeature EVP: dropped event serialization_error=#{e.class}" }
456
+ next
457
+ end
344
458
  unless encoded_event
345
459
  dropped_oversized += event_count(event)
346
460
  next
@@ -365,8 +479,9 @@ module Datadog
365
479
  send_payload_batch(batch) unless batch.empty?
366
480
  record_telemetry_count(ROWS_DEGRADED_METRIC, payload_limit_degraded, reason: REASON_PAYLOAD_LIMIT)
367
481
  record_telemetry_count(ROWS_DROPPED_METRIC, dropped_oversized, reason: REASON_PAYLOAD_LIMIT)
482
+ record_telemetry_count(ROWS_DROPPED_METRIC, dropped_serialization, reason: REASON_SERIALIZATION_ERROR)
368
483
  record_telemetry_count(PAYLOAD_SPLITS_METRIC, payload_splits)
369
- emit_payload_oversize_drops(dropped_oversized) if dropped_oversized.positive?
484
+ @logger.debug { "OpenFeature EVP: dropped events payload_oversize=#{dropped_oversized}" } if dropped_oversized.positive?
370
485
  end
371
486
 
372
487
  def send_payload_batch(events)
@@ -410,10 +525,6 @@ module Datadog
410
525
  degraded
411
526
  end
412
527
 
413
- def emit_payload_oversize_drops(dropped_oversized)
414
- @logger.debug { "OpenFeature EVP: dropped events payload_oversize=#{dropped_oversized}" }
415
- end
416
-
417
528
  def event_count(event)
418
529
  count = event["evaluation_count"].to_i
419
530
  count.positive? ? count : 1
@@ -15,8 +15,6 @@ module Datadog
15
15
  # stay on the OTel path. This hook is registered as a provider hook so it receives
16
16
  # the SDK-final EvaluationDetails after hook failures and type validation.
17
17
  class FlagEvalEVPHook
18
- TYPE_MISMATCH_ERROR_CODE = "TYPE_MISMATCH"
19
-
20
18
  # Include the Hook module if available (SDK >= 0.5.0) for interface documentation
21
19
  # and default implementations of other hook methods (before, after, error)
22
20
  include ::OpenFeature::SDK::Hooks::Hook if defined?(::OpenFeature::SDK::Hooks::Hook)
@@ -42,15 +40,31 @@ module Datadog
42
40
  eval_time_ms = metadata.is_a?(Hash) ? metadata["dd.eval.timestamp_ms"] : nil
43
41
  eval_time_ms ||= (Core::Utils::Time.now.to_f * 1000).to_i
44
42
 
43
+ # observe_full_evaluation_data travels on the event metadata (stamped from
44
+ # the UFC the evaluation ran against), never read from live config.
45
+ observe_full_evaluation_data = metadata.is_a?(Hash) ? metadata[Ext::METADATA_OBSERVE_FULL_EVALUATION_DATA] : nil
46
+ observe_full_evaluation_data = false unless observe_full_evaluation_data == true
47
+
48
+ attrs = if observe_full_evaluation_data
49
+ extract_attributes(hook_context.evaluation_context)
50
+ end
51
+
52
+ # Raw messages may contain high-cardinality data, so only error codes enter EVP.
53
+ reason = evaluation_details.reason.to_s
54
+ runtime_default = runtime_default_reason?(reason)
55
+ error_code = evaluation_details.error_code
56
+ error_code = Ext::GENERAL if reason == Ext::ERROR && error_code.to_s.empty?
57
+
45
58
  writer.enqueue(
46
59
  flag_key: hook_context.flag_key,
47
- variant: evaluation_details.variant,
48
- allocation_key: extract_allocation_key(evaluation_details),
49
- error_message: extract_error_message(evaluation_details),
50
- runtime_default: runtime_default?(evaluation_details),
60
+ variant: runtime_default ? nil : evaluation_details.variant,
61
+ allocation_key: runtime_default ? nil : extract_allocation_key(evaluation_details),
62
+ error_message: error_code,
63
+ runtime_default: runtime_default,
51
64
  targeting_key: extract_targeting_key(hook_context.evaluation_context),
52
65
  eval_time_ms: eval_time_ms,
53
- attrs: extract_attributes(hook_context.evaluation_context),
66
+ attrs: attrs,
67
+ observe_full_evaluation_data: observe_full_evaluation_data,
54
68
  )
55
69
  end
56
70
 
@@ -72,9 +86,7 @@ module Datadog
72
86
  return {} unless evaluation_context.respond_to?(:fields)
73
87
 
74
88
  fields = evaluation_context.fields
75
- return {} unless fields.is_a?(Hash)
76
-
77
- fields.reject { |k, _| k.to_s == ::OpenFeature::SDK::EvaluationContext::TARGETING_KEY }
89
+ fields.is_a?(Hash) ? fields : {}
78
90
  end
79
91
 
80
92
  def extract_allocation_key(evaluation_details)
@@ -84,17 +96,8 @@ module Datadog
84
96
  metadata[Ext::METADATA_ALLOCATION_KEY]
85
97
  end
86
98
 
87
- def extract_error_message(evaluation_details)
88
- return unless evaluation_details.respond_to?(:error_message)
89
-
90
- evaluation_details.error_message
91
- end
92
-
93
- def runtime_default?(evaluation_details)
94
- return true if evaluation_details.variant.nil?
95
- return false unless evaluation_details.respond_to?(:error_code)
96
-
97
- evaluation_details.error_code.to_s == TYPE_MISMATCH_ERROR_CODE
99
+ def runtime_default_reason?(reason)
100
+ reason == Ext::DEFAULT || reason == Ext::ERROR
98
101
  end
99
102
  end
100
103
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require_relative "../core/feature_flags"
4
5
  require_relative "ext"
5
6
  require_relative "resolution_details"
@@ -15,8 +16,11 @@ module Datadog
15
16
  # in the format expected by `libdatadog` without any modifications
16
17
  def initialize(configuration)
17
18
  @configuration = Core::FeatureFlags::Configuration.new(configuration)
19
+ @observe_full_evaluation_data = parse_observe_full_evaluation_data(configuration)
18
20
  end
19
21
 
22
+ attr_reader :observe_full_evaluation_data
23
+
20
24
  # Returns the assignment for a given flag key based on the feature flags
21
25
  # configuration
22
26
  #
@@ -27,21 +31,44 @@ module Datadog
27
31
  # @param context [Hash] The context of the evaluation, containing targeting key
28
32
  # and other attributes
29
33
  #
30
- # @return [Core::FeatureFlags::ResolutionDetails] The assignment for the flag
34
+ # @return [ResolutionDetails] The assignment for the flag
31
35
  def get_assignment(flag_key, default_value:, expected_type:, context:)
32
36
  result = @configuration.get_assignment(flag_key, expected_type, context)
33
37
 
34
38
  return invalid_flag_configuration_error(default_value) if invalid_flag_configuration?(result)
35
39
 
36
- # NOTE: This is a special case when we need to fallback to the default
37
- # value, even tho the evaluation itself doesn't produce an error
38
- # resolution details
39
- result.value = default_value if result.variant.nil?
40
- result
40
+ build_resolution_details(result, default_value)
41
41
  end
42
42
 
43
43
  private
44
44
 
45
+ # Parse observe_full_evaluation_data from the top level of the UFC JSON (a sibling
46
+ # of `environment`). Absent, null, or wrong-typed values return false.
47
+ def parse_observe_full_evaluation_data(configuration)
48
+ return false unless configuration.is_a?(String) && !configuration.empty?
49
+
50
+ parsed = JSON.parse(configuration)
51
+ parsed.is_a?(Hash) && parsed["observeFullEvaluationData"] == true
52
+ rescue
53
+ # This secondary policy parse must not reject configuration accepted by the native evaluator.
54
+ false
55
+ end
56
+
57
+ def build_resolution_details(result, default_value)
58
+ ResolutionDetails.new(
59
+ value: result.variant.nil? ? default_value : result.value,
60
+ reason: result.reason,
61
+ variant: result.variant,
62
+ error_code: result.error_code,
63
+ error_message: result.error_message,
64
+ flag_metadata: result.flag_metadata,
65
+ allocation_key: result.allocation_key,
66
+ serial_id: result.serial_id,
67
+ log?: result.log?,
68
+ error?: result.error?,
69
+ )
70
+ end
71
+
45
72
  def invalid_flag_configuration?(result)
46
73
  result.reason == Ext::DEFAULT &&
47
74
  result.error_code.nil? &&
@@ -7,6 +7,11 @@ module Datadog
7
7
  module OpenFeature
8
8
  # This class is a noop interface of evaluation logic
9
9
  class NoopEvaluator
10
+ # Privacy-preserving default before configuration is present.
11
+ def observe_full_evaluation_data
12
+ false
13
+ end
14
+
10
15
  def initialize(_configuration)
11
16
  # no-op
12
17
  end
@@ -112,6 +112,7 @@ module Datadog
112
112
  # Stamp evaluation entry time once, here on the eval thread. The EVP path uses this for
113
113
  # accurate first/last_evaluation bounds instead of a later hook-fire clock read.
114
114
  eval_time_ms = (Core::Utils::Time.now.to_f * 1000).to_i
115
+ flag_meta = nil
115
116
 
116
117
  engine = OpenFeature.engine
117
118
  return component_not_configured_default(default_value, eval_time_ms) if engine.nil?
@@ -138,7 +139,10 @@ module Datadog
138
139
  error_code: Ext::GENERAL,
139
140
  error_message: error_message
140
141
  )
141
- error_flag_meta = build_flag_metadata(error_result, eval_time_ms || (Core::Utils::Time.now.to_f * 1000).to_i)
142
+ error_flag_meta = flag_meta
143
+ error_flag_meta ||= build_flag_metadata(
144
+ error_result, eval_time_ms || (Core::Utils::Time.now.to_f * 1000).to_i
145
+ )
142
146
 
143
147
  sdk_error_details(default_value, Ext::GENERAL, error_message, Ext::ERROR, error_flag_meta)
144
148
  end
@@ -173,6 +177,8 @@ module Datadog
173
177
 
174
178
  def build_flag_metadata(result, eval_time_ms)
175
179
  metadata = result.flag_metadata&.dup || {}
180
+ metadata[Ext::METADATA_OBSERVE_FULL_EVALUATION_DATA] =
181
+ metadata[Ext::METADATA_OBSERVE_FULL_EVALUATION_DATA] == true
176
182
  allocation_key = result.allocation_key
177
183
  metadata[Ext::METADATA_ALLOCATION_KEY] = allocation_key if allocation_key && !allocation_key.empty?
178
184
 
@@ -218,7 +224,10 @@ module Datadog
218
224
  error_code: Ext::PROVIDER_FATAL,
219
225
  error_message: "Datadog's OpenFeature component must be configured",
220
226
  reason: Ext::ERROR,
221
- flag_metadata: {"dd.eval.timestamp_ms" => eval_time_ms}
227
+ flag_metadata: {
228
+ "dd.eval.timestamp_ms" => eval_time_ms,
229
+ Ext::METADATA_OBSERVE_FULL_EVALUATION_DATA => false,
230
+ }
222
231
  )
223
232
  end
224
233
  end
@@ -60,7 +60,7 @@ module Datadog
60
60
 
61
61
  if digest.trace_state || digest.trace_flags
62
62
  trace_flags = ::OpenTelemetry::Trace::TraceFlags.from_byte(digest.trace_flags)
63
- tracestate = Tracing::Distributed::TraceContext.new(fetcher: nil).send(:build_tracestate, digest)
63
+ tracestate = Tracing::Distributed::TraceState.serialize_digest(digest)
64
64
  else
65
65
  trace_flags = if Tracing::Sampling::PrioritySampler.sampled?(digest.trace_sampling_priority)
66
66
  ::OpenTelemetry::Trace::TraceFlags::SAMPLED
@@ -50,6 +50,9 @@ module Datadog
50
50
  trace_service: digest.trace_service,
51
51
  trace_state: digest.trace_state,
52
52
  trace_state_unknown_fields: digest.trace_state_unknown_fields,
53
+ trace_otel_random_value: digest.trace_otel_random_value,
54
+ trace_otel_threshold: digest.trace_otel_threshold,
55
+ trace_otel_unknown_fields: digest.trace_otel_unknown_fields,
53
56
  span_remote: digest.span_remote,
54
57
  ).freeze
55
58
  end
@@ -33,6 +33,7 @@ module Datadog
33
33
  # @rbs allocation_counting_enabled: bool
34
34
  # @rbs gvl_profiling_enabled: bool
35
35
  # @rbs sighandler_sampling_enabled: bool
36
+ # @rbs waiting_for_gvl_threshold_ns: ::Integer
36
37
  # @rbs skip_idle_samples_for_testing: false
37
38
  # @rbs return: void
38
39
  def initialize(
@@ -45,6 +46,7 @@ module Datadog
45
46
  gvl_profiling_enabled:,
46
47
  sighandler_sampling_enabled:,
47
48
  cpu_sampling_interval_ms:,
49
+ waiting_for_gvl_threshold_ns:,
48
50
  # **NOTE**: This should only be used for testing; disabling the dynamic sampling rate will increase the
49
51
  # profiler overhead!
50
52
  dynamic_sampling_rate_enabled: true,
@@ -78,6 +80,7 @@ module Datadog
78
80
  sighandler_sampling_enabled: sighandler_sampling_enabled,
79
81
  skip_idle_samples_for_testing: skip_idle_samples_for_testing,
80
82
  cpu_sampling_interval_ms: cpu_sampling_interval_ms,
83
+ waiting_for_gvl_threshold_ns: waiting_for_gvl_threshold_ns,
81
84
  )
82
85
  @worker_thread = nil
83
86
  @failure_exception = nil