datadog 2.36.0 → 2.37.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -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 +6 -3
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +17 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +219 -98
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +2 -0
  8. data/ext/datadog_profiling_native_extension/extconf.rb +11 -0
  9. data/ext/datadog_profiling_native_extension/ruby_helpers.c +7 -0
  10. data/ext/libdatadog_api/di.c +76 -0
  11. data/ext/libdatadog_api/extconf.rb +6 -0
  12. data/ext/libdatadog_extconf_helpers.rb +1 -1
  13. data/lib/datadog/ai_guard/api_client.rb +6 -4
  14. data/lib/datadog/appsec/api_security/route_extractor.rb +7 -4
  15. data/lib/datadog/appsec/configuration.rb +6 -0
  16. data/lib/datadog/appsec/context.rb +4 -2
  17. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
  18. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
  19. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +1 -1
  20. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -0
  21. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +4 -17
  22. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
  23. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +2 -1
  24. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
  25. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +4 -0
  26. data/lib/datadog/appsec/ext.rb +1 -0
  27. data/lib/datadog/appsec/metrics/collector.rb +18 -1
  28. data/lib/datadog/appsec/metrics/exporter.rb +9 -3
  29. data/lib/datadog/appsec/remote.rb +9 -3
  30. data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
  31. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
  32. data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
  33. data/lib/datadog/appsec/route_normalizer.rb +80 -0
  34. data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
  35. data/lib/datadog/core/configuration/components.rb +82 -2
  36. data/lib/datadog/core/configuration/components_state.rb +6 -1
  37. data/lib/datadog/core/configuration/settings.rb +0 -3
  38. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  39. data/lib/datadog/core/evp.rb +11 -0
  40. data/lib/datadog/core/remote/client/capabilities.rb +34 -7
  41. data/lib/datadog/core/telemetry/event/app_started.rb +8 -1
  42. data/lib/datadog/di/base.rb +3 -1
  43. data/lib/datadog/di/code_tracker.rb +5 -2
  44. data/lib/datadog/di/component.rb +110 -36
  45. data/lib/datadog/di/error.rb +10 -0
  46. data/lib/datadog/di/fatal_exceptions.rb +26 -0
  47. data/lib/datadog/di/instrumenter.rb +381 -165
  48. data/lib/datadog/di/probe.rb +14 -0
  49. data/lib/datadog/di/probe_file_loader.rb +8 -3
  50. data/lib/datadog/di/probe_manager.rb +67 -9
  51. data/lib/datadog/di/probe_notification_builder.rb +4 -1
  52. data/lib/datadog/di/probe_notifier_worker.rb +52 -32
  53. data/lib/datadog/di/redactor.rb +16 -1
  54. data/lib/datadog/di/remote.rb +175 -14
  55. data/lib/datadog/di/serializer.rb +12 -5
  56. data/lib/datadog/di/transport/input.rb +8 -4
  57. data/lib/datadog/di.rb +63 -0
  58. data/lib/datadog/kit/appsec/events/v2.rb +60 -2
  59. data/lib/datadog/open_feature/component.rb +29 -6
  60. data/lib/datadog/open_feature/configuration.rb +8 -0
  61. data/lib/datadog/open_feature/ext.rb +2 -0
  62. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
  63. data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
  64. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
  65. data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
  66. data/lib/datadog/open_feature/native_evaluator.rb +22 -0
  67. data/lib/datadog/open_feature/provider.rb +59 -27
  68. data/lib/datadog/open_feature/transport.rb +55 -1
  69. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
  70. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
  71. data/lib/datadog/opentelemetry/sdk.rb +19 -0
  72. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  73. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
  74. data/lib/datadog/symbol_database/component.rb +164 -47
  75. data/lib/datadog/symbol_database/configuration.rb +12 -4
  76. data/lib/datadog/symbol_database/extensions.rb +19 -0
  77. data/lib/datadog/symbol_database/extractor.rb +124 -87
  78. data/lib/datadog/symbol_database/file_hash.rb +3 -1
  79. data/lib/datadog/symbol_database/remote.rb +8 -3
  80. data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
  81. data/lib/datadog/symbol_database/uploader.rb +9 -2
  82. data/lib/datadog/symbol_database.rb +22 -0
  83. data/lib/datadog/tracing/distributed/baggage.rb +2 -1
  84. data/lib/datadog/tracing/ext.rb +9 -0
  85. data/lib/datadog/tracing/remote.rb +34 -3
  86. data/lib/datadog/version.rb +1 -1
  87. data/lib/datadog.rb +1 -0
  88. metadata +19 -6
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'json'
4
4
 
5
+ require_relative 'fatal_exceptions'
6
+
5
7
  module Datadog
6
8
  module DI
7
9
  module ProbeFileLoader
@@ -47,7 +49,8 @@ module Datadog
47
49
 
48
50
  payload = component.probe_notification_builder.build_errored(probe, exc)
49
51
  component.probe_notifier_worker.add_status(payload, probe: probe)
50
- rescue => exc
52
+ rescue Exception => exc # standard:disable Lint/RescueException
53
+ Datadog::DI.reraise_if_fatal(exc)
51
54
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
52
55
 
53
56
  component.logger.debug { "di: unhandled exception adding #{probe.type} probe at #{probe.location} (#{probe.id}) in DI probe file loader: #{exc.class}: #{exc.message}" }
@@ -58,7 +61,8 @@ module Datadog
58
61
  component.probe_notifier_worker.add_status(payload, probe: probe)
59
62
  end
60
63
  end
61
- rescue => exc
64
+ rescue Exception => exc # standard:disable Lint/RescueException
65
+ Datadog::DI.reraise_if_fatal(exc)
62
66
  if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
63
67
  should_propagate = true
64
68
  raise
@@ -67,7 +71,8 @@ module Datadog
67
71
  component.logger.debug { "di: unhandled exception handling a probe in DI probe file loader: #{exc.class}: #{exc.message}" }
68
72
  component.telemetry&.report(exc, description: "Unhandled exception handling probe in DI probe file loader")
69
73
  end
70
- rescue
74
+ rescue Exception => exc # standard:disable Lint/RescueException
75
+ Datadog::DI.reraise_if_fatal(exc)
71
76
  # We should generally never get here, but if component tree
72
77
  # initialization fails for some unexpected reason, don't nuke
73
78
  # the customer application.
@@ -2,6 +2,8 @@
2
2
 
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
+ require_relative 'fatal_exceptions'
6
+
5
7
  module Datadog
6
8
  module DI
7
9
  # Orchestrates probe lifecycle: installation, removal, and execution callbacks.
@@ -26,13 +28,13 @@ module Datadog
26
28
  @telemetry = telemetry
27
29
  @probe_repository = probe_repository
28
30
 
29
- @definition_trace_point = TracePoint.trace(:end) do |tp|
31
+ @definition_trace_point = TracePoint.new(:end) do |tp|
30
32
  install_pending_method_probes(tp.self)
31
- rescue => exc
33
+ rescue Exception => exc # standard:disable Lint/RescueException
34
+ Datadog::DI.reraise_if_fatal(exc)
32
35
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
33
36
  logger.debug { "di: unhandled exception in definition trace point: #{exc.class}: #{exc.message}" }
34
37
  telemetry&.report(exc, description: "Unhandled exception in definition trace point")
35
- # TODO test this path
36
38
  end
37
39
  end
38
40
 
@@ -40,15 +42,68 @@ module Datadog
40
42
  attr_reader :telemetry
41
43
  attr_reader :probe_repository
42
44
 
45
+ # Stops the probe manager without permanently releasing resources.
46
+ #
47
+ # Disables the class definition trace point and unhooks all installed
48
+ # probe instrumentation, but preserves the probe repository so that
49
+ # {#reopen} can re-install probes. Can be reversed by calling {#reopen}.
50
+ #
51
+ # Why preserve the repository: RC dispatch only delivers LIVE_DEBUGGING
52
+ # changes when the content hash changes. If a customer toggles DI off
53
+ # then back on in the UI with the same probe content, the receiver
54
+ # is not re-invoked, so probes wiped here would never be redelivered.
55
+ # Preserving the repo lets {#reopen} replay the probes locally.
56
+ #
57
+ # @return [void]
58
+ def stop
59
+ definition_trace_point.disable
60
+ probe_repository.synchronize do
61
+ probe_repository.installed_probes.each_value do |probe|
62
+ instrumenter.unhook(probe)
63
+ rescue => exc
64
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
65
+ logger.debug { "di: error unhooking #{probe.type} probe at #{probe.location} (#{probe.id}) on stop: #{exc.class}: #{exc.message}" }
66
+ telemetry&.report(exc, description: "Error unhooking probe on stop")
67
+ end
68
+ end
69
+ end
70
+
71
+ # Re-enables the probe manager after a {#stop}.
72
+ #
73
+ # Re-enables the class definition trace point so that pending
74
+ # method probes can be installed when their target classes are defined,
75
+ # and re-hooks all previously-installed probes from the repository.
76
+ # If a probe's target is no longer present (e.g. code was unloaded
77
+ # between stop and reopen), it is demoted to pending so the trace
78
+ # point or code tracker can re-install it if the target reappears.
79
+ #
80
+ # @return [void]
81
+ def reopen
82
+ definition_trace_point.enable
83
+ probe_repository.synchronize do
84
+ # values.each so concurrent mutation of @installed_probes (via
85
+ # remove_installed below) does not iterate over a stale key set.
86
+ probe_repository.installed_probes.values.each do |probe|
87
+ instrumenter.hook(probe, self)
88
+ rescue Error::DITargetNotDefined
89
+ probe_repository.remove_installed(probe.id)
90
+ probe_repository.add_pending(probe)
91
+ logger.trace { "di: target for #{probe.type} probe (#{probe.id}) no longer defined on reopen, demoting to pending" }
92
+ rescue => exc
93
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
94
+ logger.debug { "di: error re-hooking #{probe.type} probe at #{probe.location} (#{probe.id}) on reopen: #{exc.class}: #{exc.message}" }
95
+ telemetry&.report(exc, description: "Error re-hooking probe on reopen")
96
+ end
97
+ end
98
+ end
99
+
43
100
  # Shuts down the probe manager and releases all resources.
44
101
  #
45
102
  # Disables the class definition trace point and removes all installed
46
103
  # probe instrumentation. Called during component teardown.
104
+ # Unlike {#stop}, this is not reversible.
47
105
  #
48
106
  # @return [void]
49
- #
50
- # TODO test that close is called during component teardown and
51
- # the trace point is cleared
52
107
  def close
53
108
  definition_trace_point.disable
54
109
  clear_hooks
@@ -129,7 +184,8 @@ module Datadog
129
184
  false
130
185
  end
131
186
  end
132
- rescue => exc
187
+ rescue Exception => exc # standard:disable Lint/RescueException
188
+ Datadog::DI.reraise_if_fatal(exc)
133
189
  # In "propagate all exceptions" mode we will try to instrument again.
134
190
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
135
191
 
@@ -160,7 +216,8 @@ module Datadog
160
216
  begin
161
217
  instrumenter.unhook(probe)
162
218
  probe_repository.remove_installed(probe_id)
163
- rescue => exc
219
+ rescue Exception => exc # standard:disable Lint/RescueException
220
+ Datadog::DI.reraise_if_fatal(exc)
164
221
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
165
222
  # Silence all exceptions?
166
223
  # TODO should we propagate here and rescue upstream?
@@ -190,7 +247,8 @@ module Datadog
190
247
  break
191
248
  rescue Error::DITargetNotDefined
192
249
  # This should not happen... try installing again later?
193
- rescue => exc
250
+ rescue Exception => exc # standard:disable Lint/RescueException
251
+ Datadog::DI.reraise_if_fatal(exc)
194
252
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
195
253
 
196
254
  logger.debug { "di: error installing #{probe.type} probe at #{probe.location} (#{probe.id}) after class is defined: #{exc.class}: #{exc.message}" }
@@ -2,6 +2,8 @@
2
2
 
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
+ require_relative 'fatal_exceptions'
6
+
5
7
  module Datadog
6
8
  module DI
7
9
  # Builds probe status notification and snapshot payloads.
@@ -366,7 +368,8 @@ module Datadog
366
368
  else
367
369
  raise ArgumentError, "Invalid template segment type: #{segment}"
368
370
  end
369
- rescue => exc
371
+ rescue Exception => exc # standard:disable Lint/RescueException
372
+ Datadog::DI.reraise_if_fatal(exc)
370
373
  evaluation_errors << {
371
374
  message: "#{exc.class}: #{exc.message}",
372
375
  expr: segment.dsl_expr, # steep:ignore NoMethod
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../core/semaphore'
4
+ require_relative 'fatal_exceptions'
4
5
 
5
6
  module Datadog
6
7
  module DI
@@ -62,6 +63,7 @@ module Datadog
62
63
  # @return [void]
63
64
  def start
64
65
  return if @thread && @pid == Process.pid
66
+ @stop_requested = false
65
67
  logger.trace { "di: starting probe notifier: pid #{$$}" }
66
68
  @thread = Thread.new do
67
69
  loop do
@@ -92,10 +94,11 @@ module Datadog
92
94
 
93
95
  begin
94
96
  more = maybe_send
95
- rescue => exc
97
+ rescue Exception => exc # standard:disable Lint/RescueException
98
+ Datadog::DI.reraise_if_fatal(exc)
96
99
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
97
100
 
98
- logger.debug { "di: error in probe notifier worker: #{exc.class}: #{exc.message} (at #{exc.backtrace.first})" }
101
+ logger.debug { "di: error in probe notifier worker: #{exc.class}: #{exc.message} (at #{exc.backtrace&.first})" }
99
102
  telemetry&.report(exc, description: "Error in probe notifier worker")
100
103
  end
101
104
  @lock.synchronize do
@@ -257,40 +260,56 @@ module Datadog
257
260
  # if it has been more than 1 second since the last send of the same
258
261
  # event type.
259
262
  define_method("add_#{event_type}") do |event, probe: nil|
260
- @lock.synchronize do
261
- queue = send("#{event_type}_queue")
262
- if queue.length > settings.dynamic_instrumentation.internal.snapshot_queue_capacity
263
- if event_type == :status && probe
264
- status = event.dig(:debugger, :diagnostics, :status)
265
- logger.debug { "di: dropping status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status} because queue is full" }
266
- else
267
- logger.debug { "di: #{self.class.name}: dropping #{event_type} event because queue is full" }
268
- end
269
- else
270
- if event_type == :status && probe
271
- status = event.dig(:debugger, :diagnostics, :status)
272
- logger.trace { "di: queueing status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status}" }
263
+ # Enqueueing reads DI configuration and writes debug/trace logs, both
264
+ # of which can call stdlib methods (e.g. Set#include? via the
265
+ # configuration layer's supported-configuration lookup). If a customer
266
+ # has a method probe on such a method, firing it here would re-enter
267
+ # the worker through probe_executed_callback and deadlock on @lock
268
+ # (recursive locking). Hold the re-entrancy guard for the duration so
269
+ # those DI-internal calls reach the original method without firing the
270
+ # probe. Restore the prior state afterwards so a caller that already
271
+ # holds the guard (e.g. run_method_probe post-processing calling
272
+ # add_snapshot) is not cleared early.
273
+ was_in_probe = Datadog::DI.in_probe?
274
+ Datadog::DI.enter_probe unless was_in_probe
275
+ begin
276
+ @lock.synchronize do
277
+ queue = send("#{event_type}_queue")
278
+ if queue.length > settings.dynamic_instrumentation.internal.snapshot_queue_capacity
279
+ if event_type == :status && probe
280
+ status = event.dig(:debugger, :diagnostics, :status)
281
+ logger.debug { "di: dropping status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status} because queue is full" }
282
+ else
283
+ logger.debug { "di: #{self.class.name}: dropping #{event_type} event because queue is full" }
284
+ end
273
285
  else
274
- logger.trace { "di: #{self.class.name}: queueing #{event_type} event" }
286
+ if event_type == :status && probe
287
+ status = event.dig(:debugger, :diagnostics, :status)
288
+ logger.trace { "di: queueing status for #{probe.type} probe at #{probe.location} (#{probe.id}): #{status}" }
289
+ else
290
+ logger.trace { "di: #{self.class.name}: queueing #{event_type} event" }
291
+ end
292
+ queue << event
275
293
  end
276
- queue << event
277
294
  end
278
- end
279
295
 
280
- # Figure out whether to wake up the worker thread.
281
- # If minimum send interval has elapsed since the last send,
282
- # wake up immediately.
283
- @lock.synchronize do
284
- unless @wake_scheduled
285
- @wake_scheduled = true
286
- set_sleep_remaining
287
- wake.signal
296
+ # Figure out whether to wake up the worker thread.
297
+ # If minimum send interval has elapsed since the last send,
298
+ # wake up immediately.
299
+ @lock.synchronize do
300
+ unless @wake_scheduled
301
+ @wake_scheduled = true
302
+ set_sleep_remaining
303
+ wake.signal
304
+ end
288
305
  end
289
- end
290
306
 
291
- # Worker could be not running if the process forked - check and
292
- # start it again in this case.
293
- start
307
+ # Worker could be not running if the process forked - check and
308
+ # start it again in this case.
309
+ start
310
+ ensure
311
+ Datadog::DI.leave_probe unless was_in_probe
312
+ end
294
313
  end
295
314
 
296
315
  public "add_#{event_type}"
@@ -318,9 +337,10 @@ module Datadog
318
337
  @lock.synchronize do
319
338
  @last_sent = time
320
339
  end
321
- rescue => exc
340
+ rescue Exception => exc # standard:disable Lint/RescueException
341
+ Datadog::DI.reraise_if_fatal(exc)
322
342
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
323
- logger.debug { "di: failed to send #{event_name}: #{exc.class}: #{exc.message} (at #{exc.backtrace.first})" }
343
+ logger.debug { "di: failed to send #{event_name}: #{exc.class}: #{exc.message} (at #{exc.backtrace&.first})" }
324
344
  telemetry&.report(exc, description: "Error sending #{event_type}")
325
345
  end
326
346
  end
@@ -186,8 +186,23 @@ module Datadog
186
186
  ]
187
187
 
188
188
  # Input can be a string or a symbol.
189
+ #
190
+ # Hash keys captured in snapshots often originate from a Rack environment
191
+ # (Rack/Rails/Sinatra middleware and controllers all keep `env` in scope).
192
+ # Rack rewrites incoming HTTP header names to the CGI form
193
+ # `HTTP_<HEADER>` with dashes converted to underscores — for example,
194
+ # `Authorization` becomes `HTTP_AUTHORIZATION`, `Cookie` becomes
195
+ # `HTTP_COOKIE`, `X-API-Key` becomes `HTTP_X_API_KEY`. Without stripping
196
+ # the prefix here, those keys normalize to `httpauthorization`,
197
+ # `httpcookie`, `httpxapikey`, etc., none of which appear in the default
198
+ # identifier list — so the values would not be matched against entries
199
+ # like `authorization`, `cookie`, or `xapikey`. The `\Ahttp_` anchor
200
+ # limits the strip to the literal CGI form, so identifiers that merely
201
+ # begin with the letters `http` (e.g. `httpinfo`) are unaffected. The
202
+ # strip must run before the punctuation gsub: the gsub removes the
203
+ # underscore, after which `\Ahttp_` could never match.
189
204
  def normalize(str)
190
- str.to_s.strip.downcase.gsub(/[-_$@]/, "")
205
+ str.to_s.strip.downcase.sub(/\Ahttp_/, "").tr("-_$@", "")
191
206
  end
192
207
  end
193
208
  end
@@ -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}" }
@@ -114,7 +229,8 @@ module Datadog
114
229
  # try to remove instrumentation that is still supposed to be
115
230
  # active.
116
231
  #current_probe_ids[probe_spec.fetch('id')] = true
117
- rescue => exc
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
  #
@@ -177,7 +180,9 @@ module Datadog
177
180
  if condition
178
181
  begin
179
182
  condition_result = condition.call(value)
180
- rescue => e
183
+ rescue Exception => e # standard:disable Lint/RescueException
184
+ raise if SERIALIZABLE_FATAL_EXCEPTION_CLASSES.any? { |klass| e.is_a?(klass) }
185
+
181
186
  # If a custom serializer condition raises an exception (e.g., regex match
182
187
  # against invalid UTF-8), skip it and continue with the next serializer.
183
188
  # We don't want custom serializer conditions to break the entire serialization.
@@ -331,7 +336,7 @@ module Datadog
331
336
  rescue Exception => exc # standard:disable Lint/RescueException
332
337
  # Re-raise fatal exceptions that should not be caught
333
338
  # (signals, interrupts, system exit)
334
- raise if FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
339
+ raise if SERIALIZABLE_FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
335
340
 
336
341
  # Catch all other exceptions including SystemStackError and NoMemoryError.
337
342
  # These inherit from Exception (not StandardError) but can occur during
@@ -429,7 +434,9 @@ module Datadog
429
434
  end
430
435
  "#<#{class_name(value.class)}#{serialized}>"
431
436
  end
432
- rescue => exc
437
+ rescue Exception => exc # standard:disable Lint/RescueException
438
+ raise if SERIALIZABLE_FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
439
+
433
440
  telemetry&.report(exc, description: "Error serializing for message")
434
441
  # TODO class_name(foo) can also fail, which we don't handle here.
435
442
  # 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