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
|
@@ -23,27 +23,42 @@ module Datadog
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def fetch_value(flag_key, default_value:, expected_type:, evaluation_context: nil)
|
|
26
|
+
# Pre-bound to the safe default so the rescue below can still read it if the
|
|
27
|
+
# evaluator read itself raises.
|
|
28
|
+
observe_full_evaluation_data = false
|
|
29
|
+
# Snapshot the evaluator once: a concurrent reconfigure! must not retroactively
|
|
30
|
+
# apply a different environment's full-evaluation-data policy to this evaluation.
|
|
31
|
+
evaluator = @evaluator
|
|
32
|
+
observe_full_evaluation_data = evaluator.observe_full_evaluation_data == true
|
|
33
|
+
|
|
26
34
|
unless ALLOWED_TYPES.include?(expected_type)
|
|
27
35
|
message = "unknown type #{expected_type.inspect}, allowed types #{ALLOWED_TYPES.join(", ")}"
|
|
28
|
-
|
|
36
|
+
result = ResolutionDetails.build_error(
|
|
29
37
|
value: default_value, error_code: Ext::UNKNOWN_TYPE, error_message: message
|
|
30
38
|
)
|
|
39
|
+
|
|
40
|
+
return with_observe_full_evaluation_data(result, observe_full_evaluation_data)
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
context = evaluation_context&.fields.to_h
|
|
34
|
-
result =
|
|
44
|
+
result = evaluator.get_assignment(
|
|
35
45
|
flag_key, default_value: default_value, context: context, expected_type: expected_type
|
|
36
46
|
)
|
|
37
47
|
|
|
48
|
+
result = with_observe_full_evaluation_data(result, observe_full_evaluation_data)
|
|
49
|
+
|
|
38
50
|
@reporter.report(result, flag_key: flag_key, context: evaluation_context)
|
|
39
51
|
|
|
40
52
|
result
|
|
41
53
|
rescue => e
|
|
42
54
|
@telemetry.report(e, description: "OpenFeature: Failed to fetch flag value")
|
|
43
55
|
|
|
44
|
-
ResolutionDetails.build_error(
|
|
56
|
+
result = ResolutionDetails.build_error(
|
|
45
57
|
value: default_value, error_code: Ext::GENERAL, error_message: "#{e.class}: #{e.message}"
|
|
46
58
|
)
|
|
59
|
+
# `== true` is load-bearing: Steep types this local as `(nil | bool)` in a rescue
|
|
60
|
+
# body, and coercing at the use site is what narrows it to `bool`.
|
|
61
|
+
with_observe_full_evaluation_data(result, observe_full_evaluation_data == true)
|
|
47
62
|
end
|
|
48
63
|
|
|
49
64
|
# NOTE: In a currect implementation configuration is expected to be a raw
|
|
@@ -65,6 +80,17 @@ module Datadog
|
|
|
65
80
|
|
|
66
81
|
raise ReconfigurationError, "#{e.class}: #{e.message}"
|
|
67
82
|
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Stamps observe_full_evaluation_data onto the result metadata so the hook
|
|
87
|
+
# reads it from the event, not live config.
|
|
88
|
+
def with_observe_full_evaluation_data(result, observe_full_evaluation_data)
|
|
89
|
+
result = result.dup if result.frozen?
|
|
90
|
+
metadata = result.flag_metadata ||= {}
|
|
91
|
+
metadata[Ext::METADATA_OBSERVE_FULL_EVALUATION_DATA] = observe_full_evaluation_data
|
|
92
|
+
result
|
|
93
|
+
end
|
|
68
94
|
end
|
|
69
95
|
end
|
|
70
96
|
end
|
|
@@ -15,12 +15,14 @@ module Datadog
|
|
|
15
15
|
"#{flag_key}:#{context.targeting_key}"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# NOTE: The serial id is part of the value because it can appear or change on a
|
|
19
|
+
# configuration refresh while the allocation and the variant stay the same.
|
|
18
20
|
def cache_value(result, flag_key:, context:)
|
|
19
|
-
"#{result.allocation_key}:#{result.variant}"
|
|
21
|
+
"#{result.allocation_key}:#{result.variant}:#{result.serial_id}"
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def build(result, flag_key:, context:)
|
|
23
|
-
{
|
|
25
|
+
event = {
|
|
24
26
|
timestamp: current_timestamp_ms,
|
|
25
27
|
allocation: {
|
|
26
28
|
key: result.allocation_key,
|
|
@@ -35,7 +37,12 @@ module Datadog
|
|
|
35
37
|
id: context.targeting_key,
|
|
36
38
|
attributes: extract_attributes(context),
|
|
37
39
|
},
|
|
38
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
serial_id = result.serial_id
|
|
43
|
+
event[:serial_id] = serial_id unless serial_id.nil?
|
|
44
|
+
|
|
45
|
+
event.freeze
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
private
|
|
@@ -11,11 +11,30 @@ module Datadog
|
|
|
11
11
|
GENERAL = "GENERAL"
|
|
12
12
|
PROVIDER_FATAL = "PROVIDER_FATAL"
|
|
13
13
|
PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
|
|
14
|
+
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
|
|
15
|
+
TYPE_MISMATCH = "TYPE_MISMATCH"
|
|
16
|
+
TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
|
|
17
|
+
INVALID_CONTEXT = "INVALID_CONTEXT"
|
|
18
|
+
|
|
19
|
+
STANDARD_ERROR_CODES = [
|
|
20
|
+
PROVIDER_NOT_READY,
|
|
21
|
+
FLAG_NOT_FOUND,
|
|
22
|
+
PARSE_ERROR,
|
|
23
|
+
TYPE_MISMATCH,
|
|
24
|
+
TARGETING_KEY_MISSING,
|
|
25
|
+
INVALID_CONTEXT,
|
|
26
|
+
PROVIDER_FATAL,
|
|
27
|
+
GENERAL,
|
|
28
|
+
].freeze
|
|
14
29
|
|
|
15
30
|
# Flag-metadata key under which the provider threads the assignment's
|
|
16
31
|
# allocation key to the flag-evaluation hooks. The wire string is the
|
|
17
32
|
# value so the writer (provider) and readers (EVP/metrics hooks) can't drift.
|
|
18
33
|
METADATA_ALLOCATION_KEY = "__dd_allocation_key"
|
|
34
|
+
|
|
35
|
+
# Stamped from the UFC the evaluation ran against; the hook reads only this
|
|
36
|
+
# key, never live config.
|
|
37
|
+
METADATA_OBSERVE_FULL_EVALUATION_DATA = "__dd_observe_full_evaluation_data"
|
|
19
38
|
end
|
|
20
39
|
end
|
|
21
40
|
end
|
|
@@ -1,21 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Datadog
|
|
4
6
|
module OpenFeature
|
|
5
7
|
module FlagEvaluation
|
|
6
8
|
# Two-tier aggregation for EVP flagevaluation events.
|
|
7
9
|
#
|
|
8
10
|
# Two-tier design:
|
|
9
|
-
# - full-tier
|
|
11
|
+
# - full-tier key: (flag_key, variant, allocation_key, runtime_default, error_message,
|
|
12
|
+
# targeting_key, canonical_context_key, observe_full_evaluation_data)
|
|
10
13
|
# - degraded-tier key: (flag_key, variant, allocation_key, runtime_default, error_message)
|
|
11
14
|
# - Drop-and-count when degraded tier is full
|
|
12
15
|
# - canonical_context_key: sorted type-tagged length-delimited encoding (no hash digest)
|
|
13
16
|
# - Caps: global_cap=131_072 / per_flag_cap=10_000 / degraded_cap=32_768
|
|
14
|
-
#
|
|
17
|
+
#
|
|
18
|
+
# Context bounding: the writer applies `bounded_context_snapshot` on the evaluation
|
|
19
|
+
# thread before enqueue, so the queue only ever holds an already-bounded, flattened
|
|
20
|
+
# snapshot. `record` receives that snapshot and keys on it directly.
|
|
15
21
|
class Aggregator
|
|
22
|
+
# Cross-SDK context caps. The per-dimension caps match Go and Java.
|
|
16
23
|
MAX_CONTEXT_FIELDS = 256
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
MAX_VALUE_LENGTH = 256
|
|
25
|
+
MAX_KEY_LENGTH = 256
|
|
26
|
+
MAX_LIST_ELEMENTS = 256
|
|
27
|
+
MAX_STRUCTURE_PROPERTIES = 256
|
|
28
|
+
MAX_SNAPSHOT_DEPTH = 4
|
|
29
|
+
|
|
30
|
+
# Total-node budget bounding leaf-free shared subtrees, which do not increase the
|
|
31
|
+
# retained field count. It still permits every retained field to sit at the
|
|
32
|
+
# maximum snapshot depth.
|
|
33
|
+
MAX_VISITED_NODES = MAX_CONTEXT_FIELDS * (MAX_SNAPSHOT_DEPTH + 1)
|
|
34
|
+
|
|
35
|
+
# Truncation reason labels, surfaced on the `flagevaluation.context.truncated`
|
|
36
|
+
# telemetry counter so operators can tell which cap was hit.
|
|
37
|
+
REASON_MAX_CONTEXT_FIELDS = "max_context_fields"
|
|
38
|
+
REASON_MAX_VALUE_LENGTH = "max_value_length"
|
|
39
|
+
REASON_MAX_KEY_LENGTH = "max_key_length"
|
|
40
|
+
REASON_MAX_LIST_ELEMENTS = "max_list_elements"
|
|
41
|
+
REASON_MAX_STRUCTURE_PROPERTIES = "max_structure_properties"
|
|
42
|
+
REASON_MAX_SNAPSHOT_DEPTH = "max_snapshot_depth"
|
|
43
|
+
REASON_MAX_VISITED_NODES = "max_visited_nodes"
|
|
44
|
+
REASON_CYCLE = "cycle"
|
|
19
45
|
|
|
20
46
|
# Type tags so values of different Ruby types never collide in the canonical key.
|
|
21
47
|
CTX_TAG_STRING = "s"
|
|
@@ -62,35 +88,44 @@ module Datadog
|
|
|
62
88
|
# Record one evaluation event. Thread-safe. Called from the background writer.
|
|
63
89
|
def record(
|
|
64
90
|
flag_key:, variant:, allocation_key:, targeting_key:, eval_time_ms:, attrs:, error_message: nil,
|
|
65
|
-
runtime_default: nil
|
|
91
|
+
runtime_default: nil, observe_full_evaluation_data: false
|
|
66
92
|
)
|
|
67
93
|
runtime_default = variant.nil? if runtime_default.nil?
|
|
68
94
|
runtime_default = !!runtime_default
|
|
95
|
+
# Runtime defaults have no resolved variant or allocation in the EVP schema.
|
|
96
|
+
if runtime_default
|
|
97
|
+
variant = nil
|
|
98
|
+
allocation_key = nil
|
|
99
|
+
end
|
|
100
|
+
observe_full_evaluation_data = !!observe_full_evaluation_data
|
|
69
101
|
|
|
70
|
-
#
|
|
102
|
+
# Preserve targeting-key presence; other dimensions use empty strings.
|
|
71
103
|
variant = variant.to_s
|
|
72
104
|
allocation_key = allocation_key.to_s
|
|
73
105
|
error_message = error_message.to_s
|
|
74
|
-
targeting_key = targeting_key.to_s
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
targeting_key = targeting_key.to_s unless targeting_key.nil?
|
|
107
|
+
|
|
108
|
+
context_key = observe_full_evaluation_data ? canonical_context_key(attrs) : nil
|
|
109
|
+
# @type var full_key: full_key
|
|
110
|
+
full_key = [
|
|
111
|
+
flag_key, variant, allocation_key, runtime_default, error_message,
|
|
112
|
+
targeting_key, context_key, observe_full_evaluation_data,
|
|
113
|
+
]
|
|
82
114
|
evaluation_time_ms = eval_time_ms.to_i
|
|
83
115
|
|
|
84
116
|
@mutex.synchronize do
|
|
85
117
|
# --- Full tier ---
|
|
86
118
|
if (entry = @full[full_key])
|
|
87
|
-
observe(entry, evaluation_time_ms)
|
|
119
|
+
observe(entry, evaluation_time_ms, observe_full_evaluation_data)
|
|
88
120
|
return
|
|
89
121
|
end
|
|
90
122
|
|
|
91
123
|
per_flag_count = @per_flag_full[flag_key]
|
|
92
124
|
if per_flag_count >= @per_flag_cap
|
|
93
|
-
add_to_degraded(
|
|
125
|
+
add_to_degraded(
|
|
126
|
+
flag_key, variant, allocation_key, runtime_default, error_message, evaluation_time_ms,
|
|
127
|
+
observe_full_evaluation_data
|
|
128
|
+
)
|
|
94
129
|
return
|
|
95
130
|
end
|
|
96
131
|
|
|
@@ -104,21 +139,24 @@ module Datadog
|
|
|
104
139
|
runtime_default: runtime_default,
|
|
105
140
|
error_message: error_message,
|
|
106
141
|
targeting_key: targeting_key,
|
|
107
|
-
context_attrs:
|
|
142
|
+
context_attrs: observe_full_evaluation_data ? attrs : nil,
|
|
143
|
+
observe_full_evaluation_data: observe_full_evaluation_data
|
|
108
144
|
)
|
|
109
145
|
@full[full_key] = entry
|
|
110
146
|
@global_count += 1
|
|
111
147
|
else
|
|
112
148
|
# Route to degraded tier
|
|
113
|
-
add_to_degraded(
|
|
149
|
+
add_to_degraded(
|
|
150
|
+
flag_key, variant, allocation_key, runtime_default, error_message, evaluation_time_ms,
|
|
151
|
+
observe_full_evaluation_data
|
|
152
|
+
)
|
|
114
153
|
end
|
|
115
154
|
end
|
|
116
155
|
end
|
|
117
156
|
|
|
118
157
|
# Flush aggregation maps, reset state, return snapshot.
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
# reset — the count is never reset-without-emit (backpressure stays observable).
|
|
158
|
+
# The overflow count is included in the snapshot so the caller can emit it before
|
|
159
|
+
# it is reset (never reset-without-emit).
|
|
122
160
|
def flush_and_reset
|
|
123
161
|
@mutex.synchronize do
|
|
124
162
|
full_snapshot = @full
|
|
@@ -135,85 +173,192 @@ module Datadog
|
|
|
135
173
|
end
|
|
136
174
|
end
|
|
137
175
|
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 = {}
|
|
176
|
+
# Bound and flatten the caller's evaluation context on the evaluation thread,
|
|
177
|
+
# before enqueue, so the async queue only ever holds an already-bounded snapshot.
|
|
178
|
+
# Returns the flattened (dot-notation) context and the truncation reasons hit,
|
|
179
|
+
# which the writer surfaces on the `flagevaluation.context.truncated` counter.
|
|
180
|
+
#
|
|
181
|
+
# Ruby Hash insertion order lets traversal stop at the caps without sorting
|
|
182
|
+
# or scanning the complete input. Ruby therefore truncates where Go omits,
|
|
183
|
+
# whose map iteration is randomized per call.
|
|
184
|
+
def self.bounded_context_snapshot(attrs, excluded_key: nil)
|
|
185
|
+
return [{}, []] unless attrs.is_a?(Hash) && !attrs.empty?
|
|
186
|
+
|
|
187
|
+
flattened = {}
|
|
188
|
+
reasons = Set.new
|
|
167
189
|
seen = {attrs.object_id => true}
|
|
190
|
+
walked = 0
|
|
191
|
+
# Single-element array so the recursion shares one mutable counter.
|
|
192
|
+
budget = [MAX_VISITED_NODES]
|
|
168
193
|
attrs.each do |key, value|
|
|
169
|
-
|
|
194
|
+
key = context_key_string(key)
|
|
195
|
+
next unless key
|
|
196
|
+
next if key == excluded_key
|
|
197
|
+
|
|
198
|
+
if flattened.size >= MAX_CONTEXT_FIELDS
|
|
199
|
+
reasons << REASON_MAX_CONTEXT_FIELDS
|
|
200
|
+
reasons << REASON_MAX_STRUCTURE_PROPERTIES if walked >= MAX_STRUCTURE_PROPERTIES
|
|
201
|
+
break
|
|
202
|
+
end
|
|
203
|
+
if walked >= MAX_STRUCTURE_PROPERTIES
|
|
204
|
+
reasons << REASON_MAX_STRUCTURE_PROPERTIES
|
|
205
|
+
break
|
|
206
|
+
end
|
|
207
|
+
if budget[0] <= 0
|
|
208
|
+
reasons << REASON_MAX_VISITED_NODES
|
|
209
|
+
break
|
|
210
|
+
end
|
|
211
|
+
walked += 1
|
|
212
|
+
if key.length > MAX_KEY_LENGTH
|
|
213
|
+
reasons << REASON_MAX_KEY_LENGTH
|
|
214
|
+
next
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
key = key.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
218
|
+
bounded_flatten(key, value, flattened, seen, 0, reasons, budget)
|
|
170
219
|
end
|
|
171
|
-
|
|
220
|
+
[flattened, reasons.to_a]
|
|
172
221
|
end
|
|
173
222
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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)
|
|
223
|
+
def self.context_key_string(key)
|
|
224
|
+
case key
|
|
225
|
+
when String then String.new(key)
|
|
226
|
+
when Symbol then key.to_s
|
|
186
227
|
end
|
|
187
|
-
|
|
228
|
+
rescue
|
|
229
|
+
# Unsupported caller keys must not break flag evaluation.
|
|
230
|
+
nil
|
|
188
231
|
end
|
|
232
|
+
private_class_method :context_key_string
|
|
233
|
+
|
|
234
|
+
def self.bounded_flatten(prefix, value, output, seen, depth, reasons, budget)
|
|
235
|
+
# Charge every node first, before any early return. A node that exits early still
|
|
236
|
+
# cost a call, and the cheap exits (nil leaves especially) are exactly what an
|
|
237
|
+
# adversarial context is made of.
|
|
238
|
+
if budget[0] <= 0
|
|
239
|
+
reasons << REASON_MAX_VISITED_NODES
|
|
240
|
+
return
|
|
241
|
+
end
|
|
242
|
+
budget[0] -= 1
|
|
189
243
|
|
|
190
|
-
|
|
191
|
-
|
|
244
|
+
return if value.nil?
|
|
245
|
+
if output.size >= MAX_CONTEXT_FIELDS
|
|
246
|
+
reasons << REASON_MAX_CONTEXT_FIELDS
|
|
247
|
+
return
|
|
248
|
+
end
|
|
249
|
+
if depth >= MAX_SNAPSHOT_DEPTH
|
|
250
|
+
reasons << REASON_MAX_SNAPSHOT_DEPTH
|
|
251
|
+
return
|
|
252
|
+
end
|
|
253
|
+
if prefix.length > MAX_KEY_LENGTH
|
|
254
|
+
reasons << REASON_MAX_KEY_LENGTH
|
|
255
|
+
return
|
|
256
|
+
end
|
|
192
257
|
|
|
193
258
|
case value
|
|
194
259
|
when Hash
|
|
260
|
+
return if value.empty?
|
|
261
|
+
|
|
195
262
|
object_id = value.object_id
|
|
196
|
-
|
|
263
|
+
if seen[object_id]
|
|
264
|
+
reasons << REASON_CYCLE
|
|
265
|
+
return
|
|
266
|
+
end
|
|
197
267
|
|
|
198
268
|
seen[object_id] = true
|
|
269
|
+
reasons << REASON_MAX_STRUCTURE_PROPERTIES if value.size > MAX_STRUCTURE_PROPERTIES
|
|
270
|
+
walked = 0
|
|
199
271
|
value.each do |key, child_value|
|
|
200
|
-
|
|
272
|
+
if output.size >= MAX_CONTEXT_FIELDS
|
|
273
|
+
reasons << REASON_MAX_CONTEXT_FIELDS
|
|
274
|
+
break
|
|
275
|
+
end
|
|
276
|
+
break if walked >= MAX_STRUCTURE_PROPERTIES
|
|
277
|
+
if budget[0] <= 0
|
|
278
|
+
reasons << REASON_MAX_VISITED_NODES
|
|
279
|
+
break
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
walked += 1
|
|
283
|
+
child_key = context_key_string(key)
|
|
284
|
+
next unless child_key
|
|
285
|
+
if prefix.length + 1 + child_key.length > MAX_KEY_LENGTH
|
|
286
|
+
reasons << REASON_MAX_KEY_LENGTH
|
|
287
|
+
next
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
child_key = child_key.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
291
|
+
bounded_flatten("#{prefix}.#{child_key}", child_value, output, seen, depth + 1, reasons, budget)
|
|
201
292
|
end
|
|
202
293
|
seen.delete(object_id)
|
|
203
294
|
when Array
|
|
295
|
+
return if value.empty?
|
|
296
|
+
|
|
204
297
|
object_id = value.object_id
|
|
205
|
-
|
|
298
|
+
if seen[object_id]
|
|
299
|
+
reasons << REASON_CYCLE
|
|
300
|
+
return
|
|
301
|
+
end
|
|
206
302
|
|
|
207
303
|
seen[object_id] = true
|
|
304
|
+
reasons << REASON_MAX_LIST_ELEMENTS if value.size > MAX_LIST_ELEMENTS
|
|
305
|
+
walked = 0
|
|
208
306
|
value.each_with_index do |child_value, index|
|
|
209
|
-
|
|
307
|
+
if output.size >= MAX_CONTEXT_FIELDS
|
|
308
|
+
reasons << REASON_MAX_CONTEXT_FIELDS
|
|
309
|
+
break
|
|
310
|
+
end
|
|
311
|
+
break if walked >= MAX_LIST_ELEMENTS
|
|
312
|
+
if budget[0] <= 0
|
|
313
|
+
reasons << REASON_MAX_VISITED_NODES
|
|
314
|
+
break
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
walked += 1
|
|
318
|
+
bounded_flatten("#{prefix}.#{index}", child_value, output, seen, depth + 1, reasons, budget)
|
|
210
319
|
end
|
|
211
320
|
seen.delete(object_id)
|
|
212
321
|
else
|
|
213
|
-
|
|
322
|
+
if value.is_a?(String)
|
|
323
|
+
begin
|
|
324
|
+
string_value = String.new(value)
|
|
325
|
+
if string_value.length > MAX_VALUE_LENGTH
|
|
326
|
+
reasons << REASON_MAX_VALUE_LENGTH
|
|
327
|
+
return
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
output[prefix] = string_value.encode(
|
|
331
|
+
Encoding::UTF_8, invalid: :replace, undef: :replace
|
|
332
|
+
).freeze
|
|
333
|
+
rescue
|
|
334
|
+
# Unsupported caller values must not break flag evaluation.
|
|
335
|
+
end
|
|
336
|
+
return
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
case value
|
|
340
|
+
when TrueClass, FalseClass, Integer, Float
|
|
341
|
+
output[prefix] = value
|
|
342
|
+
end
|
|
214
343
|
end
|
|
215
344
|
end
|
|
216
|
-
private_class_method :
|
|
345
|
+
private_class_method :bounded_flatten
|
|
346
|
+
|
|
347
|
+
# Canonical context key: sorted type-tagged length-delimited encoding.
|
|
348
|
+
# Each field is: 8-byte big-endian key length + key bytes + type-tag byte +
|
|
349
|
+
# 8-byte big-endian value length + value bytes.
|
|
350
|
+
# No hash digest — the key IS the full encoding (collision-free, no FNV).
|
|
351
|
+
def canonical_context_key(attrs)
|
|
352
|
+
return "" if attrs.nil? || attrs.empty?
|
|
353
|
+
|
|
354
|
+
buffer = String.new("", encoding: Encoding::BINARY)
|
|
355
|
+
attrs.keys.sort.each do |key|
|
|
356
|
+
value = attrs[key]
|
|
357
|
+
buffer << length_delimited(key.to_s)
|
|
358
|
+
buffer << context_value_bytes(value)
|
|
359
|
+
end
|
|
360
|
+
buffer
|
|
361
|
+
end
|
|
217
362
|
|
|
218
363
|
private
|
|
219
364
|
|
|
@@ -230,7 +375,8 @@ module Datadog
|
|
|
230
375
|
|
|
231
376
|
# 8-byte big-endian length prefix + raw bytes. Unambiguous field boundary.
|
|
232
377
|
def length_delimited(string)
|
|
233
|
-
|
|
378
|
+
utf8 = string.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
379
|
+
bytes = utf8.dup.force_encoding(Encoding::BINARY)
|
|
234
380
|
byte_length = bytes.bytesize
|
|
235
381
|
# Build 8-byte big-endian length
|
|
236
382
|
length_bytes = String.new("", encoding: Encoding::BINARY)
|
|
@@ -240,7 +386,10 @@ module Datadog
|
|
|
240
386
|
length_bytes + bytes
|
|
241
387
|
end
|
|
242
388
|
|
|
243
|
-
def new_entry(
|
|
389
|
+
def new_entry(
|
|
390
|
+
evaluation_time_ms, runtime_default:, observe_full_evaluation_data:, error_message: nil,
|
|
391
|
+
targeting_key: nil, context_attrs: nil
|
|
392
|
+
)
|
|
244
393
|
{
|
|
245
394
|
count: 1,
|
|
246
395
|
first_evaluation: evaluation_time_ms,
|
|
@@ -249,20 +398,26 @@ module Datadog
|
|
|
249
398
|
error_message: error_message,
|
|
250
399
|
targeting_key: targeting_key,
|
|
251
400
|
context_attrs: context_attrs,
|
|
401
|
+
observe_full_evaluation_data: observe_full_evaluation_data,
|
|
252
402
|
}
|
|
253
403
|
end
|
|
254
404
|
|
|
255
|
-
def observe(entry, evaluation_time_ms)
|
|
405
|
+
def observe(entry, evaluation_time_ms, observe_full_evaluation_data)
|
|
256
406
|
entry[:count] += 1
|
|
257
407
|
entry[:first_evaluation] = evaluation_time_ms if evaluation_time_ms < entry[:first_evaluation]
|
|
258
408
|
entry[:last_evaluation] = evaluation_time_ms if evaluation_time_ms > entry[:last_evaluation]
|
|
409
|
+
entry[:observe_full_evaluation_data] &&= observe_full_evaluation_data
|
|
259
410
|
end
|
|
260
411
|
|
|
261
|
-
def add_to_degraded(
|
|
412
|
+
def add_to_degraded(
|
|
413
|
+
flag_key, variant, allocation_key, runtime_default, error_message, evaluation_time_ms,
|
|
414
|
+
observe_full_evaluation_data
|
|
415
|
+
)
|
|
416
|
+
# @type var degraded_key: degraded_key
|
|
262
417
|
degraded_key = [flag_key, variant, allocation_key, runtime_default, error_message]
|
|
263
418
|
|
|
264
419
|
if (entry = @degraded[degraded_key])
|
|
265
|
-
observe(entry, evaluation_time_ms)
|
|
420
|
+
observe(entry, evaluation_time_ms, observe_full_evaluation_data)
|
|
266
421
|
return
|
|
267
422
|
end
|
|
268
423
|
|
|
@@ -273,11 +428,12 @@ module Datadog
|
|
|
273
428
|
return
|
|
274
429
|
end
|
|
275
430
|
|
|
276
|
-
# Degraded
|
|
431
|
+
# Degraded entries omit targeting_key and context_attrs.
|
|
277
432
|
@degraded[degraded_key] = new_entry(
|
|
278
433
|
evaluation_time_ms,
|
|
279
434
|
runtime_default: runtime_default,
|
|
280
|
-
error_message: error_message
|
|
435
|
+
error_message: error_message,
|
|
436
|
+
observe_full_evaluation_data: observe_full_evaluation_data
|
|
281
437
|
)
|
|
282
438
|
end
|
|
283
439
|
end
|