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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'fatal_exceptions'
4
+
3
5
  module Datadog
4
6
  module DI
5
7
  # Provides an interface expected by the core Remote subsystem to
@@ -15,14 +17,125 @@ module Datadog
15
17
  class << self
16
18
  PRODUCT = 'LIVE_DEBUGGING'
17
19
 
20
+ # Declared here (not in Tracing::Remote::CAPABILITIES) so it is
21
+ # registered only with the gated DI block in Capabilities#register:
22
+ # when DI is explicitly disabled, or the runtime cannot run DI
23
+ # (JRuby, Ruby 2.5), that block — including this bit — is skipped.
24
+ # The enable signal itself is delivered in APM_TRACING payloads and
25
+ # routed here by Tracing::Remote.process_config.
26
+ CAPABILITIES = [
27
+ 1 << 38, # APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION: Implicit DI enablement
28
+ ].freeze
29
+
18
30
  def products
19
- # TODO: do not send our product on unsupported runtimes
20
- # (Ruby 2.5 / JRuby)
21
31
  [PRODUCT]
22
32
  end
23
33
 
24
34
  def capabilities
25
- []
35
+ CAPABILITIES
36
+ end
37
+
38
+ # Entry point for the RC-driven DI enable/disable path.
39
+ #
40
+ # Invoked from {Datadog::Tracing::Remote.process_config} when an
41
+ # APM_TRACING payload carries `dynamic_instrumentation_enabled`. Runs
42
+ # on the remote-config thread; never raises.
43
+ #
44
+ # @param enabled [Boolean] desired state from RC: true to start DI,
45
+ # false to stop it. The `DD_DYNAMIC_INSTRUMENTATION_ENABLED=false`
46
+ # env var blocks an enable here (see {.explicitly_disabled?}).
47
+ # @param repository [Datadog::Core::Remote::Configuration::Repository, nil]
48
+ # the RC repository, passed by {Datadog::Tracing::Remote.process_config}
49
+ # so that a stopped->started transition can reconcile against probes that
50
+ # were delivered in an earlier poll. nil when called outside the RC
51
+ # dispatch path (e.g. unit tests), in which case no reconcile happens.
52
+ # @return [void]
53
+ def handle_rc_enablement(enabled, repository = nil)
54
+ # allow_initialization: false because this runs on the remote-config
55
+ # thread (a callback context). The default `true` would synchronously
56
+ # build the entire component tree from the wrong thread if the RC
57
+ # signal lands before Components#initialize completed.
58
+ components = Datadog.send(:components, allow_initialization: false)
59
+ component = components&.dynamic_instrumentation
60
+ unless component
61
+ # The component is nil because Component.build returned nil at
62
+ # startup — a runtime precondition is not met (RC disabled, MRI
63
+ # required, Ruby 2.6+ required, Rails dev env, C extension absent).
64
+ # On disable, silently no-op: RC asking us to turn off something we
65
+ # don't have is fine. On enable, warn with the reason: this is the
66
+ # implicit-enablement counterpart to the warn-on-explicit message
67
+ # at build time. The customer who clicked "create probe" in the UI
68
+ # gets the same visibility a customer who set
69
+ # DD_DYNAMIC_INSTRUMENTATION_ENABLED would have gotten at boot.
70
+ if enabled
71
+ if explicitly_disabled?
72
+ Datadog.logger.warn(
73
+ "di: cannot enable dynamic instrumentation via remote configuration " \
74
+ "because DD_DYNAMIC_INSTRUMENTATION_ENABLED is explicitly set to false",
75
+ )
76
+ else
77
+ reason = DI.unsupported_reason
78
+ Datadog.logger.warn(
79
+ "di: cannot enable dynamic instrumentation via remote configuration: " \
80
+ "#{reason || "dynamic instrumentation was not initialized at startup"}",
81
+ )
82
+ end
83
+ end
84
+ return
85
+ end
86
+
87
+ if enabled
88
+ if explicitly_disabled?
89
+ Datadog.logger.warn(
90
+ "di: ignoring implicit enablement signal from remote configuration " \
91
+ "because DD_DYNAMIC_INSTRUMENTATION_ENABLED is explicitly set to false. " \
92
+ "To allow remote enablement, unset DD_DYNAMIC_INSTRUMENTATION_ENABLED.",
93
+ )
94
+ return
95
+ end
96
+ # component is non-nil here only because Component.build's preconditions
97
+ # passed, which is the same condition under which di/base.rb is loaded
98
+ # and DI.activate_tracking is defined.
99
+ DI.activate_tracking
100
+ was_started = component.started?
101
+ component.start!
102
+ # A probe delivered in an earlier poll while the component was stopped
103
+ # was dropped by #receivers (which ignores changes while !started?) and
104
+ # never entered the probe repository. RC dispatch only re-delivers a
105
+ # config when its content hash changes (Core::Remote::Client#apply_config
106
+ # skips unchanged content), so the probe would otherwise stay dropped
107
+ # until the customer edits it. On the stopped->started transition,
108
+ # reconcile against the current LIVE_DEBUGGING contents so it installs now.
109
+ replay_current_probes(repository, component) if repository && !was_started
110
+ else
111
+ component.stop!
112
+ end
113
+ rescue => e
114
+ Datadog.logger.debug { "di: error handling implicit enablement: #{e.class}: #{e.message}" }
115
+ Datadog.send(:components, allow_initialization: false)&.telemetry&.report(
116
+ e,
117
+ description: "Error handling DI implicit enablement",
118
+ )
119
+ end
120
+
121
+ # Symmetric to {DI::Component.explicitly_enabled?} (see there for why
122
+ # using_default? rather than options[:enabled].default_precedence?).
123
+ #
124
+ # Canonical home for the explicit-disable check: it gates
125
+ # {DI::Component.build} and the DI block in
126
+ # {Core::Remote::Client::Capabilities#register}. The latter runs before
127
+ # DI::Component is loaded, so the check lives here on DI::Remote (always
128
+ # required by capabilities.rb) rather than on DI::Component. The
129
+ # `settings` argument lets those startup callers pass the settings being
130
+ # configured; the RC handler omits it and reads the global config.
131
+ #
132
+ # @param settings [Datadog::Core::Configuration::Settings]
133
+ # @return [Boolean] true when the customer set
134
+ # `DD_DYNAMIC_INSTRUMENTATION_ENABLED=false` (or the programmatic
135
+ # equivalent), which blocks DI build and RC-driven enablement.
136
+ def explicitly_disabled?(settings = Datadog.configuration)
137
+ !settings.dynamic_instrumentation.using_default?(:enabled) &&
138
+ !settings.dynamic_instrumentation.enabled
26
139
  end
27
140
 
28
141
  def receivers(telemetry)
@@ -32,17 +145,18 @@ module Datadog
32
145
  # DEV: Apply this refactor to AppSec as well if implemented.
33
146
 
34
147
  component = DI.component
35
- # We should always have a non-nil DI component here, because we
36
- # only add DI product to remote config request if DI is enabled.
37
- # Ideally, we should be injected with the DI component here
38
- # rather than having to retrieve it from global state.
39
- # If the component is nil for some reason, we also don't have a
40
- # logger instance to report the issue.
41
- if component
148
+ if component&.started?
42
149
  changes.each do |change|
43
150
  case change.type
44
151
  when :insert
45
- add_probe(change.content, component) # steep:ignore NoMethod
152
+ # A stopped->started reconcile (#replay_current_probes) may have
153
+ # installed this probe earlier in the same dispatch — the enable
154
+ # signal is carried by the Tracing receiver, which runs before the
155
+ # DI receiver. Skip the redundant install rather than letting
156
+ # probe_manager raise AlreadyInstrumented and report a false error.
157
+ unless probe_in_content_known?(change.content, component) # steep:ignore NoMethod
158
+ add_probe(change.content, component) # steep:ignore NoMethod
159
+ end
46
160
  when :update
47
161
  # We do not implement updates at the moment, remove the
48
162
  # probe and reinstall.
@@ -89,7 +203,8 @@ module Datadog
89
203
  # message.
90
204
  # TODO assert content state (errored for this example)
91
205
  content.errored("Error applying dynamic instrumentation configuration: #{exc.class}: #{exc.message}")
92
- rescue => exc
206
+ rescue Exception => exc # standard:disable Lint/RescueException
207
+ Datadog::DI.reraise_if_fatal(exc)
93
208
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
94
209
 
95
210
  component.logger.debug { "di: unhandled exception adding #{probe.type} probe at #{probe.location} (#{probe.id}) in DI remote receiver: #{exc.class}: #{exc.message}" }
@@ -113,8 +228,9 @@ module Datadog
113
228
  # we need to note it as being current so that we do not
114
229
  # try to remove instrumentation that is still supposed to be
115
230
  # active.
116
- #current_probe_ids[probe_spec.fetch('id')] = true
117
- rescue => exc
231
+ # current_probe_ids[probe_spec.fetch('id')] = true
232
+ rescue Exception => exc # standard:disable Lint/RescueException
233
+ Datadog::DI.reraise_if_fatal(exc)
118
234
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
119
235
 
120
236
  component.logger.debug { "di: unhandled exception handling a probe in DI remote receiver: #{exc.class}: #{exc.message}" }
@@ -124,6 +240,50 @@ module Datadog
124
240
  content.errored("Error applying dynamic instrumentation configuration: #{exc.class}: #{exc.message}")
125
241
  end
126
242
 
243
+ # Reconciles the DI probe repository against the LIVE_DEBUGGING configs
244
+ # currently held by the RC repository. Called on the stopped->started
245
+ # transition to install probes that arrived while DI was stopped: those
246
+ # were dropped by {.receivers} and RC will not re-dispatch them because
247
+ # their content hash is unchanged.
248
+ #
249
+ # Probes already tracked by the component (installed, pending, or failed)
250
+ # are skipped, so a probe handled in the same dispatch by {.receivers} is
251
+ # not added twice.
252
+ def replay_current_probes(repository, component)
253
+ repository.contents.each do |content|
254
+ next unless content.path.product == PRODUCT
255
+
256
+ begin
257
+ probe_id = parse_content(content)["id"]
258
+ rescue => exc
259
+ component.logger.debug { "di: skipping unparseable LIVE_DEBUGGING content on enable reconcile: #{exc.class}: #{exc.message}" }
260
+ next
261
+ end
262
+
263
+ next if probe_id && probe_known?(probe_id, component)
264
+
265
+ add_probe(content, component)
266
+ end
267
+ end
268
+
269
+ def probe_known?(probe_id, component)
270
+ repo = component.probe_manager.probe_repository
271
+ !!(repo.find_installed(probe_id) ||
272
+ repo.find_pending(probe_id) ||
273
+ repo.find_failed(probe_id))
274
+ end
275
+
276
+ # True when the probe carried by +content+ is already tracked by the
277
+ # component. Returns false when the id cannot be determined, so add_probe
278
+ # still runs and reports the parse error through its own handling.
279
+ def probe_in_content_known?(content, component)
280
+ probe_id = parse_content(content)["id"]
281
+ !!(probe_id && probe_known?(probe_id, component))
282
+ rescue => exc
283
+ component.logger.debug { "di: could not check probe id for insert dedup: #{exc.class}: #{exc.message}" }
284
+ false
285
+ end
286
+
127
287
  # This method does not mark +previous_content+ as succeeded or errored,
128
288
  # because that content is from a previous RC response and has already
129
289
  # been marked. Removal of probes happens when an RC entry disappears,
@@ -133,7 +293,8 @@ module Datadog
133
293
  probe_spec = parse_content(previous_content)
134
294
  probe_id = probe_spec.fetch('id')
135
295
  component.probe_manager.remove_probe(probe_id)
136
- rescue => exc
296
+ rescue Exception => exc # standard:disable Lint/RescueException
297
+ Datadog::DI.reraise_if_fatal(exc)
137
298
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
138
299
 
139
300
  component.logger.debug { "di: unhandled exception removing probes in DI remote receiver: #{exc.class}: #{exc.message}" }
@@ -44,8 +44,11 @@ module Datadog
44
44
  class Serializer
45
45
  # Exception classes that should never be caught during serialization.
46
46
  # These represent fatal conditions (signals, interrupts, system exit)
47
- # that must propagate to the caller.
48
- FATAL_EXCEPTION_CLASSES = [SignalException, Interrupt, SystemExit].freeze
47
+ # that must propagate to the caller. NoMemoryError is deliberately
48
+ # excluded (unlike DI::FATAL_EXCEPTION_CLASSES): serialization of large
49
+ # or deeply recursive objects can exhaust memory, and the serializer must
50
+ # return a safe stub rather than tear the process down.
51
+ SERIALIZABLE_FATAL_EXCEPTION_CLASSES = [SystemExit, SignalException].freeze
49
52
 
50
53
  # Third-party library integration / custom serializers.
51
54
  #
@@ -128,9 +131,12 @@ module Datadog
128
131
  # in upstream code.
129
132
  def serialize_args(args, kwargs, target_self,
130
133
  depth: settings.dynamic_instrumentation.max_capture_depth,
131
- attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count)
134
+ attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count,
135
+ length: nil,
136
+ collection_size: nil)
132
137
  combined = combine_args(args, kwargs, target_self)
133
- serialize_vars(combined, depth: depth, attribute_count: attribute_count)
138
+ serialize_vars(combined, depth: depth, attribute_count: attribute_count,
139
+ length: length, collection_size: collection_size)
134
140
  end
135
141
 
136
142
  # Serializes variables captured by a line probe.
@@ -139,9 +145,12 @@ module Datadog
139
145
  # of executed code.
140
146
  def serialize_vars(vars,
141
147
  depth: settings.dynamic_instrumentation.max_capture_depth,
142
- attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count)
148
+ attribute_count: settings.dynamic_instrumentation.max_capture_attribute_count,
149
+ length: nil,
150
+ collection_size: nil)
143
151
  vars.each_with_object({}) do |(k, v), agg|
144
- agg[k] = serialize_value(v, name: k, depth: depth, attribute_count: attribute_count)
152
+ agg[k] = serialize_value(v, name: k, depth: depth, attribute_count: attribute_count,
153
+ length: length, collection_size: collection_size)
145
154
  end
146
155
  end
147
156
 
@@ -160,6 +169,8 @@ module Datadog
160
169
  def serialize_value(value, name: nil,
161
170
  depth: settings.dynamic_instrumentation.max_capture_depth,
162
171
  attribute_count: nil,
172
+ length: nil,
173
+ collection_size: nil,
163
174
  type: nil)
164
175
  attribute_count ||= settings.dynamic_instrumentation.max_capture_attribute_count
165
176
  cls = type || value.class
@@ -177,7 +188,9 @@ module Datadog
177
188
  if condition
178
189
  begin
179
190
  condition_result = condition.call(value)
180
- rescue => e
191
+ rescue Exception => e # standard:disable Lint/RescueException
192
+ raise if SERIALIZABLE_FATAL_EXCEPTION_CLASSES.any? { |klass| e.is_a?(klass) }
193
+
181
194
  # If a custom serializer condition raises an exception (e.g., regex match
182
195
  # against invalid UTF-8), skip it and continue with the next serializer.
183
196
  # We don't want custom serializer conditions to break the entire serialization.
@@ -230,7 +243,8 @@ module Datadog
230
243
  #
231
244
  # Truncate binary data BEFORE escaping to avoid cutting mid-escape-sequence.
232
245
  # For regular strings, the limit is applied to string length in characters.
233
- max = settings.dynamic_instrumentation.max_capture_string_length
246
+ length ||= settings.dynamic_instrumentation.max_capture_string_length
247
+ max = length
234
248
 
235
249
  if value.encoding == Encoding::BINARY || !value.valid_encoding?
236
250
  # Truncate binary data BEFORE escaping to avoid cutting mid-escape-sequence
@@ -258,7 +272,8 @@ module Datadog
258
272
  if depth <= 0
259
273
  serialized.update(notCapturedReason: "depth")
260
274
  else
261
- max = settings.dynamic_instrumentation.max_capture_collection_size
275
+ collection_size ||= settings.dynamic_instrumentation.max_capture_collection_size
276
+ max = collection_size
262
277
  if max != 0 && value.length > max
263
278
  serialized.update(notCapturedReason: "collectionSize", size: value.length)
264
279
  # same steep failure with array slices.
@@ -266,7 +281,7 @@ module Datadog
266
281
  value = value[0...max] || []
267
282
  end
268
283
  entries = value.map do |elt|
269
- serialize_value(elt, depth: depth - 1)
284
+ serialize_value(elt, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count)
270
285
  end
271
286
  serialized.update(elements: entries)
272
287
  end
@@ -274,7 +289,8 @@ module Datadog
274
289
  if depth <= 0
275
290
  serialized.update(notCapturedReason: "depth")
276
291
  else
277
- max = settings.dynamic_instrumentation.max_capture_collection_size
292
+ collection_size ||= settings.dynamic_instrumentation.max_capture_collection_size
293
+ max = collection_size
278
294
  cur = 0
279
295
  entries = []
280
296
  value.each do |k, v|
@@ -283,7 +299,8 @@ module Datadog
283
299
  break
284
300
  end
285
301
  cur += 1
286
- entries << [serialize_value(k, depth: depth - 1), serialize_value(v, name: k, depth: depth - 1)]
302
+ entries << [serialize_value(k, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count),
303
+ serialize_value(v, name: k, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count)]
287
304
  end
288
305
  serialized.update(entries: entries)
289
306
  end
@@ -322,7 +339,7 @@ module Datadog
322
339
  break
323
340
  end
324
341
  cur += 1
325
- fields[ivar] = serialize_value(value.instance_variable_get(ivar), name: ivar, depth: depth - 1)
342
+ fields[ivar] = serialize_value(value.instance_variable_get(ivar), name: ivar, depth: depth - 1, length: length, collection_size: collection_size, attribute_count: attribute_count)
326
343
  end
327
344
  serialized.update(fields: fields)
328
345
  end
@@ -331,7 +348,7 @@ module Datadog
331
348
  rescue Exception => exc # standard:disable Lint/RescueException
332
349
  # Re-raise fatal exceptions that should not be caught
333
350
  # (signals, interrupts, system exit)
334
- raise if FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
351
+ raise if SERIALIZABLE_FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
335
352
 
336
353
  # Catch all other exceptions including SystemStackError and NoMemoryError.
337
354
  # These inherit from Exception (not StandardError) but can occur during
@@ -429,7 +446,9 @@ module Datadog
429
446
  end
430
447
  "#<#{class_name(value.class)}#{serialized}>"
431
448
  end
432
- rescue => exc
449
+ rescue Exception => exc # standard:disable Lint/RescueException
450
+ raise if SERIALIZABLE_FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
451
+
433
452
  telemetry&.report(exc, description: "Error serializing for message")
434
453
  # TODO class_name(foo) can also fail, which we don't handle here.
435
454
  # Telemetry reporting could potentially also fail?
@@ -7,6 +7,7 @@ require_relative '../../core/transport/parcel'
7
7
  require_relative '../../core/transport/request'
8
8
  require_relative '../../core/transport/transport'
9
9
  require_relative '../error'
10
+ require_relative '../fatal_exceptions'
10
11
  require_relative 'http/input'
11
12
 
12
13
  module Datadog
@@ -77,7 +78,8 @@ module Datadog
77
78
  next
78
79
  end
79
80
  encoded_snapshots << encoded
80
- rescue => exc
81
+ rescue Exception => exc # standard:disable Lint/RescueException
82
+ Datadog::DI.reraise_if_fatal(exc)
81
83
  # Serialization failed for this snapshot - report via callback
82
84
  # This catches JSON::GeneratorError, Encoding errors, TypeError, etc.
83
85
  probe_id = snapshot.dig(:debugger, :snapshot, :probe, :id)
@@ -87,7 +89,8 @@ module Datadog
87
89
  if probe_id
88
90
  begin
89
91
  on_serialization_error.call(probe_id, exc)
90
- rescue => callback_exc
92
+ rescue Exception => callback_exc # standard:disable Lint/RescueException
93
+ Datadog::DI.reraise_if_fatal(callback_exc)
91
94
  logger.debug { "di: error in serialization error callback for probe #{probe_id}: #{callback_exc.class}: #{callback_exc.message}" }
92
95
  telemetry&.report(callback_exc, description: "Error in serialization error callback")
93
96
  end
@@ -109,8 +112,9 @@ module Datadog
109
112
  # subsequent chunks are attempted to be sent.
110
113
  begin
111
114
  send_input_chunk(chunked_payload, serialized_tags)
112
- rescue => exc
113
- logger.debug { "di: failed to send snapshot chunk: #{exc.class}: #{exc.message} (at #{exc.backtrace.first})" }
115
+ rescue Exception => exc # standard:disable Lint/RescueException
116
+ Datadog::DI.reraise_if_fatal(exc)
117
+ logger.debug { "di: failed to send snapshot chunk: #{exc.class}: #{exc.message} (at #{exc.backtrace&.first})" }
114
118
  telemetry&.report(exc, description: "Error sending snapshot chunk")
115
119
  end
116
120
  end
data/lib/datadog/di.rb CHANGED
@@ -71,6 +71,69 @@ module Datadog
71
71
  Datadog.configuration.dynamic_instrumentation.enabled
72
72
  end
73
73
 
74
+ # Returns a human-readable reason why dynamic instrumentation cannot run
75
+ # under the given settings, or nil if all build-time preconditions are met.
76
+ #
77
+ # Single source of truth for the preconditions checked in
78
+ # {Component.build} and reported back by {Remote.handle_rc_enablement}
79
+ # when an implicit enablement signal arrives but the component was not
80
+ # built at startup. Checks are ordered from most-actionable to
81
+ # platform-constraint so the most useful reason wins.
82
+ #
83
+ # The settings argument is optional so the helper can be called from
84
+ # contexts that don't have settings in scope (e.g. the RC handler).
85
+ #
86
+ # @param settings [Datadog::Core::Configuration::Settings]
87
+ # @return [String, nil] reason string or nil when supported
88
+ def unsupported_reason(settings = Datadog.configuration)
89
+ # Symmetric to the respond_to?(:remote) guard below: in unusual
90
+ # configurations (test doubles, partial Settings) the DI namespace
91
+ # may be absent. Returning a reason here lets callers — most
92
+ # importantly Remote.handle_rc_enablement — emit the customer-facing
93
+ # warn instead of raising NoMethodError on the unguarded access at
94
+ # line 92 (`settings.dynamic_instrumentation.internal.development`).
95
+ unless settings.respond_to?(:dynamic_instrumentation)
96
+ return "dynamic instrumentation settings are not available"
97
+ end
98
+ unless settings.respond_to?(:remote) && settings.remote.enabled
99
+ return "Remote Configuration is not enabled. See https://docs.datadoghq.com/agent/remote_config"
100
+ end
101
+ unless settings.dynamic_instrumentation.internal.development
102
+ if Datadog::Core::Environment::Execution.development?
103
+ return "development environment detected"
104
+ end
105
+ end
106
+ if (reason = unsupported_platform_reason)
107
+ return reason
108
+ end
109
+ unless respond_to?(:exception_message)
110
+ return "C extension is not available"
111
+ end
112
+ nil
113
+ end
114
+
115
+ # Whether the current Ruby runtime can run dynamic instrumentation:
116
+ # MRI (CRuby) on Ruby 2.6 or later.
117
+ #
118
+ # @return [Boolean]
119
+ def supported_runtime?
120
+ unsupported_platform_reason.nil?
121
+ end
122
+
123
+ # Reason the current Ruby runtime cannot run dynamic instrumentation, or
124
+ # nil when the platform is supported. Single source of truth for the
125
+ # minimum-runtime definition, shared by {unsupported_reason} (which layers
126
+ # the settings and C-extension checks on top) and {supported_runtime?}.
127
+ #
128
+ # @return [String, nil]
129
+ private def unsupported_platform_reason
130
+ if RUBY_ENGINE != 'ruby'
131
+ "MRI is required, but running on #{RUBY_ENGINE}"
132
+ elsif Datadog::RubyVersion.is?('< 2.6')
133
+ "Ruby 2.6+ is required, but running on #{RUBY_VERSION}"
134
+ end
135
+ end
136
+
74
137
  # Returns iseqs that correspond to loaded files (filtering out eval'd code).
75
138
  #
76
139
  # There are several types of iseqs returned by +all_iseqs+:
@@ -9,14 +9,17 @@ module Datadog
9
9
  module Events
10
10
  # The second version of Business Logic Events SDK
11
11
  module V2
12
+ SIGNUP_EVENT = 'users.signup'
12
13
  LOGIN_SUCCESS_EVENT = 'users.login.success'
13
14
  LOGIN_FAILURE_EVENT = 'users.login.failure'
15
+
14
16
  TELEMETRY_METRICS_NAMESPACE = 'appsec'
15
17
  TELEMETRY_METRICS_SDK_EVENT = 'sdk.event'
16
18
  TELEMETRY_METRICS_SDK_VERSION = 'v2'
17
19
  TELEMETRY_METRICS_EVENTS_INTO_TYPES = {
18
20
  LOGIN_SUCCESS_EVENT => 'login_success',
19
- LOGIN_FAILURE_EVENT => 'login_failure'
21
+ LOGIN_FAILURE_EVENT => 'login_failure',
22
+ SIGNUP_EVENT => 'signup'
20
23
  }.freeze
21
24
 
22
25
  class << self
@@ -125,6 +128,61 @@ module Datadog
125
128
  ::Datadog::AppSec::Instrumentation.gateway.push('identity.set_user', user)
126
129
  end
127
130
 
131
+ # Attach user signup information to the service entry span
132
+ # and trigger AppSec event processing.
133
+ #
134
+ # @param login [String] The user login (e.g., username or email).
135
+ # @param user_or_id [String, Hash<Symbol, String>] (optional) If a
136
+ # String, considered as a user ID, if a Hash, considered as a user
137
+ # attributes. The Hash must include `:id` as a key.
138
+ # @param metadata [Hash<Symbol, String>] Additional flat free-form
139
+ # metadata to attach to the event.
140
+ #
141
+ # @example Login only
142
+ # Datadog::Kit::AppSec::Events::V2.track_user_signup('alice@example.com')
143
+ #
144
+ # @example Login, user attributes, and metadata
145
+ # Datadog::Kit::AppSec::Events::V2.track_user_signup(
146
+ # 'alice@example.com',
147
+ # { id: 'user-123', email: 'alice@example.com', name: 'Alice' },
148
+ # ip: '192.168.1.1', device: 'mobile', 'usr.country': 'US'
149
+ # )
150
+ #
151
+ # @return [void]
152
+ def track_user_signup(login, user_or_id = nil, metadata = {})
153
+ trace = service_entry_trace
154
+ span = service_entry_span
155
+
156
+ if trace.nil? || span.nil?
157
+ return Datadog.logger.warn(
158
+ 'Kit::AppSec: Tracing is not enabled. Please enable tracing if you want to track events'
159
+ )
160
+ end
161
+
162
+ raise TypeError, '`login` argument must be a String' unless login.is_a?(String)
163
+ raise TypeError, '`metadata` argument must be a Hash' unless metadata.is_a?(Hash)
164
+
165
+ user_attributes = build_user_attributes(user_or_id, login)
166
+
167
+ set_span_tags(span, metadata, namespace: SIGNUP_EVENT)
168
+ set_span_tags(span, user_attributes, namespace: "#{SIGNUP_EVENT}.usr")
169
+ span.set_tag('appsec.events.users.signup.track', 'true')
170
+ span.set_tag('_dd.appsec.events.users.signup.sdk', 'true')
171
+
172
+ ::Datadog::AppSec::TraceKeeper.keep!(trace)
173
+
174
+ record_event_telemetry_metric(SIGNUP_EVENT)
175
+
176
+ if user_attributes.key?(:id)
177
+ Kit::Identity.set_user(trace, span, **user_attributes)
178
+ else
179
+ # NOTE: {Datadog::Kit::Identity.set_user} requires an ID,
180
+ # but WAF can evaluate login alone
181
+ user = ::Datadog::AppSec::Instrumentation::Gateway::User.new(nil, login)
182
+ ::Datadog::AppSec::Instrumentation.gateway.push('identity.set_user', user)
183
+ end
184
+ end
185
+
128
186
  private
129
187
 
130
188
  # NOTE: Current tracer implementation does not provide a way to
@@ -155,7 +213,7 @@ module Datadog
155
213
  raise ArgumentError, 'missing required user key `:id`' unless user_or_id.key?(:id)
156
214
  raise TypeError, 'user key `:id` must be a String' unless user_or_id[:id].is_a?(String)
157
215
 
158
- user_or_id.merge(login: login) #: {login: ::String, ?id: ::String?}
216
+ user_or_id.merge(login: login)
159
217
  else
160
218
  raise TypeError, '`user_or_id` argument must be either String or Hash'
161
219
  end
@@ -12,7 +12,7 @@ module Datadog
12
12
  module OpenFeature
13
13
  # This class is the entry point for the OpenFeature component
14
14
  class Component
15
- attr_reader :engine, :flag_eval_hook
15
+ attr_reader :engine, :flag_eval_metrics_hook, :flag_eval_evp_hook
16
16
 
17
17
  def self.build(settings, agent_settings, logger:, telemetry:)
18
18
  return unless settings.respond_to?(:open_feature) && settings.open_feature.enabled
@@ -54,21 +54,44 @@ module Datadog
54
54
 
55
55
  @telemetry = telemetry
56
56
  @logger = logger
57
- @flag_eval_hook = create_flag_eval_hook
57
+ @settings = settings
58
+ @agent_settings = agent_settings
59
+ @flag_eval_metrics_hook = create_flag_eval_metrics_hook
60
+ @flag_eval_evp_hook = create_flag_eval_evp_hook
58
61
  end
59
62
 
60
63
  def shutdown!
61
64
  @worker.graceful_shutdown
65
+ @flag_eval_evp_writer&.stop
62
66
  end
63
67
 
64
68
  private
65
69
 
66
- def create_flag_eval_hook
67
- require_relative 'hooks/flag_eval_hook'
68
- return unless Hooks::FlagEvalHook.available?
70
+ def create_flag_eval_metrics_hook
71
+ require_relative 'hooks/flag_eval_metrics_hook'
72
+ return unless Hooks::FlagEvalMetricsHook.available?
69
73
 
70
74
  metrics = Metrics::FlagEvalMetrics.new(telemetry: @telemetry, logger: @logger)
71
- Hooks::FlagEvalHook.new(metrics)
75
+ Hooks::FlagEvalMetricsHook.new(metrics)
76
+ rescue LoadError
77
+ nil
78
+ end
79
+
80
+ # Killswitch: DD_FLAGGING_EVALUATION_COUNTS_ENABLED (default on) gates only the EVP path.
81
+ # Read through the datadog config registry, not raw ENV.
82
+ def create_flag_eval_evp_hook
83
+ return unless @settings.open_feature.evaluation_counts_enabled
84
+
85
+ require_relative 'hooks/flag_eval_evp_hook'
86
+ return unless Hooks::FlagEvalEVPHook.available?
87
+
88
+ evp_transport = Transport::HTTP.build_flagevaluations(
89
+ agent_settings: @agent_settings,
90
+ logger: @logger,
91
+ )
92
+ require_relative 'flag_evaluation/writer'
93
+ @flag_eval_evp_writer = FlagEvaluation::Writer.new(transport: evp_transport, logger: @logger, telemetry: @telemetry)
94
+ Hooks::FlagEvalEVPHook.new(@flag_eval_evp_writer)
72
95
  rescue LoadError
73
96
  nil
74
97
  end
@@ -20,6 +20,14 @@ module Datadog
20
20
  o.env 'DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED'
21
21
  o.default false
22
22
  end
23
+
24
+ # Killswitch for the EVP `flagevaluation` emission path only. Default on; when
25
+ # disabled the existing OTel `feature_flag.evaluations` metric is unaffected.
26
+ option :evaluation_counts_enabled do |o|
27
+ o.type :bool
28
+ o.env 'DD_FLAGGING_EVALUATION_COUNTS_ENABLED'
29
+ o.default true
30
+ end
23
31
  end
24
32
  end
25
33
  end
@@ -4,8 +4,10 @@ module Datadog
4
4
  module OpenFeature
5
5
  module Ext
6
6
  ERROR = 'ERROR'
7
+ DEFAULT = 'DEFAULT'
7
8
  INITIALIZING = 'INITIALIZING'
8
9
  UNKNOWN_TYPE = 'UNKNOWN_TYPE'
10
+ PARSE_ERROR = 'PARSE_ERROR'
9
11
  GENERAL = 'GENERAL'
10
12
  PROVIDER_FATAL = 'PROVIDER_FATAL'
11
13
  PROVIDER_NOT_READY = 'PROVIDER_NOT_READY'