datadog 2.36.0 → 2.38.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 +74 -1
- data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +5 -12
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +29 -18
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +17 -3
- data/ext/datadog_profiling_native_extension/collectors_stack.c +19 -24
- data/ext/datadog_profiling_native_extension/collectors_stack.h +0 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +348 -213
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -0
- data/ext/datadog_profiling_native_extension/extconf.rb +11 -3
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -0
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +7 -0
- data/ext/datadog_profiling_native_extension/time_helpers.h +12 -7
- data/ext/libdatadog_api/di.c +76 -0
- data/ext/libdatadog_api/extconf.rb +6 -0
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/ai_guard/api_client.rb +6 -4
- data/lib/datadog/appsec/api_security/route_extractor.rb +7 -4
- data/lib/datadog/appsec/configuration.rb +6 -0
- data/lib/datadog/appsec/context.rb +4 -2
- data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
- data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
- data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +1 -1
- data/lib/datadog/appsec/contrib/rack/gateway/request.rb +5 -0
- data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -0
- data/lib/datadog/appsec/contrib/rack/input_peeker.rb +4 -17
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
- data/lib/datadog/appsec/contrib/rails/gateway/request.rb +7 -1
- data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +4 -0
- data/lib/datadog/appsec/ext.rb +1 -0
- data/lib/datadog/appsec/metrics/collector.rb +18 -1
- data/lib/datadog/appsec/metrics/exporter.rb +9 -3
- data/lib/datadog/appsec/remote.rb +9 -3
- data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
- data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
- data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
- data/lib/datadog/appsec/route_normalizer.rb +80 -0
- data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
- data/lib/datadog/core/configuration/agentless_settings_resolver.rb +2 -2
- data/lib/datadog/core/configuration/components.rb +82 -2
- data/lib/datadog/core/configuration/components_state.rb +6 -1
- data/lib/datadog/core/configuration/settings.rb +4 -7
- data/lib/datadog/core/configuration/supported_configurations.rb +3 -0
- data/lib/datadog/core/evp.rb +11 -0
- data/lib/datadog/core/knuth_sampler.rb +3 -1
- data/lib/datadog/core/metrics/client.rb +1 -1
- data/lib/datadog/core/remote/client/capabilities.rb +34 -7
- data/lib/datadog/core/telemetry/event/app_started.rb +8 -1
- data/lib/datadog/core/transport/http/builder.rb +1 -1
- data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
- data/lib/datadog/core/utils/time.rb +5 -1
- data/lib/datadog/di/base.rb +3 -1
- data/lib/datadog/di/boot.rb +3 -0
- data/lib/datadog/di/capture_expression.rb +21 -0
- data/lib/datadog/di/capture_expression_evaluator.rb +71 -0
- data/lib/datadog/di/capture_limits.rb +41 -0
- data/lib/datadog/di/code_tracker.rb +5 -2
- data/lib/datadog/di/component.rb +112 -38
- data/lib/datadog/di/configuration.rb +6 -0
- data/lib/datadog/di/context.rb +9 -3
- data/lib/datadog/di/el/compiler.rb +1 -1
- data/lib/datadog/di/error.rb +10 -0
- data/lib/datadog/di/fatal_exceptions.rb +26 -0
- data/lib/datadog/di/instrumenter.rb +412 -165
- data/lib/datadog/di/probe.rb +61 -2
- data/lib/datadog/di/probe_builder.rb +83 -1
- data/lib/datadog/di/probe_file_loader.rb +8 -3
- data/lib/datadog/di/probe_manager.rb +67 -9
- data/lib/datadog/di/probe_notification_builder.rb +52 -8
- data/lib/datadog/di/probe_notifier_worker.rb +52 -32
- data/lib/datadog/di/redactor.rb +16 -1
- data/lib/datadog/di/remote.rb +176 -15
- data/lib/datadog/di/serializer.rb +34 -15
- data/lib/datadog/di/transport/input.rb +8 -4
- data/lib/datadog/di.rb +63 -0
- data/lib/datadog/kit/appsec/events/v2.rb +60 -2
- data/lib/datadog/open_feature/component.rb +29 -6
- data/lib/datadog/open_feature/configuration.rb +8 -0
- data/lib/datadog/open_feature/ext.rb +2 -0
- data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
- data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
- data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
- data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
- data/lib/datadog/open_feature/native_evaluator.rb +22 -0
- data/lib/datadog/open_feature/provider.rb +59 -27
- data/lib/datadog/open_feature/transport.rb +55 -1
- data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
- data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
- data/lib/datadog/opentelemetry/sdk.rb +19 -0
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +6 -1
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
- data/lib/datadog/profiling/collectors/thread_context.rb +9 -1
- data/lib/datadog/profiling/component.rb +17 -0
- data/lib/datadog/symbol_database/component.rb +164 -47
- data/lib/datadog/symbol_database/configuration.rb +12 -4
- data/lib/datadog/symbol_database/extensions.rb +19 -0
- data/lib/datadog/symbol_database/extractor.rb +124 -87
- data/lib/datadog/symbol_database/file_hash.rb +3 -1
- data/lib/datadog/symbol_database/remote.rb +8 -3
- data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
- data/lib/datadog/symbol_database/uploader.rb +9 -2
- data/lib/datadog/symbol_database.rb +22 -0
- data/lib/datadog/tracing/contrib/active_job/data_streams.rb +62 -0
- data/lib/datadog/tracing/contrib/active_job/patcher.rb +12 -0
- data/lib/datadog/tracing/contrib/active_model_serializers/events/serialize.rb +2 -3
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -12
- data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
- data/lib/datadog/tracing/contrib/sequel/utils.rb +2 -2
- data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
- data/lib/datadog/tracing/contrib/sinatra/framework.rb +1 -1
- data/lib/datadog/tracing/distributed/baggage.rb +2 -1
- data/lib/datadog/tracing/ext.rb +9 -0
- data/lib/datadog/tracing/remote.rb +34 -3
- data/lib/datadog/tracing/transport/trace_formatter.rb +1 -1
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +1 -0
- metadata +25 -8
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module OpenFeature
|
|
5
|
+
module FlagEvaluation
|
|
6
|
+
# Two-tier aggregation for EVP flagevaluation events.
|
|
7
|
+
#
|
|
8
|
+
# Two-tier design:
|
|
9
|
+
# - full-tier key: (flag_key, variant, allocation_key, runtime_default, error_message, targeting_key, canonical_context_key)
|
|
10
|
+
# - degraded-tier key: (flag_key, variant, allocation_key, runtime_default, error_message)
|
|
11
|
+
# - Drop-and-count when degraded tier is full
|
|
12
|
+
# - canonical_context_key: sorted type-tagged length-delimited encoding (no hash digest)
|
|
13
|
+
# - Caps: global_cap=131_072 / per_flag_cap=10_000 / degraded_cap=32_768
|
|
14
|
+
# - Context pruning: 256 fields / 256 chars (matches flageval-worker backend limits)
|
|
15
|
+
class Aggregator
|
|
16
|
+
MAX_CONTEXT_FIELDS = 256
|
|
17
|
+
MAX_FIELD_LENGTH = 256
|
|
18
|
+
MAX_CONTEXT_DEPTH = 32
|
|
19
|
+
|
|
20
|
+
# Type tags so values of different Ruby types never collide in the canonical key.
|
|
21
|
+
CTX_TAG_STRING = 's'
|
|
22
|
+
CTX_TAG_BOOL = 'b'
|
|
23
|
+
CTX_TAG_INTEGER = 'i'
|
|
24
|
+
CTX_TAG_FLOAT = 'f'
|
|
25
|
+
CTX_TAG_OTHER = 'o'
|
|
26
|
+
|
|
27
|
+
EVAL_SCALE_TARGET_FLAGS = 2_500
|
|
28
|
+
EVAL_SCALE_FULL_BUCKETS_PER_FLAG = 50
|
|
29
|
+
EVAL_SCALE_USERS_PER_FLAG = 1_000
|
|
30
|
+
EVAL_SCALE_PER_FLAG_HEADROOM_MULTIPLIER = 10
|
|
31
|
+
EVAL_SCALE_DEGRADED_BUCKETS_PER_FLAG = 10
|
|
32
|
+
EVAL_SCALE_FULL_BUCKET_TARGET = EVAL_SCALE_TARGET_FLAGS * EVAL_SCALE_FULL_BUCKETS_PER_FLAG
|
|
33
|
+
EVAL_SCALE_PER_FLAG_BUCKET_TARGET = EVAL_SCALE_PER_FLAG_HEADROOM_MULTIPLIER * EVAL_SCALE_USERS_PER_FLAG
|
|
34
|
+
EVAL_SCALE_DEGRADED_BUCKET_TARGET = EVAL_SCALE_TARGET_FLAGS * EVAL_SCALE_DEGRADED_BUCKETS_PER_FLAG
|
|
35
|
+
|
|
36
|
+
DEFAULT_GLOBAL_CAP = 131_072
|
|
37
|
+
DEFAULT_PER_FLAG_CAP = EVAL_SCALE_PER_FLAG_BUCKET_TARGET
|
|
38
|
+
DEFAULT_DEGRADED_CAP = 32_768
|
|
39
|
+
|
|
40
|
+
attr_reader :dropped_degraded_overflow
|
|
41
|
+
|
|
42
|
+
def initialize(
|
|
43
|
+
global_cap: DEFAULT_GLOBAL_CAP,
|
|
44
|
+
per_flag_cap: DEFAULT_PER_FLAG_CAP,
|
|
45
|
+
degraded_cap: DEFAULT_DEGRADED_CAP
|
|
46
|
+
)
|
|
47
|
+
@global_cap = global_cap
|
|
48
|
+
@per_flag_cap = per_flag_cap
|
|
49
|
+
@degraded_cap = degraded_cap
|
|
50
|
+
|
|
51
|
+
@mutex = Mutex.new
|
|
52
|
+
# full-tier: Array key -> Hash entry
|
|
53
|
+
@full = {}
|
|
54
|
+
# degraded-tier: Array key -> Hash entry
|
|
55
|
+
@degraded = {}
|
|
56
|
+
# per-flag full-bucket count for per_flag_cap enforcement
|
|
57
|
+
@per_flag_full = Hash.new(0)
|
|
58
|
+
@global_count = 0
|
|
59
|
+
@dropped_degraded_overflow = 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Record one evaluation event. Thread-safe. Called from the background writer.
|
|
63
|
+
def record(
|
|
64
|
+
flag_key:, variant:, allocation_key:, targeting_key:, eval_time_ms:, attrs:, error_message: nil,
|
|
65
|
+
runtime_default: nil
|
|
66
|
+
)
|
|
67
|
+
runtime_default = variant.nil? if runtime_default.nil?
|
|
68
|
+
runtime_default = !!runtime_default
|
|
69
|
+
|
|
70
|
+
# Normalize nil/empty strings
|
|
71
|
+
variant = variant.to_s
|
|
72
|
+
allocation_key = allocation_key.to_s
|
|
73
|
+
error_message = error_message.to_s
|
|
74
|
+
targeting_key = targeting_key.to_s
|
|
75
|
+
|
|
76
|
+
# Context pruning + canonical key (see prune_context and canonical_context_key).
|
|
77
|
+
# Runs in the background writer so caller eval threads do not pay the flatten/prune cost.
|
|
78
|
+
pruned_context = prune_context(attrs)
|
|
79
|
+
context_key = canonical_context_key(pruned_context)
|
|
80
|
+
|
|
81
|
+
full_key = [flag_key, variant, allocation_key, runtime_default, error_message, targeting_key, context_key]
|
|
82
|
+
evaluation_time_ms = eval_time_ms.to_i
|
|
83
|
+
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
# --- Full tier ---
|
|
86
|
+
if (entry = @full[full_key])
|
|
87
|
+
observe(entry, evaluation_time_ms)
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
per_flag_count = @per_flag_full[flag_key]
|
|
92
|
+
if per_flag_count >= @per_flag_cap
|
|
93
|
+
add_to_degraded(flag_key, variant, allocation_key, runtime_default, error_message, evaluation_time_ms)
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Count the full-tier attempt before checking the global cap so per-flag overflow stays
|
|
98
|
+
# active even when the global full-tier cap is already saturated.
|
|
99
|
+
@per_flag_full[flag_key] = per_flag_count + 1
|
|
100
|
+
|
|
101
|
+
if @global_count < @global_cap
|
|
102
|
+
entry = new_entry(
|
|
103
|
+
evaluation_time_ms,
|
|
104
|
+
runtime_default: runtime_default,
|
|
105
|
+
error_message: error_message,
|
|
106
|
+
targeting_key: targeting_key,
|
|
107
|
+
context_attrs: pruned_context
|
|
108
|
+
)
|
|
109
|
+
@full[full_key] = entry
|
|
110
|
+
@global_count += 1
|
|
111
|
+
else
|
|
112
|
+
# Route to degraded tier
|
|
113
|
+
add_to_degraded(flag_key, variant, allocation_key, runtime_default, error_message, evaluation_time_ms)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Flush aggregation maps, reset state, return snapshot.
|
|
119
|
+
# Returns { full: Hash, degraded: Hash, dropped_degraded_overflow: Integer }.
|
|
120
|
+
# The overflow count is included in the snapshot so the caller can EMIT it before it is
|
|
121
|
+
# reset — the count is never reset-without-emit (backpressure stays observable).
|
|
122
|
+
def flush_and_reset
|
|
123
|
+
@mutex.synchronize do
|
|
124
|
+
full_snapshot = @full
|
|
125
|
+
degraded_snapshot = @degraded
|
|
126
|
+
dropped_snapshot = @dropped_degraded_overflow
|
|
127
|
+
|
|
128
|
+
@full = {}
|
|
129
|
+
@degraded = {}
|
|
130
|
+
@per_flag_full = Hash.new(0)
|
|
131
|
+
@global_count = 0
|
|
132
|
+
@dropped_degraded_overflow = 0
|
|
133
|
+
|
|
134
|
+
{full: full_snapshot, degraded: degraded_snapshot, dropped_degraded_overflow: dropped_snapshot}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Prune context: keep first MAX_CONTEXT_FIELDS fields (sorted), skip string values >256 chars.
|
|
139
|
+
# Keys are sorted before pruning to ensure deterministic subset selection.
|
|
140
|
+
def prune_context(attrs)
|
|
141
|
+
self.class.prune_context(attrs)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.prune_context(attrs)
|
|
145
|
+
flattened_context = flatten_context(attrs)
|
|
146
|
+
return {} if flattened_context.empty?
|
|
147
|
+
|
|
148
|
+
pruned_context = {}
|
|
149
|
+
count = 0
|
|
150
|
+
flattened_context.keys.sort.each do |key|
|
|
151
|
+
break if count >= MAX_CONTEXT_FIELDS
|
|
152
|
+
|
|
153
|
+
value = flattened_context[key]
|
|
154
|
+
# Skip oversized string values (mirrors flageval-worker pruning behavior).
|
|
155
|
+
next if value.is_a?(String) && value.length > MAX_FIELD_LENGTH
|
|
156
|
+
|
|
157
|
+
pruned_context[key] = value
|
|
158
|
+
count += 1
|
|
159
|
+
end
|
|
160
|
+
pruned_context
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.flatten_context(attrs)
|
|
164
|
+
return {} unless attrs.is_a?(Hash) && !attrs.empty?
|
|
165
|
+
|
|
166
|
+
flattened_context = {}
|
|
167
|
+
seen = {attrs.object_id => true}
|
|
168
|
+
attrs.each do |key, value|
|
|
169
|
+
flatten_value(key.to_s, value, flattened_context, seen, 0)
|
|
170
|
+
end
|
|
171
|
+
flattened_context
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Canonical context key: sorted type-tagged length-delimited encoding.
|
|
175
|
+
# Each field is: 8-byte big-endian key length + key bytes + type-tag byte +
|
|
176
|
+
# 8-byte big-endian value length + value bytes.
|
|
177
|
+
# No hash digest — the key IS the full encoding (collision-free, no FNV).
|
|
178
|
+
def canonical_context_key(attrs)
|
|
179
|
+
return '' if attrs.nil? || attrs.empty?
|
|
180
|
+
|
|
181
|
+
buffer = String.new('', encoding: Encoding::BINARY)
|
|
182
|
+
attrs.keys.sort.each do |key|
|
|
183
|
+
value = attrs[key]
|
|
184
|
+
buffer << length_delimited(key.to_s)
|
|
185
|
+
buffer << context_value_bytes(value)
|
|
186
|
+
end
|
|
187
|
+
buffer
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.flatten_value(prefix, value, output, seen, depth)
|
|
191
|
+
return if depth > MAX_CONTEXT_DEPTH
|
|
192
|
+
|
|
193
|
+
case value
|
|
194
|
+
when Hash
|
|
195
|
+
object_id = value.object_id
|
|
196
|
+
return if seen[object_id]
|
|
197
|
+
|
|
198
|
+
seen[object_id] = true
|
|
199
|
+
value.each do |key, child_value|
|
|
200
|
+
flatten_value("#{prefix}.#{key}", child_value, output, seen, depth + 1)
|
|
201
|
+
end
|
|
202
|
+
seen.delete(object_id)
|
|
203
|
+
when Array
|
|
204
|
+
object_id = value.object_id
|
|
205
|
+
return if seen[object_id]
|
|
206
|
+
|
|
207
|
+
seen[object_id] = true
|
|
208
|
+
value.each_with_index do |child_value, index|
|
|
209
|
+
flatten_value("#{prefix}.#{index}", child_value, output, seen, depth + 1)
|
|
210
|
+
end
|
|
211
|
+
seen.delete(object_id)
|
|
212
|
+
else
|
|
213
|
+
output[prefix] = value unless value.nil?
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
private_class_method :flatten_value
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def context_value_bytes(value)
|
|
221
|
+
tag, encoded = case value
|
|
222
|
+
when String then [CTX_TAG_STRING, value.to_s]
|
|
223
|
+
when TrueClass, FalseClass then [CTX_TAG_BOOL, value.to_s]
|
|
224
|
+
when Integer then [CTX_TAG_INTEGER, value.to_s]
|
|
225
|
+
when Float then [CTX_TAG_FLOAT, value.to_s]
|
|
226
|
+
else [CTX_TAG_OTHER, value.to_s]
|
|
227
|
+
end
|
|
228
|
+
String.new(tag, encoding: Encoding::BINARY) + length_delimited(encoded)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# 8-byte big-endian length prefix + raw bytes. Unambiguous field boundary.
|
|
232
|
+
def length_delimited(string)
|
|
233
|
+
bytes = string.encode(Encoding::BINARY, invalid: :replace, undef: :replace)
|
|
234
|
+
byte_length = bytes.bytesize
|
|
235
|
+
# Build 8-byte big-endian length
|
|
236
|
+
length_bytes = String.new('', encoding: Encoding::BINARY)
|
|
237
|
+
8.times do |index|
|
|
238
|
+
length_bytes.prepend(((byte_length >> (8 * index)) & 0xFF).chr(Encoding::BINARY))
|
|
239
|
+
end
|
|
240
|
+
length_bytes + bytes
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def new_entry(evaluation_time_ms, runtime_default:, error_message: nil, targeting_key: nil, context_attrs: nil)
|
|
244
|
+
{
|
|
245
|
+
count: 1,
|
|
246
|
+
first_evaluation: evaluation_time_ms,
|
|
247
|
+
last_evaluation: evaluation_time_ms,
|
|
248
|
+
runtime_default: runtime_default,
|
|
249
|
+
error_message: error_message,
|
|
250
|
+
targeting_key: targeting_key,
|
|
251
|
+
context_attrs: context_attrs,
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def observe(entry, evaluation_time_ms)
|
|
256
|
+
entry[:count] += 1
|
|
257
|
+
entry[:first_evaluation] = evaluation_time_ms if evaluation_time_ms < entry[:first_evaluation]
|
|
258
|
+
entry[:last_evaluation] = evaluation_time_ms if evaluation_time_ms > entry[:last_evaluation]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def add_to_degraded(flag_key, variant, allocation_key, runtime_default, error_message, evaluation_time_ms)
|
|
262
|
+
degraded_key = [flag_key, variant, allocation_key, runtime_default, error_message]
|
|
263
|
+
|
|
264
|
+
if (entry = @degraded[degraded_key])
|
|
265
|
+
observe(entry, evaluation_time_ms)
|
|
266
|
+
return
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# New degraded bucket — check degraded_cap (terminal tier)
|
|
270
|
+
if @degraded.size >= @degraded_cap
|
|
271
|
+
# Terminal tier full — drop and count (explicit overflow counter)
|
|
272
|
+
@dropped_degraded_overflow += 1
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Degraded entry omits targeting_key + context_attrs (schema omitempty fields)
|
|
277
|
+
@degraded[degraded_key] = new_entry(
|
|
278
|
+
evaluation_time_ms,
|
|
279
|
+
runtime_default: runtime_default,
|
|
280
|
+
error_message: error_message
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|