Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1eaaa3c
DI: capture expressions
p-ddsign Jun 1, 2026
e654ddc
DI: capture expressions: full per-expression limit support + Steep sigs
p-ddsign Jun 1, 2026
c6b5c3c
DI: capture expressions: reuse DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO…
p-ddsign Jun 1, 2026
720d6ee
DI: capture-expression resolver honors probe-level collection_size an…
p-ddsign Jun 2, 2026
46b14dc
DI: ProbeBuilder validates captureExpressions type before checking empty
p-ddsign Jun 2, 2026
1223e4d
DI: method-probe capture expressions can reference arg/kwarg locals
p-ddsign Jun 2, 2026
3731094
DI: thread probe-level capture limits through snapshot serialization
p-ddsign Jun 2, 2026
dd5b753
remove docs
Jun 3, 2026
f4e4a52
Wire telemetry through capture-expression evaluator and fix probe-lev…
p-ddsign Jun 2, 2026
f2a676a
Split CaptureLimits into its own file
p-ddsign Jun 2, 2026
16784b7
DI: inject logger/telemetry into capture-expression evaluator + requi…
p-ddsign Jun 3, 2026
951c8b9
DI: document types/contracts of capture-expression public surfaces
p-ddsign Jun 3, 2026
c177f28
DI: remove references to internal planning repo from public source
p-ddsign Jun 3, 2026
d98276b
DI: tighten RBS sigs and fix trailing blank line in capture_expressio…
p-ddsign Jun 3, 2026
03df970
DI: update integration spec to match new ProbeNotificationBuilder sig…
p-ddsign Jun 3, 2026
15a411f
DI: add end-to-end tests for capture-expression behaviors, restore op…
p-ddsign Jun 3, 2026
1bcc718
DI: declare max_time_to_serialize_ms in _DI settings RBS interface
p-ddsign Jun 9, 2026
a223e5c
DI: drop entry.captureExpressions from method probes
p-ddsign Jun 9, 2026
25fbe0c
DI: rename capture-expression timeout counter, add mid-loop test, gua…
p-ddsign Jun 9, 2026
f006105
DI: honor evaluateAt on capture-expression method probes
p-ddsign Jun 9, 2026
536e37b
Fix StandardRB Performance/UnfreezeString violations
p-ddsign Jun 9, 2026
7c1d614
DI: drop private-repo path from capture-expression comments
p-ddsign Jun 10, 2026
003bb43
DI: protect entry-time capture-expression evaluation with rescue
p-ddsign Jun 15, 2026
e6e0c12
DI: integration coverage for probe-level capture limits
p-ddsign Jun 15, 2026
5b9e104
DI: use instance_double for CaptureExpression expr in probe_spec
p-ddsign Jun 15, 2026
538c4eb
Merge branch 'master' into di-capture-expressions
p-datadog Jul 1, 2026
894fc41
DI: fix YARD logger type to DI::Logger facade
p-ddsign Jul 1, 2026
9895aca
DI: correct capture_expression_evaluator construction comment
p-ddsign Jul 1, 2026
bac314e
DI: make @duration EL expansion nil-safe at entry time
p-ddsign Jul 1, 2026
a083101
DI: drop exit-time consumer sentence from entry-capture comment
p-ddsign Jul 2, 2026
dea1048
DI: trim entry-capture comment in instrumenter
p-ddsign Jul 2, 2026
3dae84b
DI: apply #5945 fatal-exception handling to capture-expression rescues
p-ddsign Jul 2, 2026
b5cd07b
DI: inject logger into ProbeBuilder instead of Datadog.logger global
p-ddsign Jul 2, 2026
2c65598
Merge remote-tracking branch 'origin/master' into di-capture-expressions
p-ddsign Jul 2, 2026
81ffa25
Use ASCII characters in capture-expression comments
p-ddsign Jul 2, 2026
14ee270
Use instance_double for settings in capture-limit specs
p-ddsign Jul 2, 2026
89ab76a
DI: collapse duplicate capture-expression names at parse time
p-ddsign Jul 3, 2026
73c860e
DI: note cross-tracer last-wins parity in dedup comment
p-ddsign Jul 3, 2026
8700aac
DI: capture expressions — remove explanatory comments
p-ddsign Jul 13, 2026
427a214
DI: drop .NET cross-tracer reference and timeout-stub detail from MAX…
p-ddsign Jul 13, 2026
90a64e1
DI: drop Java cross-tracer reference from probe_builder_spec context
p-ddsign Jul 13, 2026
1ef3cfb
DI: drop libdatadog reference from probe_builder_spec example descrip…
p-ddsign Jul 13, 2026
f10f3f6
Merge branch 'master' into di-capture-expressions
p-datadog Jul 13, 2026
0111853
Extract capture-expression predicates and simplify limit resolution o…
p-ddsign Jul 14, 2026
c19b846
DI RBS: use `any` alias for diff-added arbitrary-value untyped
p-ddsign Jul 14, 2026
60e8c77
Merge branch 'master' into di-capture-expressions
TonyCTHsu Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -2958,6 +2958,7 @@ Dynamic Instrumentation allows you to add log probes, metric probes, and span pr
| `DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS` | `Array` | Comma-separated list of variable/key names to redact in addition to the built-in list. Names are normalized (underscores, dashes, `@`, `$` removed) and matched case-insensitively. | `[]` |
| `DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS` | `Array` | Comma-separated list of variable/key names to exclude from the built-in redaction list, allowing their values to be captured. | `[]` |
| `DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES` | `Array` | Comma-separated list of class names whose values will be redacted. Suffix with `*` for wildcard matching (for example, `Foo*` redacts `Foo`, `FooBar`, `Foo::Bar`). | `[]` |
| `DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE` | `Integer` | Time budget (in milliseconds) per probe fire for capture-expression evaluation. | `200` |

Alternatively, set DI parameters inside a `Datadog.configure` block:

Expand All @@ -2967,6 +2968,7 @@ Alternatively, set DI parameters inside a `Datadog.configure` block:
| `c.dynamic_instrumentation.redacted_identifiers` | `Array` | Variable/key names to redact in addition to the built-in list. | `[]` |
| `c.dynamic_instrumentation.redaction_excluded_identifiers` | `Array` | Variable/key names to exclude from the built-in redaction list. | `[]` |
| `c.dynamic_instrumentation.redacted_type_names` | `Array` | Class names whose values will be redacted. Suffix with `*` for wildcard. | `[]` |
| `c.dynamic_instrumentation.max_time_to_serialize_ms` | `Integer` | Per-probe-fire time budget (in milliseconds) for capture-expression evaluation. | `200` |

#### Symbol Database

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ module Configuration
"DD_DBM_PROPAGATION_MODE",
"DD_DISABLE_DATADOG_RAILS",
"DD_DYNAMIC_INSTRUMENTATION_ENABLED",
"DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE",
"DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE",
"DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS",
"DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES",
Expand Down
3 changes: 3 additions & 0 deletions lib/datadog/di/boot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
require_relative 'code_tracker'
require_relative 'component'
require_relative 'context'
require_relative 'capture_limits'
require_relative 'capture_expression'
require_relative 'capture_expression_evaluator'
require_relative 'instrumenter'
require_relative 'probe'
require_relative 'probe_builder'
Expand Down
21 changes: 21 additions & 0 deletions lib/datadog/di/capture_expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require_relative "capture_limits"

module Datadog
module DI
class CaptureExpression
def initialize(name:, expr:, limits: nil)
@name = name
@expr = expr
@limits = limits
end

attr_reader :name

attr_reader :expr

attr_reader :limits
end
end
end
71 changes: 71 additions & 0 deletions lib/datadog/di/capture_expression_evaluator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require_relative "capture_expression"
require_relative "capture_limits"
require_relative "fatal_exceptions"

module Datadog
module DI
class CaptureExpressionEvaluator
TELEMETRY_NAMESPACE = "dynamic_instrumentation"

def initialize(settings:, serializer:, logger:, telemetry: nil)
@settings = settings
@serializer = serializer
@logger = logger
@telemetry = telemetry
end

attr_reader :settings

attr_reader :serializer

attr_reader :logger

attr_reader :telemetry

def evaluate(probe, context)
budget_ns = settings.dynamic_instrumentation.max_time_to_serialize_ms * 1_000_000
deadline_ns = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :nanosecond) + budget_ns

output = {}
evaluation_errors = []

probe.capture_expressions.each do |capture_expression|
name = capture_expression.name

if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :nanosecond) >= deadline_ns
output[name] = {notCapturedReason: "timeout"}
telemetry&.inc(TELEMETRY_NAMESPACE, "capture_expressions_skipped_by_timeout", 1)
next
end

begin
value = capture_expression.expr.evaluate(context)
limits = CaptureLimits.resolve(
expr_limits: capture_expression.limits,
probe: probe,
settings: settings,
)
output[name] = serializer.serialize_value(
value, name: name,
depth: limits[:depth],
attribute_count: limits[:attribute_count],
length: limits[:length],
collection_size: limits[:collection_size],
)
rescue Exception => exc # standard:disable Lint/RescueException
Datadog::DI.reraise_if_fatal(exc)
evaluation_errors << {expr: name, message: "#{exc.class}: #{exc.message}"}
logger.debug do
"di: probe #{probe.id}: capture expression #{name}: evaluation failed: #{exc.class}: #{exc.message}"
end
telemetry&.report(exc, description: "DI capture-expression evaluation failed")
end
end

[output, evaluation_errors]
end
end
end
end
41 changes: 41 additions & 0 deletions lib/datadog/di/capture_limits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Datadog
module DI
class CaptureLimits
def initialize(max_reference_depth: nil, max_collection_size: nil,
max_length: nil, max_field_count: nil)
@max_reference_depth = max_reference_depth
@max_collection_size = max_collection_size
@max_length = max_length
@max_field_count = max_field_count
end

attr_reader :max_reference_depth

attr_reader :max_collection_size

attr_reader :max_length

attr_reader :max_field_count

def self.resolve(expr_limits:, probe:, settings:)
di = settings.dynamic_instrumentation
{
depth: expr_limits&.max_reference_depth ||
probe.max_capture_depth ||
di.max_capture_depth,
collection_size: expr_limits&.max_collection_size ||
probe.max_capture_collection_size ||
di.max_capture_collection_size,
length: expr_limits&.max_length ||
probe.max_capture_string_length ||
di.max_capture_string_length,
attribute_count: expr_limits&.max_field_count ||
probe.max_capture_attribute_count ||
di.max_capture_attribute_count,
}
end
end
end
end
4 changes: 2 additions & 2 deletions lib/datadog/di/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def initialize(settings, agent_settings, logger, code_tracker: nil, telemetry: n
@serializer = Serializer.new(settings, redactor, telemetry: telemetry)
@instrumenter = Instrumenter.new(settings, serializer, logger, code_tracker: code_tracker, telemetry: telemetry)
@probe_repository = ProbeRepository.new
@probe_notification_builder = ProbeNotificationBuilder.new(settings, serializer)
@probe_notification_builder = ProbeNotificationBuilder.new(settings, serializer, logger, telemetry: telemetry)
@probe_notifier_worker = ProbeNotifierWorker.new(
settings, logger,
agent_settings: agent_settings,
Expand Down Expand Up @@ -185,7 +185,7 @@ def shutdown!(replacement = nil)
end

def parse_probe_spec_and_notify(probe_spec)
probe = ProbeBuilder.build_from_remote_config(probe_spec)
probe = ProbeBuilder.build_from_remote_config(probe_spec, logger: logger)
rescue Exception => exc # standard:disable Lint/RescueException
Datadog::DI.reraise_if_fatal(exc)
begin
Expand Down
6 changes: 6 additions & 0 deletions lib/datadog/di/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ def self.add_settings!(base)
o.default 20
end

option :max_time_to_serialize_ms do |o|
o.type :int
o.default 200
o.env 'DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE'
end

# Settings in the 'internal' group are for internal Datadog
# use only, and are needed to test dynamic instrumentation or
# experiment with features not released to customers.
Expand Down
12 changes: 9 additions & 3 deletions lib/datadog/di/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def initialize(probe:, settings:, serializer:, locals: nil,
target_self: nil,
path: nil, caller_locations: nil,
serialized_entry_args: nil,
entry_capture_expressions: nil,
entry_capture_evaluation_errors: nil,
return_value: nil, duration: nil, exception: nil)
@probe = probe
@settings = settings
Expand All @@ -23,6 +25,8 @@ def initialize(probe:, settings:, serializer:, locals: nil,
@path = path
@caller_locations = caller_locations
@serialized_entry_args = serialized_entry_args
@entry_capture_expressions = entry_capture_expressions
@entry_capture_evaluation_errors = entry_capture_evaluation_errors
@return_value = return_value
@duration = duration
@exception = exception
Expand All @@ -44,6 +48,10 @@ def initialize(probe:, settings:, serializer:, locals: nil,

attr_reader :serialized_entry_args

attr_reader :entry_capture_expressions

attr_reader :entry_capture_evaluation_errors

# Return value for the method, for a method probe
attr_reader :return_value

Expand All @@ -55,9 +63,7 @@ def initialize(probe:, settings:, serializer:, locals: nil,

def serialized_locals
# TODO cache?
locals && serializer.serialize_vars(locals,
depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count,)
locals && serializer.serialize_vars(locals, **probe.snapshot_serializer_limits(settings))
end

def fetch(var_name)
Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/di/el/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def compile_partial(ast)
# We could format to a string here but what if customer
# has @duration as part of an expression and wants
# to retain it as a number?
"(context.duration * 1000)"
"(context.duration && context.duration * 1000)"
when '@exception'
"context.exception"
else
Expand Down
37 changes: 34 additions & 3 deletions lib/datadog/di/instrumenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require_relative '../core/utils/time'
require_relative '../ruby_version'
require_relative 'fatal_exceptions'
require_relative 'capture_expression_evaluator'

# rubocop:disable Lint/AssignmentInCondition
# rubocop:disable Style/AndOr
Expand Down Expand Up @@ -84,6 +85,12 @@ def initialize(settings, serializer, logger, code_tracker: nil, telemetry: nil)
attr_reader :telemetry
attr_reader :code_tracker

def capture_expression_evaluator
@capture_expression_evaluator ||= CaptureExpressionEvaluator.new(
settings: settings, serializer: serializer, logger: logger, telemetry: telemetry,
)
end

# This is a substitute for Thread::Backtrace::Location
# which does not have a public constructor.
# Used for the fabricated stack frame for the method itself
Expand Down Expand Up @@ -520,8 +527,27 @@ def run_method_probe(args, kwargs, target_block, target_self,
# they need to be serialized prior to method invocation.
serialized_entry_args = if probe.capture_snapshot?
serializer.serialize_args(args, kwargs, target_self,
depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
**probe.snapshot_serializer_limits(settings))
end

entry_capture_expressions = nil
entry_capture_evaluation_errors = nil
if probe.capture_entry_expressions?
begin
entry_context = Context.new(
probe: probe, settings: settings, serializer: serializer,
target_self: target_self,
locals: serializer.combine_args(args, kwargs, target_self),
)
entry_capture_expressions, entry_capture_evaluation_errors =
capture_expression_evaluator.evaluate(probe, entry_context)
rescue Exception => exc # standard:disable Lint/RescueException
Datadog::DI.reraise_if_fatal(exc)
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions

logger.debug { "di: error evaluating entry-time capture expressions: #{exc.class}: #{exc.message}" }
telemetry&.report(exc, description: "Error evaluating entry-time capture expressions")
end
end
# We intentionally do not use Core::Utils::Time.get_time
# here because the time provider may be overridden by the
Expand Down Expand Up @@ -584,9 +610,14 @@ def run_method_probe(args, kwargs, target_block, target_self,
caller_locs = method_frame + (caller_locations(2) || [])
# TODO capture arguments at exit

context = Context.new(locals: nil, target_self: target_self,
capture_expression_locals = if probe.capture_expressions_only?
serializer.combine_args(args, kwargs, target_self)
end
context = Context.new(locals: capture_expression_locals, target_self: target_self,
probe: probe, settings: settings, serializer: serializer,
serialized_entry_args: serialized_entry_args,
entry_capture_expressions: entry_capture_expressions,
entry_capture_evaluation_errors: entry_capture_evaluation_errors,
caller_locations: caller_locs,
return_value: rv, duration: duration, exception: exc,)

Expand Down
49 changes: 47 additions & 2 deletions lib/datadog/di/probe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,17 @@ module DI
class Probe
KNOWN_TYPES = %i[log].freeze

EVALUATE_AT_VALUES = %i[entry exit].freeze

def initialize(id:, type:,
file: nil, line_no: nil, type_name: nil, method_name: nil,
template: nil, template_segments: nil,
capture_snapshot: false, max_capture_depth: nil,
max_capture_attribute_count: nil, condition: nil,
max_capture_attribute_count: nil,
max_capture_collection_size: nil, max_capture_string_length: nil,
capture_expressions: [],
evaluate_at: nil,
condition: nil,
rate_limit: nil)
# Perform some sanity checks here to detect unexpected attribute
# combinations, in order to not do them in subsequent code.
Expand Down Expand Up @@ -81,9 +87,17 @@ def initialize(id:, type:,
@capture_snapshot = !!capture_snapshot
@max_capture_depth = max_capture_depth
@max_capture_attribute_count = max_capture_attribute_count
@max_capture_collection_size = max_capture_collection_size
@max_capture_string_length = max_capture_string_length
@capture_expressions = capture_expressions || []
evaluate_at = :exit if evaluate_at.nil?
unless EVALUATE_AT_VALUES.include?(evaluate_at)
raise ArgumentError, "Unknown evaluate_at value: #{evaluate_at.inspect} (expected one of #{EVALUATE_AT_VALUES.inspect})"
end
@evaluate_at = evaluate_at
@condition = condition

@rate_limit = rate_limit || (@capture_snapshot ? 1 : 5000)
@rate_limit = rate_limit || ((@capture_snapshot || !@capture_expressions.empty?) ? 1 : 5000)
@rate_limiter = Datadog::Core::TokenBucket.new(@rate_limit)

# At most one report per second.
Expand Down Expand Up @@ -120,6 +134,14 @@ def initialize(id:, type:,
# the global default will be used.
attr_reader :max_capture_attribute_count

attr_reader :max_capture_collection_size

attr_reader :max_capture_string_length

attr_reader :capture_expressions

attr_reader :evaluate_at

# Rate limit in effect, in invocations per second. Always present.
attr_reader :rate_limit

Expand All @@ -140,6 +162,29 @@ def capture_snapshot?
@capture_snapshot
end

def capture_expressions?
!@capture_expressions.empty?
end

def evaluate_at_entry?
evaluate_at == :entry
end

# Capture-expression mode: the probe captures expressions but not a full
# snapshot. Snapshot capture serializes its own values, so expression
# capture only runs when a snapshot is not being taken.
def capture_expressions_only?
capture_expressions? && !capture_snapshot?
end

def capture_entry_expressions?
capture_expressions_only? && evaluate_at_entry?
end

def snapshot_serializer_limits(settings)
Comment thread
p-datadog marked this conversation as resolved.
CaptureLimits.resolve(expr_limits: nil, probe: self, settings: settings)
end

# Returns whether the probe is a line probe.
#
# Method probes may still specify a file name (to aid in locating the
Expand Down
Loading
Loading