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
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../core/utils/time'
4
+ require_relative '../ruby_version'
5
+ require_relative 'fatal_exceptions'
4
6
 
5
7
  # rubocop:disable Lint/AssignmentInCondition
6
8
  # rubocop:disable Style/AndOr
@@ -95,6 +97,18 @@ module Datadog
95
97
  # are not reachable via prepend on the class itself. Line probes are
96
98
  # unaffected since they install via TracePoint, not method dispatch.
97
99
  def hook_method(probe, responder)
100
+ # Reject method probes targeting any class or module in the Datadog
101
+ # namespace. The tracer uses these classes internally while building
102
+ # and dispatching probe snapshots; allowing a method probe on them
103
+ # would let DI's own code paths re-enter the probe wrapper. Line
104
+ # probes are unaffected (TracePoint is self-disabling during its
105
+ # callback) and are not rejected here.
106
+ type_name = probe.type_name
107
+ if type_name && datadog_namespace_type_name?(type_name)
108
+ raise Error::ProbeTargetForbidden,
109
+ "Method probes on the Datadog namespace are not permitted: #{type_name}##{probe.method_name}"
110
+ end
111
+
98
112
  lock.synchronize do
99
113
  if probe.instrumentation_module
100
114
  # Already instrumented, warn?
@@ -103,10 +117,9 @@ module Datadog
103
117
  end
104
118
 
105
119
  cls = symbolize_class_name(probe.type_name)
106
- serializer = self.serializer
107
- method_name = probe.method_name
108
- loc = begin
109
- cls.instance_method(method_name).source_location # steep:ignore ArgumentTypeMismatch
120
+ method_name = probe.method_name!
121
+ target_method = begin
122
+ cls.instance_method(method_name)
110
123
  rescue NameError
111
124
  # The target method is not defined.
112
125
  # This could be because it will be explicitly defined later
@@ -114,175 +127,97 @@ module Datadog
114
127
  # or the method is virtual (provided by a method_missing handler).
115
128
  # In these cases we do not have a source location for the
116
129
  # target method here.
130
+ nil
117
131
  end
118
- rate_limiter = probe.rate_limiter
119
- settings = self.settings
120
- instrumenter = self
121
132
 
122
- mod = Module.new do
123
- define_method(method_name) do |*args, **kwargs, &target_block| # steep:ignore NoMethod
124
- # Steep: Unsure why it cannot detect kwargs in this block. Workaround:
125
- # @type var kwargs: ::Hash[::Symbol, untyped]
126
- di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
133
+ # Reject method probes whose target method resolves to Kernel#lambda,
134
+ # including inherited targets. Every class inherits Kernel#lambda, so a
135
+ # probe naming any type that does not override lambda (Object#lambda,
136
+ # String#lambda, a plain user class, ...) prepends the wrapper ahead of
137
+ # Kernel#lambda and reaches the wrapper's super(&block) path. On
138
+ # Ruby 3.3+ that path raises ArgumentError for every lambda { ... }
139
+ # call site passing a captured block, breaking lambda creation
140
+ # process-wide. Resolving the method owner catches the inherited case
141
+ # that a textual "Kernel" name match misses; a type that defines its
142
+ # own lambda has a different owner and is left alone. Probing lambda
143
+ # creation has no legitimate customer use case. See #5560.
144
+ if method_name == "lambda" && target_method&.owner == ::Kernel
145
+ raise Error::ProbeTargetForbidden,
146
+ "Method probes on Kernel#lambda are not permitted: #{probe.type_name}##{method_name}"
147
+ end
127
148
 
128
- if continue = probe.enabled?
129
- if condition = probe.condition
130
- begin
131
- # This context will be recreated later, unlike for line probes.
132
- #
133
- # We do not need the stack for condition evaluation, therefore
134
- # stack is not passed to Context here.
135
- context = Context.new(
136
- locals: serializer.combine_args(args, kwargs, self),
137
- target_self: self,
138
- probe: probe, settings: settings, serializer: serializer,
139
- )
140
- continue = condition.satisfied?(context)
141
- rescue => exc
142
- # Evaluation error exception can be raised for "expected"
143
- # errors, we probably need another setting to control whether
144
- # these exceptions are propagated.
145
- raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions &&
146
- !exc.is_a?(DI::Error::ExpressionEvaluationError)
147
-
148
- if context
149
- # We want to report evaluation errors for conditions
150
- # as probe snapshots. However, if we failed to create
151
- # the context, we won't be able to report anything as
152
- # the probe notifier builder requires a context.
153
- begin
154
- responder.probe_condition_evaluation_failed_callback(context, exc)
155
- rescue => nested_exc
156
- raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
157
-
158
- instrumenter.logger.debug { "di: error in probe condition evaluation failed callback: #{nested_exc.class}: #{nested_exc.message}" }
159
- instrumenter.telemetry&.report(nested_exc, description: "Error in probe condition evaluation failed callback")
160
- end
161
- else
162
- _ = 42 # stop standard from wrecking this code
163
-
164
- raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
165
-
166
- instrumenter.logger.debug { "di: error evaluating condition without context (tracer bug?): #{exc.class}: #{exc.message}" }
167
- instrumenter.telemetry&.report(exc, description: "Error evaluating condition without context")
168
- # If execution gets here, there is probably a bug in the tracer.
169
- end
170
-
171
- continue = false
172
- end
173
- end
174
- end
149
+ loc = target_method&.source_location
150
+ instrumenter = self
175
151
 
176
- if continue and rate_limiter.nil? || rate_limiter.allow?
177
- # Arguments may be mutated by the method, therefore
178
- # they need to be serialized prior to method invocation.
179
- serialized_entry_args = if probe.capture_snapshot?
180
- serializer.serialize_args(args, kwargs, self,
181
- depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
182
- attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
152
+ mod = Module.new do
153
+ # Argument delegation follows docs/Delegation.md: on Ruby 3+
154
+ # positional and keyword arguments are captured and forwarded
155
+ # separately; on Ruby < 3 everything is captured into a single
156
+ # splat that ruby2_keywords flags, which is the only way to forward
157
+ # a trailing hash while preserving its positional-vs-keyword
158
+ # identity. The pre-3 branch is what lets an empty trailing hash
159
+ # (e.g. foo('x', {})) reach the original method intact.
160
+ #
161
+ # Re-entrancy guard: if we are already inside a DI method-probe
162
+ # callback, skip DI processing and call the original method
163
+ # directly. This prevents SystemStackError when a method probe is
164
+ # set on a stdlib method that DI itself calls during method-probe
165
+ # snapshot building (e.g., String#length, Hash#each). Line probes
166
+ # do not enter the guard — they rely on Ruby's TracePoint, which
167
+ # self-disables during its own callback. Nested invocations during
168
+ # DI processing also bypass the rate limiter: they are not
169
+ # user-observable probe firings, just internal calls that happen to
170
+ # land on a probed method. Guard storage is fiber-local;
171
+ # DI.in_probe?/enter_probe/leave_probe are implemented in C and
172
+ # access it directly, bypassing Thread#[]/Thread#[]= method dispatch
173
+ # so user probes on those cannot recurse.
174
+ #
175
+ # The original method is invoked via super. That super call lives
176
+ # in a block passed to #run_method_probe and captures its binding
177
+ # from inside this define_method block — the only place super
178
+ # resolves to the prepended-on class's method. #run_method_probe
179
+ # invokes it with yield, which does not dispatch Proc#call, so a
180
+ # user probe on Proc#call cannot intercept the trampoline, and no
181
+ # Proc is allocated for the block.
182
+ if RubyVersion.is?('>= 3')
183
+ define_method(method_name) do |*args, **kwargs, &target_block| # steep:ignore NoMethod
184
+ # steep:ignore FallbackAny below: Steep cannot narrow the
185
+ # **kwargs parameter inside this define_method block, so it
186
+ # falls back to untyped at the super and run_method_probe sites.
187
+ if DI.in_probe?
188
+ return super(*args, **kwargs, &target_block) # steep:ignore FallbackAny
183
189
  end
184
- # We intentionally do not use Core::Utils::Time.get_time
185
- # here because the time provider may be overridden by the
186
- # customer, and DI is not allowed to invoke customer code.
187
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
188
-
189
- di_duration = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) - di_start_time
190
190
 
191
- rv = nil
192
- begin
193
- # Under Ruby 2.6 we cannot just call super(*args, **kwargs)
194
- # for methods defined via method_missing.
195
- rv = if args.any?
196
- if kwargs.any?
197
- super(*args, **kwargs, &target_block)
198
- else
199
- super(*args, &target_block)
200
- end
201
- elsif kwargs.any?
202
- super(**kwargs, &target_block)
203
- else
204
- super(&target_block)
205
- end
206
- rescue NoMemoryError, Interrupt, SystemExit
207
- raise
208
- rescue Exception => exc # standard:disable Lint/RescueException
209
- # We will raise the exception captured here later, after
210
- # the instrumentation callback runs.
191
+ instrumenter.run_method_probe(
192
+ args, kwargs, target_block, # steep:ignore FallbackAny
193
+ self,
194
+ probe, responder,
195
+ loc, method_name,
196
+ ) do
197
+ super(*args, **kwargs, &target_block) # steep:ignore FallbackAny
211
198
  end
212
-
213
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
214
- duration = end_time - start_time
215
-
216
- # Restart DI timer.
217
- # The DI execution duration covers time spent in DI code before
218
- # the customer method is invoked and time spent in DI code
219
- # after the customer method finishes.
220
- di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
221
-
222
- # The method itself is not part of the stack trace because
223
- # we are getting the stack trace from outside of the method.
224
- # Add the method in manually as the top frame.
225
- method_frame = if loc
226
- [Location.new(loc.first, loc.last, method_name)]
227
- else
228
- # For virtual and lazily-defined methods, we do not have
229
- # the original source location here, and they won't be
230
- # included in the stack trace currently.
231
- # TODO when begin/end trace points are added for local
232
- # variable capture in method probes, we should be able
233
- # to obtain actual method execution location and use
234
- # that location here.
235
- []
236
- end
237
- caller_locs = method_frame + caller_locations
238
- # TODO capture arguments at exit
239
-
240
- context = Context.new(locals: nil, target_self: self,
241
- probe: probe, settings: settings, serializer: serializer,
242
- serialized_entry_args: serialized_entry_args,
243
- caller_locations: caller_locs,
244
- return_value: rv, duration: duration, exception: exc,)
245
-
246
- begin
247
- responder.probe_executed_callback(context)
248
-
249
- instrumenter.send(:check_and_disable_if_exceeded, probe, responder, di_start_time, di_duration)
250
- rescue => di_exc
251
- raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
252
-
253
- instrumenter.logger.debug { "di: unhandled exception in method probe: #{di_exc.class}: #{di_exc.message}" }
254
- instrumenter.telemetry&.report(di_exc, description: "Unhandled exception in method probe")
199
+ end
200
+ else
201
+ define_method(method_name) do |*args, &target_block| # steep:ignore NoMethod
202
+ if DI.in_probe?
203
+ return super(*args, &target_block)
255
204
  end
256
205
 
257
- if exc
258
- raise exc
259
- else
260
- rv
261
- end
262
- else
263
- # stop standard from trying to mess up my code
264
- _ = 42
265
-
266
- # The necessity to invoke super in each of these specific
267
- # ways is very difficult to test.
268
- # Existing tests, even though I wrote many, still don't
269
- # cause a failure if I replace all of the below with a
270
- # simple super(*args, **kwargs, &target_block).
271
- # But, let's be safe and go through the motions in case
272
- # there is actually a legitimate need for the breakdown.
273
- # TODO figure out how to test this properly.
274
- if args.any?
275
- if kwargs.any?
276
- super(*args, **kwargs, &target_block)
277
- else
278
- super(*args, &target_block)
279
- end
280
- elsif kwargs.any?
281
- super(**kwargs, &target_block)
282
- else
283
- super(&target_block)
206
+ # The super block replays the original *args (closed over here)
207
+ # so the ruby2_keywords-flagged trailing hash keeps its
208
+ # identity. run_method_probe receives the serialization-split
209
+ # arguments separately; forwarding does not use them.
210
+ pos_args, kwargs = instrumenter.kwargs_from_splat(args)
211
+ instrumenter.run_method_probe(
212
+ pos_args, kwargs, target_block,
213
+ self,
214
+ probe, responder,
215
+ loc, method_name,
216
+ ) do
217
+ super(*args, &target_block)
284
218
  end
285
219
  end
220
+ ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true) # steep:ignore NoMethod
286
221
  end
287
222
  end
288
223
 
@@ -490,6 +425,259 @@ module Datadog
490
425
 
491
426
  attr_reader :lock
492
427
 
428
+ # Body of the method probe wrapper. Extracted from the define_method
429
+ # block in #hook_method so the begin/ensure structure can use normal
430
+ # indentation. The original method is invoked with yield, calling the
431
+ # block passed by #hook_method; that block captures super's binding
432
+ # from inside the define_method block, the only place super resolves
433
+ # to the prepended-on class's method.
434
+ #
435
+ # Performance: this method takes eight positional parameters, not
436
+ # keyword parameters, deliberately. Every probed method invocation
437
+ # passes through here on the firing/disabled/rate-limited paths.
438
+ # Keyword-argument calls in Ruby allocate a fresh Hash on every call
439
+ # to materialize the keyword bindings; with eight keyword arguments
440
+ # (~300-1000 ns of allocation per invocation), that hash dominated
441
+ # the wrapper's per-call overhead. Positional parameters skip the
442
+ # allocation entirely. The cost is a long unlabeled signature and
443
+ # an unlabeled call site in #hook_method — both small price for
444
+ # eliminating per-call allocation on the hot path. The argument
445
+ # order matches #hook_method's call site exactly; do not reorder
446
+ # without updating both sides.
447
+ #
448
+ # @param args [Array] positional arguments passed to the probed method
449
+ # @param kwargs [Hash{Symbol => Object}] keyword arguments passed to the probed method
450
+ # @param target_block [Proc, nil] block argument passed to the probed method
451
+ # @param target_self [Object] the receiver of the probed method invocation
452
+ # @param probe [Datadog::DI::Probe] the probe whose callback this invocation runs
453
+ # @param responder [#probe_executed_callback, #probe_condition_evaluation_failed_callback] callback target invoked with the built Context
454
+ # @param loc [Array(String, Integer), nil] source location of the probed method, or nil for virtual/lazily-defined methods
455
+ # @param method_name [String] name of the probed method, used as the synthetic top stack frame label
456
+ # @yield invokes the original method via super and returns its value
457
+ # @return [Object] the original method's return value, or re-raises its exception
458
+ def run_method_probe(args, kwargs, target_block, target_self,
459
+ probe, responder, loc, method_name)
460
+ # Disabled probe: skip DI processing entirely. enter_probe is not
461
+ # called on this path so the guard is never set — there is no DI
462
+ # work to guard, and any nested probed methods invoked by the
463
+ # original method should fire normally without short-circuit.
464
+ return yield unless probe.enabled?
465
+
466
+ DI.enter_probe
467
+ begin
468
+ di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
469
+
470
+ continue = true
471
+ if condition = probe.condition
472
+ begin
473
+ # This context will be recreated later, unlike for line probes.
474
+ #
475
+ # We do not need the stack for condition evaluation, therefore
476
+ # stack is not passed to Context here.
477
+ context = Context.new(
478
+ locals: serializer.combine_args(args, kwargs, target_self),
479
+ target_self: target_self,
480
+ probe: probe, settings: settings, serializer: serializer,
481
+ )
482
+ continue = condition.satisfied?(context)
483
+ rescue Exception => exc # standard:disable Lint/RescueException
484
+ Datadog::DI.reraise_if_fatal(exc)
485
+ # Evaluation error exception can be raised for "expected"
486
+ # errors, we probably need another setting to control whether
487
+ # these exceptions are propagated.
488
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions &&
489
+ !exc.is_a?(DI::Error::ExpressionEvaluationError)
490
+
491
+ if context
492
+ # We want to report evaluation errors for conditions
493
+ # as probe snapshots. However, if we failed to create
494
+ # the context, we won't be able to report anything as
495
+ # the probe notifier builder requires a context.
496
+ begin
497
+ responder.probe_condition_evaluation_failed_callback(context, exc)
498
+ rescue Exception => nested_exc # standard:disable Lint/RescueException
499
+ Datadog::DI.reraise_if_fatal(nested_exc)
500
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
501
+
502
+ logger.debug { "di: error in probe condition evaluation failed callback: #{nested_exc.class}: #{nested_exc.message}" }
503
+ telemetry&.report(nested_exc, description: "Error in probe condition evaluation failed callback")
504
+ end
505
+ else
506
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
507
+
508
+ logger.debug { "di: error evaluating condition without context (tracer bug?): #{exc.class}: #{exc.message}" }
509
+ telemetry&.report(exc, description: "Error evaluating condition without context")
510
+ # If execution gets here, there is probably a bug in the tracer.
511
+ end
512
+
513
+ continue = false
514
+ end
515
+ end
516
+
517
+ rate_limiter = probe.rate_limiter
518
+ if continue and rate_limiter.nil? || rate_limiter.allow?
519
+ # Arguments may be mutated by the method, therefore
520
+ # they need to be serialized prior to method invocation.
521
+ serialized_entry_args = if probe.capture_snapshot?
522
+ serializer.serialize_args(args, kwargs, target_self,
523
+ depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
524
+ attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
525
+ end
526
+ # We intentionally do not use Core::Utils::Time.get_time
527
+ # here because the time provider may be overridden by the
528
+ # customer, and DI is not allowed to invoke customer code.
529
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
530
+
531
+ di_duration = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) - di_start_time
532
+
533
+ # Release re-entrancy guard for the original method so that
534
+ # probes on other methods (or recursive calls) fire normally
535
+ # during user code execution.
536
+ DI.leave_probe
537
+
538
+ rv = nil
539
+ begin
540
+ # yield invokes the super block without dispatching Proc#call,
541
+ # so a user probe on Proc#call cannot intercept the trampoline.
542
+ # The guard was just released above so nested probes on the
543
+ # customer method body fire normally — but Proc#call must
544
+ # remain bypassed regardless, because the disabled-probe early
545
+ # return at the top of this method also invokes the block.
546
+ rv = yield
547
+ rescue Exception => exc # standard:disable Lint/RescueException
548
+ Datadog::DI.reraise_if_fatal(exc)
549
+ # We will raise the (non-fatal) exception captured here later,
550
+ # after the instrumentation callback runs.
551
+ end
552
+
553
+ # Re-acquire re-entrancy guard for DI post-processing
554
+ # (building context, notification callback).
555
+ DI.enter_probe
556
+
557
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
558
+ duration = end_time - start_time
559
+
560
+ # Restart DI timer.
561
+ # The DI execution duration covers time spent in DI code before
562
+ # the customer method is invoked and time spent in DI code
563
+ # after the customer method finishes.
564
+ di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
565
+
566
+ # The method itself is not part of the stack trace because
567
+ # we are getting the stack trace from outside of the method.
568
+ # Add the method in manually as the top frame.
569
+ method_frame = if loc
570
+ [Location.new(loc.first, loc.last, method_name)]
571
+ else
572
+ # For virtual and lazily-defined methods, we do not have
573
+ # the original source location here, and they won't be
574
+ # included in the stack trace currently.
575
+ # TODO when begin/end trace points are added for local
576
+ # variable capture in method probes, we should be able
577
+ # to obtain actual method execution location and use
578
+ # that location here.
579
+ []
580
+ end
581
+ # depth 2 skips run_method_probe and the wrapper define_method
582
+ # block, leaving the user's call site as the first returned frame.
583
+ # caller_locations is RBS-nilable but never nil at this depth.
584
+ caller_locs = method_frame + (caller_locations(2) || [])
585
+ # TODO capture arguments at exit
586
+
587
+ context = Context.new(locals: nil, target_self: target_self,
588
+ probe: probe, settings: settings, serializer: serializer,
589
+ serialized_entry_args: serialized_entry_args,
590
+ caller_locations: caller_locs,
591
+ return_value: rv, duration: duration, exception: exc,)
592
+
593
+ begin
594
+ responder.probe_executed_callback(context)
595
+
596
+ check_and_disable_if_exceeded(probe, responder, di_start_time, di_duration)
597
+ rescue Exception => di_exc # standard:disable Lint/RescueException
598
+ Datadog::DI.reraise_if_fatal(di_exc)
599
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
600
+
601
+ logger.debug { "di: unhandled exception in method probe: #{di_exc.class}: #{di_exc.message}" }
602
+ telemetry&.report(di_exc, description: "Unhandled exception in method probe")
603
+ end
604
+
605
+ if exc
606
+ raise exc
607
+ else
608
+ rv
609
+ end
610
+ else
611
+ # Condition not satisfied or rate-limited: skip DI processing
612
+ # and call super. (The disabled-probe case is handled by the
613
+ # early return at the top of this method, so it never reaches
614
+ # this branch.)
615
+ #
616
+ # The guard must be released here (not relied on the ensure)
617
+ # because the super block invokes the customer's method, which
618
+ # may call other probed methods. Those nested probes should fire
619
+ # normally — they would not if the guard were still set,
620
+ # because the wrapper's early-return short-circuits when
621
+ # DI.in_probe? is true.
622
+ #
623
+ # No post-processing in this branch, so we don't re-acquire
624
+ # the guard after super. The ensure's leave_probe below is
625
+ # a no-op here (guard already cleared) and serves the
626
+ # firing branch above, where post-processing re-enters.
627
+ DI.leave_probe
628
+ # yield bypasses Proc#call dispatch — see the firing branch
629
+ # above for the same rationale.
630
+ yield
631
+ end
632
+ ensure
633
+ DI.leave_probe
634
+ end
635
+ end
636
+ # `run_method_probe` is documentation-private (`# @api private`) but must
637
+ # be lexically public so that the prepended wrapper can call it without
638
+ # `.send`. The wrapper hot path is reached when a probed customer method
639
+ # is invoked, and a customer probe on `Object#send` / `Kernel#send`
640
+ # would intercept any `.send` call before the wrapper had a chance to
641
+ # set the re-entrancy guard, causing infinite recursion. Calling the
642
+ # method directly avoids `Object#send` dispatch entirely.
643
+ public :run_method_probe
644
+
645
+ # Splits a Ruby < 3 splat-captured argument list into positional
646
+ # arguments and the trailing keyword-argument hash, for argument
647
+ # serialization only. Returns `[positional_args, kwargs]`. Forwarding
648
+ # does not use this — the wrapper's do_super replays the original
649
+ # splat so the trailing hash keeps its keyword identity.
650
+ #
651
+ # A trailing hash is treated as keyword arguments regardless of how it
652
+ # was passed at the call site, matching the previous `**kwargs`
653
+ # capture (which, on Ruby 2.x, absorbed a trailing hash into kwargs).
654
+ # The ruby2_keywords flag is intentionally not consulted here: callers
655
+ # that pass a hash positionally (e.g. `m(opts)`) still expect it shown
656
+ # as a keyword argument, consistent with Ruby 2.x serialization.
657
+ #
658
+ # The trailing-element test goes through DI.hash? (a C type check)
659
+ # rather than `last.is_a?(Hash)`: kwargs_from_splat runs before the
660
+ # re-entrancy guard is set, so a customer method probe on Kernel#is_a?
661
+ # would otherwise re-enter the wrapper and recurse. DI.hash? is a
662
+ # singleton method and cannot be targeted by a method probe.
663
+ #
664
+ # Defined only on Ruby < 3; the Ruby 3+ wrapper captures keyword
665
+ # arguments directly and never calls this.
666
+ if RubyVersion.is?('< 3')
667
+ def kwargs_from_splat(args)
668
+ last = args.last
669
+ if DI.hash?(last)
670
+ [args[0...-1] || [], last]
671
+ else
672
+ [args, {}]
673
+ end
674
+ end
675
+ # Lexically public for the same reason as #run_method_probe: the
676
+ # prepended wrapper must call it without `.send`, which a customer
677
+ # probe on Object#send could intercept.
678
+ public :kwargs_from_splat
679
+ end
680
+
493
681
  def line_trace_point_callback(probe, iseq, responder, tp)
494
682
  di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
495
683
 
@@ -528,7 +716,8 @@ module Datadog
528
716
  begin
529
717
  context = build_trace_point_context(probe, tp)
530
718
  return unless condition.satisfied?(context)
531
- rescue => exc
719
+ rescue Exception => exc # standard:disable Lint/RescueException
720
+ Datadog::DI.reraise_if_fatal(exc)
532
721
  # Evaluation error exception can be raised for "expected"
533
722
  # errors, we probably need another setting to control whether
534
723
  # these exceptions are propagated.
@@ -542,7 +731,8 @@ module Datadog
542
731
  # the probe notifier builder requires a context.
543
732
  begin
544
733
  responder.probe_condition_evaluation_failed_callback(context, condition, exc)
545
- rescue => nested_exc
734
+ rescue Exception => nested_exc # standard:disable Lint/RescueException
735
+ Datadog::DI.reraise_if_fatal(nested_exc)
546
736
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
547
737
 
548
738
  logger.debug { "di: error in probe condition evaluation failed callback: #{nested_exc.class}: #{nested_exc.message}" }
@@ -575,7 +765,8 @@ module Datadog
575
765
  responder.probe_executed_callback(context)
576
766
 
577
767
  check_and_disable_if_exceeded(probe, responder, di_start_time)
578
- rescue => exc
768
+ rescue Exception => exc # standard:disable Lint/RescueException
769
+ Datadog::DI.reraise_if_fatal(exc)
579
770
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
580
771
  logger.debug { "di: unhandled exception in line trace point: #{exc.class}: #{exc.message}" }
581
772
  telemetry&.report(exc, description: "Unhandled exception in line trace point")
@@ -672,6 +863,31 @@ module Datadog
672
863
  rescue NameError => exc
673
864
  raise Error::DITargetNotDefined, "Class not defined: #{cls_name}: #{exc.class}: #{exc.message}"
674
865
  end
866
+
867
+ # Matches a class name in the Datadog namespace, tolerating the alias
868
+ # forms that Object.const_get resolves to the same top-level constants.
869
+ # Two such forms exist: an optional leading "::" (Ruby's root-namespace
870
+ # prefix), and any number of leading "Object::" segments. Top-level
871
+ # constants are constants of Object, so "Object::Datadog::DI::Instrumenter"
872
+ # resolves to Datadog::DI::Instrumenter; without stripping "Object::" a
873
+ # probe could name a tracer-internal class through an Object:: alias and
874
+ # bypass the rejection, then get prepended onto tracer internals. These
875
+ # are the only alias paths to the top-level Datadog: "Foo::Datadog" does
876
+ # not resolve through const_get for an arbitrary Foo. Anchored at \A so
877
+ # the match decides in O(prefix length) without scanning.
878
+ DATADOG_NAMESPACE_TYPE_NAME = /\A(?:::)?(?:Object::)*Datadog(?:::|\z)/
879
+ private_constant :DATADOG_NAMESPACE_TYPE_NAME
880
+
881
+ # Returns true when +cls_name+ names the +Datadog+ module itself or
882
+ # any class/module under it (e.g. +Datadog::Tracing::SpanOperation+).
883
+ # The check is purely textual on the probe's declared type name; it
884
+ # does not require the class to be loaded. A leading +::+ and any
885
+ # number of leading +Object::+ segments are treated the same as the
886
+ # bare form, since Object.const_get resolves those aliases to the same
887
+ # top-level constants.
888
+ def datadog_namespace_type_name?(cls_name)
889
+ DATADOG_NAMESPACE_TYPE_NAME.match?(cls_name)
890
+ end
675
891
  end
676
892
  end
677
893
  end
@@ -171,6 +171,20 @@ module Datadog
171
171
  line_no
172
172
  end
173
173
 
174
+ # Returns the method name associated with the probe, raising
175
+ # Error::MissingMethodName if the probe does not have a method name
176
+ # associated with it.
177
+ #
178
+ # This method is used by instrumentation driver to ensure a method name
179
+ # that is passed into the instrumentation logic is actually a method name
180
+ # and not nil.
181
+ def method_name!
182
+ if method_name.nil?
183
+ raise Error::MissingMethodName, "Probe #{id} does not have a method name associated with it"
184
+ end
185
+ method_name
186
+ end
187
+
174
188
  # Source code location of the probe, for diagnostic reporting.
175
189
  def location
176
190
  if method?