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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -1
  3. data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +5 -12
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +29 -18
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +17 -3
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +19 -24
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +0 -1
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +348 -213
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -0
  10. data/ext/datadog_profiling_native_extension/extconf.rb +11 -3
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -0
  12. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
  13. data/ext/datadog_profiling_native_extension/ruby_helpers.c +7 -0
  14. data/ext/datadog_profiling_native_extension/time_helpers.h +12 -7
  15. data/ext/libdatadog_api/di.c +76 -0
  16. data/ext/libdatadog_api/extconf.rb +6 -0
  17. data/ext/libdatadog_extconf_helpers.rb +1 -1
  18. data/lib/datadog/ai_guard/api_client.rb +6 -4
  19. data/lib/datadog/appsec/api_security/route_extractor.rb +7 -4
  20. data/lib/datadog/appsec/configuration.rb +6 -0
  21. data/lib/datadog/appsec/context.rb +4 -2
  22. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
  23. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
  24. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +1 -1
  25. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +5 -0
  26. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -0
  27. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +4 -17
  28. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
  29. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +7 -1
  30. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
  31. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +4 -0
  32. data/lib/datadog/appsec/ext.rb +1 -0
  33. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  34. data/lib/datadog/appsec/metrics/exporter.rb +9 -3
  35. data/lib/datadog/appsec/remote.rb +9 -3
  36. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  37. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  38. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  39. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  40. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  41. data/lib/datadog/core/configuration/agentless_settings_resolver.rb +2 -2
  42. data/lib/datadog/core/configuration/components.rb +82 -2
  43. data/lib/datadog/core/configuration/components_state.rb +6 -1
  44. data/lib/datadog/core/configuration/settings.rb +4 -7
  45. data/lib/datadog/core/configuration/supported_configurations.rb +3 -0
  46. data/lib/datadog/core/evp.rb +11 -0
  47. data/lib/datadog/core/knuth_sampler.rb +3 -1
  48. data/lib/datadog/core/metrics/client.rb +1 -1
  49. data/lib/datadog/core/remote/client/capabilities.rb +34 -7
  50. data/lib/datadog/core/telemetry/event/app_started.rb +8 -1
  51. data/lib/datadog/core/transport/http/builder.rb +1 -1
  52. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  53. data/lib/datadog/core/utils/time.rb +5 -1
  54. data/lib/datadog/di/base.rb +3 -1
  55. data/lib/datadog/di/boot.rb +3 -0
  56. data/lib/datadog/di/capture_expression.rb +21 -0
  57. data/lib/datadog/di/capture_expression_evaluator.rb +71 -0
  58. data/lib/datadog/di/capture_limits.rb +41 -0
  59. data/lib/datadog/di/code_tracker.rb +5 -2
  60. data/lib/datadog/di/component.rb +112 -38
  61. data/lib/datadog/di/configuration.rb +6 -0
  62. data/lib/datadog/di/context.rb +9 -3
  63. data/lib/datadog/di/el/compiler.rb +1 -1
  64. data/lib/datadog/di/error.rb +10 -0
  65. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  66. data/lib/datadog/di/instrumenter.rb +412 -165
  67. data/lib/datadog/di/probe.rb +61 -2
  68. data/lib/datadog/di/probe_builder.rb +83 -1
  69. data/lib/datadog/di/probe_file_loader.rb +8 -3
  70. data/lib/datadog/di/probe_manager.rb +67 -9
  71. data/lib/datadog/di/probe_notification_builder.rb +52 -8
  72. data/lib/datadog/di/probe_notifier_worker.rb +52 -32
  73. data/lib/datadog/di/redactor.rb +16 -1
  74. data/lib/datadog/di/remote.rb +176 -15
  75. data/lib/datadog/di/serializer.rb +34 -15
  76. data/lib/datadog/di/transport/input.rb +8 -4
  77. data/lib/datadog/di.rb +63 -0
  78. data/lib/datadog/kit/appsec/events/v2.rb +60 -2
  79. data/lib/datadog/open_feature/component.rb +29 -6
  80. data/lib/datadog/open_feature/configuration.rb +8 -0
  81. data/lib/datadog/open_feature/ext.rb +2 -0
  82. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
  83. data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
  84. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
  85. data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
  86. data/lib/datadog/open_feature/native_evaluator.rb +22 -0
  87. data/lib/datadog/open_feature/provider.rb +59 -27
  88. data/lib/datadog/open_feature/transport.rb +55 -1
  89. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
  90. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
  91. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  92. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +6 -1
  93. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
  94. data/lib/datadog/profiling/collectors/thread_context.rb +9 -1
  95. data/lib/datadog/profiling/component.rb +17 -0
  96. data/lib/datadog/symbol_database/component.rb +164 -47
  97. data/lib/datadog/symbol_database/configuration.rb +12 -4
  98. data/lib/datadog/symbol_database/extensions.rb +19 -0
  99. data/lib/datadog/symbol_database/extractor.rb +124 -87
  100. data/lib/datadog/symbol_database/file_hash.rb +3 -1
  101. data/lib/datadog/symbol_database/remote.rb +8 -3
  102. data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
  103. data/lib/datadog/symbol_database/uploader.rb +9 -2
  104. data/lib/datadog/symbol_database.rb +22 -0
  105. data/lib/datadog/tracing/contrib/active_job/data_streams.rb +62 -0
  106. data/lib/datadog/tracing/contrib/active_job/patcher.rb +12 -0
  107. data/lib/datadog/tracing/contrib/active_model_serializers/events/serialize.rb +2 -3
  108. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -12
  109. data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
  110. data/lib/datadog/tracing/contrib/sequel/utils.rb +2 -2
  111. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
  112. data/lib/datadog/tracing/contrib/sinatra/framework.rb +1 -1
  113. data/lib/datadog/tracing/distributed/baggage.rb +2 -1
  114. data/lib/datadog/tracing/ext.rb +9 -0
  115. data/lib/datadog/tracing/remote.rb +34 -3
  116. data/lib/datadog/tracing/transport/trace_formatter.rb +1 -1
  117. data/lib/datadog/version.rb +1 -1
  118. data/lib/datadog.rb +1 -0
  119. 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