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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -2
- data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
- data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
- data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
- data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
- data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
- data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
- data/ext/datadog_profiling_native_extension/profiling.c +2 -0
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
- data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
- data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
- data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
- data/ext/libdatadog_api/di.c +10 -0
- data/ext/libdatadog_api/extconf.rb +3 -0
- data/ext/libdatadog_api/init.c +2 -0
- data/ext/libdatadog_api/otel_thread_context.c +232 -0
- data/ext/libdatadog_api/otel_thread_context.h +5 -0
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/assets/blocked.html +1 -108
- data/lib/datadog/core/configuration/components.rb +1 -0
- data/lib/datadog/core/crashtracking/component.rb +5 -1
- data/lib/datadog/data_streams/pathway_context.rb +20 -22
- data/lib/datadog/data_streams/processor.rb +31 -0
- data/lib/datadog/di/instrumenter.rb +41 -1
- data/lib/datadog/di/logger.rb +2 -2
- data/lib/datadog/di/probe.rb +9 -1
- data/lib/datadog/di/probe_notification_builder.rb +1 -0
- data/lib/datadog/di/remote.rb +3 -3
- data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
- data/lib/datadog/open_feature/exposures/event.rb +10 -3
- data/lib/datadog/open_feature/ext.rb +19 -0
- data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
- data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
- data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
- data/lib/datadog/open_feature/native_evaluator.rb +33 -6
- data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
- data/lib/datadog/open_feature/provider.rb +11 -2
- data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
- data/lib/datadog/opentelemetry/trace.rb +3 -0
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
- data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
- data/lib/datadog/profiling/component.rb +8 -16
- data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
- data/lib/datadog/tracing/distributed/baggage.rb +0 -1
- data/lib/datadog/tracing/distributed/datadog.rb +3 -3
- data/lib/datadog/tracing/distributed/propagation.rb +3 -0
- data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
- data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
- data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
- data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
- data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
- data/lib/datadog/tracing/otel_thread_context.rb +30 -0
- data/lib/datadog/tracing/remote.rb +195 -27
- data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
- data/lib/datadog/tracing/trace_digest.rb +22 -4
- data/lib/datadog/tracing/trace_operation.rb +22 -10
- data/lib/datadog/tracing/tracer.rb +5 -5
- data/lib/datadog/version.rb +1 -1
- metadata +14 -8
- 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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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:
|
|
75
|
-
variant:
|
|
76
|
-
allocation_key:
|
|
77
|
-
error_message:
|
|
78
|
-
runtime_default:
|
|
79
|
-
targeting_key:
|
|
80
|
-
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
|
-
#
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
127
|
-
return
|
|
181
|
+
def snapshot_string(value)
|
|
182
|
+
return unless value
|
|
128
183
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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]
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
301
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
50
|
-
runtime_default: runtime_default
|
|
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:
|
|
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
|
-
|
|
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
|
|
88
|
-
|
|
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 [
|
|
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
|
-
|
|
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 =
|
|
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: {
|
|
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::
|
|
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
|