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,6 +1,9 @@
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'
6
+ require_relative 'capture_expression_evaluator'
4
7
 
5
8
  # rubocop:disable Lint/AssignmentInCondition
6
9
  # rubocop:disable Style/AndOr
@@ -82,6 +85,12 @@ module Datadog
82
85
  attr_reader :telemetry
83
86
  attr_reader :code_tracker
84
87
 
88
+ def capture_expression_evaluator
89
+ @capture_expression_evaluator ||= CaptureExpressionEvaluator.new(
90
+ settings: settings, serializer: serializer, logger: logger, telemetry: telemetry,
91
+ )
92
+ end
93
+
85
94
  # This is a substitute for Thread::Backtrace::Location
86
95
  # which does not have a public constructor.
87
96
  # Used for the fabricated stack frame for the method itself
@@ -95,6 +104,18 @@ module Datadog
95
104
  # are not reachable via prepend on the class itself. Line probes are
96
105
  # unaffected since they install via TracePoint, not method dispatch.
97
106
  def hook_method(probe, responder)
107
+ # Reject method probes targeting any class or module in the Datadog
108
+ # namespace. The tracer uses these classes internally while building
109
+ # and dispatching probe snapshots; allowing a method probe on them
110
+ # would let DI's own code paths re-enter the probe wrapper. Line
111
+ # probes are unaffected (TracePoint is self-disabling during its
112
+ # callback) and are not rejected here.
113
+ type_name = probe.type_name
114
+ if type_name && datadog_namespace_type_name?(type_name)
115
+ raise Error::ProbeTargetForbidden,
116
+ "Method probes on the Datadog namespace are not permitted: #{type_name}##{probe.method_name}"
117
+ end
118
+
98
119
  lock.synchronize do
99
120
  if probe.instrumentation_module
100
121
  # Already instrumented, warn?
@@ -103,10 +124,9 @@ module Datadog
103
124
  end
104
125
 
105
126
  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
127
+ method_name = probe.method_name!
128
+ target_method = begin
129
+ cls.instance_method(method_name)
110
130
  rescue NameError
111
131
  # The target method is not defined.
112
132
  # This could be because it will be explicitly defined later
@@ -114,175 +134,97 @@ module Datadog
114
134
  # or the method is virtual (provided by a method_missing handler).
115
135
  # In these cases we do not have a source location for the
116
136
  # target method here.
137
+ nil
117
138
  end
118
- rate_limiter = probe.rate_limiter
119
- settings = self.settings
120
- instrumenter = self
121
139
 
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)
140
+ # Reject method probes whose target method resolves to Kernel#lambda,
141
+ # including inherited targets. Every class inherits Kernel#lambda, so a
142
+ # probe naming any type that does not override lambda (Object#lambda,
143
+ # String#lambda, a plain user class, ...) prepends the wrapper ahead of
144
+ # Kernel#lambda and reaches the wrapper's super(&block) path. On
145
+ # Ruby 3.3+ that path raises ArgumentError for every lambda { ... }
146
+ # call site passing a captured block, breaking lambda creation
147
+ # process-wide. Resolving the method owner catches the inherited case
148
+ # that a textual "Kernel" name match misses; a type that defines its
149
+ # own lambda has a different owner and is left alone. Probing lambda
150
+ # creation has no legitimate customer use case. See #5560.
151
+ if method_name == "lambda" && target_method&.owner == ::Kernel
152
+ raise Error::ProbeTargetForbidden,
153
+ "Method probes on Kernel#lambda are not permitted: #{probe.type_name}##{method_name}"
154
+ end
127
155
 
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
156
+ loc = target_method&.source_location
157
+ instrumenter = self
175
158
 
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)
159
+ mod = Module.new do
160
+ # Argument delegation follows docs/Delegation.md: on Ruby 3+
161
+ # positional and keyword arguments are captured and forwarded
162
+ # separately; on Ruby < 3 everything is captured into a single
163
+ # splat that ruby2_keywords flags, which is the only way to forward
164
+ # a trailing hash while preserving its positional-vs-keyword
165
+ # identity. The pre-3 branch is what lets an empty trailing hash
166
+ # (e.g. foo('x', {})) reach the original method intact.
167
+ #
168
+ # Re-entrancy guard: if we are already inside a DI method-probe
169
+ # callback, skip DI processing and call the original method
170
+ # directly. This prevents SystemStackError when a method probe is
171
+ # set on a stdlib method that DI itself calls during method-probe
172
+ # snapshot building (e.g., String#length, Hash#each). Line probes
173
+ # do not enter the guard — they rely on Ruby's TracePoint, which
174
+ # self-disables during its own callback. Nested invocations during
175
+ # DI processing also bypass the rate limiter: they are not
176
+ # user-observable probe firings, just internal calls that happen to
177
+ # land on a probed method. Guard storage is fiber-local;
178
+ # DI.in_probe?/enter_probe/leave_probe are implemented in C and
179
+ # access it directly, bypassing Thread#[]/Thread#[]= method dispatch
180
+ # so user probes on those cannot recurse.
181
+ #
182
+ # The original method is invoked via super. That super call lives
183
+ # in a block passed to #run_method_probe and captures its binding
184
+ # from inside this define_method block — the only place super
185
+ # resolves to the prepended-on class's method. #run_method_probe
186
+ # invokes it with yield, which does not dispatch Proc#call, so a
187
+ # user probe on Proc#call cannot intercept the trampoline, and no
188
+ # Proc is allocated for the block.
189
+ if RubyVersion.is?('>= 3')
190
+ define_method(method_name) do |*args, **kwargs, &target_block| # steep:ignore NoMethod
191
+ # steep:ignore FallbackAny below: Steep cannot narrow the
192
+ # **kwargs parameter inside this define_method block, so it
193
+ # falls back to untyped at the super and run_method_probe sites.
194
+ if DI.in_probe?
195
+ return super(*args, **kwargs, &target_block) # steep:ignore FallbackAny
183
196
  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
197
 
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.
198
+ instrumenter.run_method_probe(
199
+ args, kwargs, target_block, # steep:ignore FallbackAny
200
+ self,
201
+ probe, responder,
202
+ loc, method_name,
203
+ ) do
204
+ super(*args, **kwargs, &target_block) # steep:ignore FallbackAny
211
205
  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
- []
206
+ end
207
+ else
208
+ define_method(method_name) do |*args, &target_block| # steep:ignore NoMethod
209
+ if DI.in_probe?
210
+ return super(*args, &target_block)
236
211
  end
237
- caller_locs = method_frame + caller_locations
238
- # TODO capture arguments at exit
239
212
 
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")
255
- end
256
-
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)
213
+ # The super block replays the original *args (closed over here)
214
+ # so the ruby2_keywords-flagged trailing hash keeps its
215
+ # identity. run_method_probe receives the serialization-split
216
+ # arguments separately; forwarding does not use them.
217
+ pos_args, kwargs = instrumenter.kwargs_from_splat(args)
218
+ instrumenter.run_method_probe(
219
+ pos_args, kwargs, target_block,
220
+ self,
221
+ probe, responder,
222
+ loc, method_name,
223
+ ) do
224
+ super(*args, &target_block)
284
225
  end
285
226
  end
227
+ ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true) # steep:ignore NoMethod
286
228
  end
287
229
  end
288
230
 
@@ -490,6 +432,283 @@ module Datadog
490
432
 
491
433
  attr_reader :lock
492
434
 
435
+ # Body of the method probe wrapper. Extracted from the define_method
436
+ # block in #hook_method so the begin/ensure structure can use normal
437
+ # indentation. The original method is invoked with yield, calling the
438
+ # block passed by #hook_method; that block captures super's binding
439
+ # from inside the define_method block, the only place super resolves
440
+ # to the prepended-on class's method.
441
+ #
442
+ # Performance: this method takes eight positional parameters, not
443
+ # keyword parameters, deliberately. Every probed method invocation
444
+ # passes through here on the firing/disabled/rate-limited paths.
445
+ # Keyword-argument calls in Ruby allocate a fresh Hash on every call
446
+ # to materialize the keyword bindings; with eight keyword arguments
447
+ # (~300-1000 ns of allocation per invocation), that hash dominated
448
+ # the wrapper's per-call overhead. Positional parameters skip the
449
+ # allocation entirely. The cost is a long unlabeled signature and
450
+ # an unlabeled call site in #hook_method — both small price for
451
+ # eliminating per-call allocation on the hot path. The argument
452
+ # order matches #hook_method's call site exactly; do not reorder
453
+ # without updating both sides.
454
+ #
455
+ # @param args [Array] positional arguments passed to the probed method
456
+ # @param kwargs [Hash{Symbol => Object}] keyword arguments passed to the probed method
457
+ # @param target_block [Proc, nil] block argument passed to the probed method
458
+ # @param target_self [Object] the receiver of the probed method invocation
459
+ # @param probe [Datadog::DI::Probe] the probe whose callback this invocation runs
460
+ # @param responder [#probe_executed_callback, #probe_condition_evaluation_failed_callback] callback target invoked with the built Context
461
+ # @param loc [Array(String, Integer), nil] source location of the probed method, or nil for virtual/lazily-defined methods
462
+ # @param method_name [String] name of the probed method, used as the synthetic top stack frame label
463
+ # @yield invokes the original method via super and returns its value
464
+ # @return [Object] the original method's return value, or re-raises its exception
465
+ def run_method_probe(args, kwargs, target_block, target_self,
466
+ probe, responder, loc, method_name)
467
+ # Disabled probe: skip DI processing entirely. enter_probe is not
468
+ # called on this path so the guard is never set — there is no DI
469
+ # work to guard, and any nested probed methods invoked by the
470
+ # original method should fire normally without short-circuit.
471
+ return yield unless probe.enabled?
472
+
473
+ DI.enter_probe
474
+ begin
475
+ di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
476
+
477
+ continue = true
478
+ if condition = probe.condition
479
+ begin
480
+ # This context will be recreated later, unlike for line probes.
481
+ #
482
+ # We do not need the stack for condition evaluation, therefore
483
+ # stack is not passed to Context here.
484
+ context = Context.new(
485
+ locals: serializer.combine_args(args, kwargs, target_self),
486
+ target_self: target_self,
487
+ probe: probe, settings: settings, serializer: serializer,
488
+ )
489
+ continue = condition.satisfied?(context)
490
+ rescue Exception => exc # standard:disable Lint/RescueException
491
+ Datadog::DI.reraise_if_fatal(exc)
492
+ # Evaluation error exception can be raised for "expected"
493
+ # errors, we probably need another setting to control whether
494
+ # these exceptions are propagated.
495
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions &&
496
+ !exc.is_a?(DI::Error::ExpressionEvaluationError)
497
+
498
+ if context
499
+ # We want to report evaluation errors for conditions
500
+ # as probe snapshots. However, if we failed to create
501
+ # the context, we won't be able to report anything as
502
+ # the probe notifier builder requires a context.
503
+ begin
504
+ responder.probe_condition_evaluation_failed_callback(context, exc)
505
+ rescue Exception => nested_exc # standard:disable Lint/RescueException
506
+ Datadog::DI.reraise_if_fatal(nested_exc)
507
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
508
+
509
+ logger.debug { "di: error in probe condition evaluation failed callback: #{nested_exc.class}: #{nested_exc.message}" }
510
+ telemetry&.report(nested_exc, description: "Error in probe condition evaluation failed callback")
511
+ end
512
+ else
513
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
514
+
515
+ logger.debug { "di: error evaluating condition without context (tracer bug?): #{exc.class}: #{exc.message}" }
516
+ telemetry&.report(exc, description: "Error evaluating condition without context")
517
+ # If execution gets here, there is probably a bug in the tracer.
518
+ end
519
+
520
+ continue = false
521
+ end
522
+ end
523
+
524
+ rate_limiter = probe.rate_limiter
525
+ if continue and rate_limiter.nil? || rate_limiter.allow?
526
+ # Arguments may be mutated by the method, therefore
527
+ # they need to be serialized prior to method invocation.
528
+ serialized_entry_args = if probe.capture_snapshot?
529
+ serializer.serialize_args(args, kwargs, target_self,
530
+ **probe.snapshot_serializer_limits(settings))
531
+ end
532
+
533
+ entry_capture_expressions = nil
534
+ entry_capture_evaluation_errors = nil
535
+ if probe.capture_entry_expressions?
536
+ begin
537
+ entry_context = Context.new(
538
+ probe: probe, settings: settings, serializer: serializer,
539
+ target_self: target_self,
540
+ locals: serializer.combine_args(args, kwargs, target_self),
541
+ )
542
+ entry_capture_expressions, entry_capture_evaluation_errors =
543
+ capture_expression_evaluator.evaluate(probe, entry_context)
544
+ rescue Exception => exc # standard:disable Lint/RescueException
545
+ Datadog::DI.reraise_if_fatal(exc)
546
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
547
+
548
+ logger.debug { "di: error evaluating entry-time capture expressions: #{exc.class}: #{exc.message}" }
549
+ telemetry&.report(exc, description: "Error evaluating entry-time capture expressions")
550
+ end
551
+ end
552
+ # We intentionally do not use Core::Utils::Time.get_time
553
+ # here because the time provider may be overridden by the
554
+ # customer, and DI is not allowed to invoke customer code.
555
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
556
+
557
+ di_duration = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) - di_start_time
558
+
559
+ # Release re-entrancy guard for the original method so that
560
+ # probes on other methods (or recursive calls) fire normally
561
+ # during user code execution.
562
+ DI.leave_probe
563
+
564
+ rv = nil
565
+ begin
566
+ # yield invokes the super block without dispatching Proc#call,
567
+ # so a user probe on Proc#call cannot intercept the trampoline.
568
+ # The guard was just released above so nested probes on the
569
+ # customer method body fire normally — but Proc#call must
570
+ # remain bypassed regardless, because the disabled-probe early
571
+ # return at the top of this method also invokes the block.
572
+ rv = yield
573
+ rescue Exception => exc # standard:disable Lint/RescueException
574
+ Datadog::DI.reraise_if_fatal(exc)
575
+ # We will raise the (non-fatal) exception captured here later,
576
+ # after the instrumentation callback runs.
577
+ end
578
+
579
+ # Re-acquire re-entrancy guard for DI post-processing
580
+ # (building context, notification callback).
581
+ DI.enter_probe
582
+
583
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
584
+ duration = end_time - start_time
585
+
586
+ # Restart DI timer.
587
+ # The DI execution duration covers time spent in DI code before
588
+ # the customer method is invoked and time spent in DI code
589
+ # after the customer method finishes.
590
+ di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
591
+
592
+ # The method itself is not part of the stack trace because
593
+ # we are getting the stack trace from outside of the method.
594
+ # Add the method in manually as the top frame.
595
+ method_frame = if loc
596
+ [Location.new(loc.first, loc.last, method_name)]
597
+ else
598
+ # For virtual and lazily-defined methods, we do not have
599
+ # the original source location here, and they won't be
600
+ # included in the stack trace currently.
601
+ # TODO when begin/end trace points are added for local
602
+ # variable capture in method probes, we should be able
603
+ # to obtain actual method execution location and use
604
+ # that location here.
605
+ []
606
+ end
607
+ # depth 2 skips run_method_probe and the wrapper define_method
608
+ # block, leaving the user's call site as the first returned frame.
609
+ # caller_locations is RBS-nilable but never nil at this depth.
610
+ caller_locs = method_frame + (caller_locations(2) || [])
611
+ # TODO capture arguments at exit
612
+
613
+ capture_expression_locals = if probe.capture_expressions_only?
614
+ serializer.combine_args(args, kwargs, target_self)
615
+ end
616
+ context = Context.new(locals: capture_expression_locals, target_self: target_self,
617
+ probe: probe, settings: settings, serializer: serializer,
618
+ serialized_entry_args: serialized_entry_args,
619
+ entry_capture_expressions: entry_capture_expressions,
620
+ entry_capture_evaluation_errors: entry_capture_evaluation_errors,
621
+ caller_locations: caller_locs,
622
+ return_value: rv, duration: duration, exception: exc,)
623
+
624
+ begin
625
+ responder.probe_executed_callback(context)
626
+
627
+ check_and_disable_if_exceeded(probe, responder, di_start_time, di_duration)
628
+ rescue Exception => di_exc # standard:disable Lint/RescueException
629
+ Datadog::DI.reraise_if_fatal(di_exc)
630
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
631
+
632
+ logger.debug { "di: unhandled exception in method probe: #{di_exc.class}: #{di_exc.message}" }
633
+ telemetry&.report(di_exc, description: "Unhandled exception in method probe")
634
+ end
635
+
636
+ if exc
637
+ raise exc
638
+ else
639
+ rv
640
+ end
641
+ else
642
+ # Condition not satisfied or rate-limited: skip DI processing
643
+ # and call super. (The disabled-probe case is handled by the
644
+ # early return at the top of this method, so it never reaches
645
+ # this branch.)
646
+ #
647
+ # The guard must be released here (not relied on the ensure)
648
+ # because the super block invokes the customer's method, which
649
+ # may call other probed methods. Those nested probes should fire
650
+ # normally — they would not if the guard were still set,
651
+ # because the wrapper's early-return short-circuits when
652
+ # DI.in_probe? is true.
653
+ #
654
+ # No post-processing in this branch, so we don't re-acquire
655
+ # the guard after super. The ensure's leave_probe below is
656
+ # a no-op here (guard already cleared) and serves the
657
+ # firing branch above, where post-processing re-enters.
658
+ DI.leave_probe
659
+ # yield bypasses Proc#call dispatch — see the firing branch
660
+ # above for the same rationale.
661
+ yield
662
+ end
663
+ ensure
664
+ DI.leave_probe
665
+ end
666
+ end
667
+ # `run_method_probe` is documentation-private (`# @api private`) but must
668
+ # be lexically public so that the prepended wrapper can call it without
669
+ # `.send`. The wrapper hot path is reached when a probed customer method
670
+ # is invoked, and a customer probe on `Object#send` / `Kernel#send`
671
+ # would intercept any `.send` call before the wrapper had a chance to
672
+ # set the re-entrancy guard, causing infinite recursion. Calling the
673
+ # method directly avoids `Object#send` dispatch entirely.
674
+ public :run_method_probe
675
+
676
+ # Splits a Ruby < 3 splat-captured argument list into positional
677
+ # arguments and the trailing keyword-argument hash, for argument
678
+ # serialization only. Returns `[positional_args, kwargs]`. Forwarding
679
+ # does not use this — the wrapper's do_super replays the original
680
+ # splat so the trailing hash keeps its keyword identity.
681
+ #
682
+ # A trailing hash is treated as keyword arguments regardless of how it
683
+ # was passed at the call site, matching the previous `**kwargs`
684
+ # capture (which, on Ruby 2.x, absorbed a trailing hash into kwargs).
685
+ # The ruby2_keywords flag is intentionally not consulted here: callers
686
+ # that pass a hash positionally (e.g. `m(opts)`) still expect it shown
687
+ # as a keyword argument, consistent with Ruby 2.x serialization.
688
+ #
689
+ # The trailing-element test goes through DI.hash? (a C type check)
690
+ # rather than `last.is_a?(Hash)`: kwargs_from_splat runs before the
691
+ # re-entrancy guard is set, so a customer method probe on Kernel#is_a?
692
+ # would otherwise re-enter the wrapper and recurse. DI.hash? is a
693
+ # singleton method and cannot be targeted by a method probe.
694
+ #
695
+ # Defined only on Ruby < 3; the Ruby 3+ wrapper captures keyword
696
+ # arguments directly and never calls this.
697
+ if RubyVersion.is?('< 3')
698
+ def kwargs_from_splat(args)
699
+ last = args.last
700
+ if DI.hash?(last)
701
+ [args[0...-1] || [], last]
702
+ else
703
+ [args, {}]
704
+ end
705
+ end
706
+ # Lexically public for the same reason as #run_method_probe: the
707
+ # prepended wrapper must call it without `.send`, which a customer
708
+ # probe on Object#send could intercept.
709
+ public :kwargs_from_splat
710
+ end
711
+
493
712
  def line_trace_point_callback(probe, iseq, responder, tp)
494
713
  di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
495
714
 
@@ -528,7 +747,8 @@ module Datadog
528
747
  begin
529
748
  context = build_trace_point_context(probe, tp)
530
749
  return unless condition.satisfied?(context)
531
- rescue => exc
750
+ rescue Exception => exc # standard:disable Lint/RescueException
751
+ Datadog::DI.reraise_if_fatal(exc)
532
752
  # Evaluation error exception can be raised for "expected"
533
753
  # errors, we probably need another setting to control whether
534
754
  # these exceptions are propagated.
@@ -542,7 +762,8 @@ module Datadog
542
762
  # the probe notifier builder requires a context.
543
763
  begin
544
764
  responder.probe_condition_evaluation_failed_callback(context, condition, exc)
545
- rescue => nested_exc
765
+ rescue Exception => nested_exc # standard:disable Lint/RescueException
766
+ Datadog::DI.reraise_if_fatal(nested_exc)
546
767
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
547
768
 
548
769
  logger.debug { "di: error in probe condition evaluation failed callback: #{nested_exc.class}: #{nested_exc.message}" }
@@ -575,7 +796,8 @@ module Datadog
575
796
  responder.probe_executed_callback(context)
576
797
 
577
798
  check_and_disable_if_exceeded(probe, responder, di_start_time)
578
- rescue => exc
799
+ rescue Exception => exc # standard:disable Lint/RescueException
800
+ Datadog::DI.reraise_if_fatal(exc)
579
801
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
580
802
  logger.debug { "di: unhandled exception in line trace point: #{exc.class}: #{exc.message}" }
581
803
  telemetry&.report(exc, description: "Unhandled exception in line trace point")
@@ -672,6 +894,31 @@ module Datadog
672
894
  rescue NameError => exc
673
895
  raise Error::DITargetNotDefined, "Class not defined: #{cls_name}: #{exc.class}: #{exc.message}"
674
896
  end
897
+
898
+ # Matches a class name in the Datadog namespace, tolerating the alias
899
+ # forms that Object.const_get resolves to the same top-level constants.
900
+ # Two such forms exist: an optional leading "::" (Ruby's root-namespace
901
+ # prefix), and any number of leading "Object::" segments. Top-level
902
+ # constants are constants of Object, so "Object::Datadog::DI::Instrumenter"
903
+ # resolves to Datadog::DI::Instrumenter; without stripping "Object::" a
904
+ # probe could name a tracer-internal class through an Object:: alias and
905
+ # bypass the rejection, then get prepended onto tracer internals. These
906
+ # are the only alias paths to the top-level Datadog: "Foo::Datadog" does
907
+ # not resolve through const_get for an arbitrary Foo. Anchored at \A so
908
+ # the match decides in O(prefix length) without scanning.
909
+ DATADOG_NAMESPACE_TYPE_NAME = /\A(?:::)?(?:Object::)*Datadog(?:::|\z)/
910
+ private_constant :DATADOG_NAMESPACE_TYPE_NAME
911
+
912
+ # Returns true when +cls_name+ names the +Datadog+ module itself or
913
+ # any class/module under it (e.g. +Datadog::Tracing::SpanOperation+).
914
+ # The check is purely textual on the probe's declared type name; it
915
+ # does not require the class to be loaded. A leading +::+ and any
916
+ # number of leading +Object::+ segments are treated the same as the
917
+ # bare form, since Object.const_get resolves those aliases to the same
918
+ # top-level constants.
919
+ def datadog_namespace_type_name?(cls_name)
920
+ DATADOG_NAMESPACE_TYPE_NAME.match?(cls_name)
921
+ end
675
922
  end
676
923
  end
677
924
  end