DI: capture expressions#5845
Conversation
Typing analysisNote: Ignored files are excluded from the next sections.
|
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 73c860e | Docs | Datadog PR Page | Give us feedback! |
BenchmarksBenchmark execution time: 2026-07-03 02:00:32 Comparing candidate commit 73c860e in PR branch Found 1 performance improvements and 0 performance regressions! Performance is the same for 47 metrics, 1 unstable metrics.
|
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Adds Dynamic Instrumentation “capture expressions” support for Ruby log probes, enabling selective value capture (via DSL expressions) into snapshot payloads under captureExpressions, with configurable per-fire serialization time budgeting.
Changes:
- Parse
captureExpressions(including per-expression capture limits) from remote config and attach them toDatadog::DI::Probe. - Evaluate capture expressions at probe fire time with a configurable time budget and merge per-expression evaluation errors into snapshot
evaluationErrors. - Extend serializer plumbing to allow per-evaluation overrides for string length / collection size limits; add docs, RBS signatures, and specs.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| supported-configurations.json | Adds env var metadata for max serialization time budget. |
| spec/datadog/di/probe_spec.rb | Adds unit coverage for probe capture_expressions behavior and rate-limit defaults. |
| spec/datadog/di/probe_builder_spec.rb | Adds remote-config parsing tests for captureExpressions and related behaviors. |
| spec/datadog/di/capture_expression_spec.rb | Adds tests for capture expression / limits value objects and limit resolution behavior. |
| spec/datadog/di/capture_expression_evaluator_spec.rb | Adds tests for evaluation success/failure, timeout behavior, and limit propagation to serializer. |
| sig/datadog/di/serializer.rbs | Updates serialize_value signature to include length and collection_size. |
| sig/datadog/di/probe.rbs | Adds capture-expression-related fields and predicate signature. |
| sig/datadog/di/probe_notification_builder.rbs | Adds capture_expression_evaluator ivar/reader typing. |
| sig/datadog/di/probe_builder.rbs | Adds capture-expression builder helper typings and name pattern constant. |
| sig/datadog/di/capture_expression.rbs | Adds RBS for CaptureExpression and CaptureLimits. |
| sig/datadog/di/capture_expression_evaluator.rbs | Adds RBS for evaluator class and return types. |
| lib/datadog/di/serializer.rb | Threads per-evaluation length/collection_size through recursive serialization. |
| lib/datadog/di/probe.rb | Stores capture-expression config on probes and adjusts default rate limiting. |
| lib/datadog/di/probe_notification_builder.rb | Evaluates capture expressions for snapshots and merges evaluation errors. |
| lib/datadog/di/probe_builder.rb | Parses captureExpressions from remote config and validates names/expr structure. |
| lib/datadog/di/configuration.rb | Introduces max_time_to_serialize_ms DI setting backed by env var. |
| lib/datadog/di/capture_expression.rb | Adds CaptureExpression and CaptureLimits classes plus limit-resolution logic. |
| lib/datadog/di/capture_expression_evaluator.rb | Implements evaluator with time budget, serialization, and error reporting. |
| lib/datadog/di/boot.rb | Ensures capture-expression components are loaded during DI boot. |
| lib/datadog/core/configuration/supported_configurations.rb | Registers new env var in supported configuration list (generated). |
| docs/DynamicInstrumentation.md | Documents capture expressions, behavior choices, and new env var. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5affe59f77
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…d length The CaptureLimits.resolve doc comment promises a per-field fallback chain of expr_limits -> probe -> settings, but for collection_size and length the code went straight from the expression-level override to settings, skipping the probe-level overrides parsed into probe.max_capture_collection_size / probe.max_capture_string_length. That meant a probe-level capture.maxCollectionSize or capture.maxLength from remote config was silently ignored unless repeated on every expression. Adds the probe-level step to both fallback chains so the resolver matches its documented contract. The serializer already accepts length: and collection_size: keyword arguments, so the fix flows end-to-end. Addresses review comments on PR #5845 from @Copilot (3344166066) and @chatgpt-codex-connector (3344167297). Co-Authored-By: Claude <noreply@anthropic.com>
build_capture_expressions called raw.empty? before verifying raw was an Array. A non-Array value from remote config (e.g. an Integer) would raise NoMethodError instead of the intended ArgumentError, and the outer rescue KeyError in build_from_remote_config would not wrap it either — producing a misleading error class even though the bare rescue in Component#parse_probe_spec_and_notify still kept it from escaping. Reorders the early-return: nil short-circuits, then the Array type check, then the empty-array short-circuit. Adds a probe_builder_spec test for the non-Array case. Addresses review comment on PR #5845 from @Copilot (3344166098). Co-Authored-By: Claude <noreply@anthropic.com>
The return-time Context for method probes was built with locals: nil, so capture expressions referencing arg1 or kwarg names resolved as undefined/nil — the condition path at instrumenter.rb:135 already populates locals via serializer.combine_args(args, kwargs, self) but the return-time path didn't. Passes combine_args(args, kwargs, self) as locals when the probe has capture expressions; keeps nil otherwise to avoid an allocation on every method invocation for probes that don't need it. At return time the args reference is the same Ruby object as at entry, so values reflect any in-place mutation by the method body — matching what conditions already see and what cross-tracer convention reports for method exit scope. Adds an integration test exercising a method probe with a capture expression referencing arg1 against a positional argument. Addresses review comment on PR #5845 from @chatgpt-codex-connector (3344167304). Co-Authored-By: Claude <noreply@anthropic.com>
…d length The CaptureLimits.resolve doc comment promises a per-field fallback chain of expr_limits -> probe -> settings, but for collection_size and length the code went straight from the expression-level override to settings, skipping the probe-level overrides parsed into probe.max_capture_collection_size / probe.max_capture_string_length. That meant a probe-level capture.maxCollectionSize or capture.maxLength from remote config was silently ignored unless repeated on every expression. Adds the probe-level step to both fallback chains so the resolver matches its documented contract. The serializer already accepts length: and collection_size: keyword arguments, so the fix flows end-to-end. Addresses review comments on PR #5845 from @Copilot (3344166066) and @chatgpt-codex-connector (3344167297). Co-Authored-By: Claude <noreply@anthropic.com>
build_capture_expressions called raw.empty? before verifying raw was an Array. A non-Array value from remote config (e.g. an Integer) would raise NoMethodError instead of the intended ArgumentError, and the outer rescue KeyError in build_from_remote_config would not wrap it either — producing a misleading error class even though the bare rescue in Component#parse_probe_spec_and_notify still kept it from escaping. Reorders the early-return: nil short-circuits, then the Array type check, then the empty-array short-circuit. Adds a probe_builder_spec test for the non-Array case. Addresses review comment on PR #5845 from @Copilot (3344166098). Co-Authored-By: Claude <noreply@anthropic.com>
The return-time Context for method probes was built with locals: nil, so capture expressions referencing arg1 or kwarg names resolved as undefined/nil — the condition path at instrumenter.rb:135 already populates locals via serializer.combine_args(args, kwargs, self) but the return-time path didn't. Passes combine_args(args, kwargs, self) as locals when the probe has capture expressions; keeps nil otherwise to avoid an allocation on every method invocation for probes that don't need it. At return time the args reference is the same Ruby object as at entry, so values reflect any in-place mutation by the method body — matching what conditions already see and what cross-tracer convention reports for method exit scope. Adds an integration test exercising a method probe with a capture expression referencing arg1 against a positional argument. Addresses review comment on PR #5845 from @chatgpt-codex-connector (3344167304). Co-Authored-By: Claude <noreply@anthropic.com>
8b413eb to
1bf6c26
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7cf491474a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Adds capture-expression support to Ruby log probes.
* Parse the `captureExpressions` field in remote-config LOG_PROBE payloads.
* Evaluate each expression against the probe scope at fire time.
* Emit a `captureExpressions` block in the snapshot under the same
`captures.{lines.<n>|entry|return}` key that full snapshots use.
* Per-fire time budget controlled by
`DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS` (default 200).
Remaining expressions emit a stub `{notCapturedReason: "timeout"}`.
* Snapshot wins at fire time when both `captureSnapshot` and
`captureExpressions` are set on the same probe (matches Python/Java/Go).
* Per-expression evaluation errors are added to `evaluationErrors` with
the failed expression's name; the failed key is omitted from the
`captureExpressions` block.
* Capture-expression probes default to the snapshot rate-limit bucket
(1/sec); existing `sampling.snapshotsPerSecond` overrides this.
Tests cover parsing (including the backend name pattern
`^[a-zA-Z0-9_?]+$`), the rate-limit fallback, per-expression depth
override, time-budget exhaustion, and the success/error/timeout output
shapes.
Known limitation: per-expression `maxLength` and `maxCollectionSize`
are not yet honored end-to-end; the snapshot serializer reads those
two limits from the DI settings directly. Per-expression
`maxReferenceDepth` and `maxFieldCount` work.
- Thread max_length and max_collection_size kwargs through Serializer#serialize_value and its recursive calls, so per-expression CaptureLimits overrides for all four fields work end-to-end (previously only depth and attribute_count were honored). - Add Steep signatures for the new ProbeBuilder helpers, the capture_expression_evaluator accessor on ProbeNotificationBuilder, and the new length/collection_size kwargs on Serializer#serialize_value. - Remove the 'known limitation' note from docs/DynamicInstrumentation.md.
…_SERIALIZE The prior commit added DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS as a new key, which would require central Configuration Registry coordination and broke validate_supported_configurations_v2_local_file. Switch to the existing DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE env var that .NET already declares (and which is already centrally registered). Its 200 ms default matches our chosen budget; its semantics extend naturally to whole-snapshot serialization if Ruby later applies the budget more broadly. * Rename the setting from capture_expression_timeout_ms to max_time_to_serialize_ms. * Drop the new DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS entry from supported-configurations.json and supported_configurations.rb; add the existing DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE. * Update docs/DynamicInstrumentation.md to reference the .NET-aligned env var. * Update the capture-expression evaluator and spec to use the new setting name.
…d length The CaptureLimits.resolve doc comment promises a per-field fallback chain of expr_limits -> probe -> settings, but for collection_size and length the code went straight from the expression-level override to settings, skipping the probe-level overrides parsed into probe.max_capture_collection_size / probe.max_capture_string_length. That meant a probe-level capture.maxCollectionSize or capture.maxLength from remote config was silently ignored unless repeated on every expression. Adds the probe-level step to both fallback chains so the resolver matches its documented contract. The serializer already accepts length: and collection_size: keyword arguments, so the fix flows end-to-end. Addresses review comments on PR #5845 from @Copilot (3344166066) and @chatgpt-codex-connector (3344167297). Co-Authored-By: Claude <noreply@anthropic.com>
Replace String.new('literal') with unary +'literal' in two test
sites added by the evaluateAt commit. The unary + form produces an
unfrozen string literal without an extra constructor call, which is
what StandardRB's Performance/UnfreezeString cop requires.
Both sites use the mutable string to test that mutating_method's
in-place sub! changes the entry vs return-time visible value, so the
mutability requirement is unchanged.
Verified locally: bundle exec rake standard reports no offenses.
Mirror the condition-evaluation rescue pattern used a few lines above in the same method. The evaluator itself catches per-expression StandardError; this outer rescue covers non-evaluator failures (building the entry Context, combine_args) so a raise during entry-time evaluation no longer escapes the instrumented method body. propagate_all_exceptions still surfaces failures in the test suite. Otherwise the failure is logged at debug + reported to telemetry, and the method continues with no entry block stashed (the snapshot will omit captures.entry.captureExpressions for this fire, matching the 'no entry block was stashed' fallback already covered by spec).
Two new integration tests cover the bug-fix paths the PR description
calls out but the existing maxLength test did not exercise:
- Method probe, captureSnapshot=true, max_capture_collection_size=2:
verifies array truncation in entry args, @return, and self ivar.
Adds InstrumentationSpecTestClass#collection_method as the target.
- Line probe at instrumentation_integration_test_class.rb:40,
captureSnapshot=true, max_capture_collection_size=1: verifies the
redacted hash local is truncated via the line-probe locals path
(Context#serialized_locals → serialize_vars → probe-level limits).
The earlier maxLength test covered method probes only. These two close
the maxCollectionSize and line-probe-locals gaps the PR description
claims as fixed.
CaptureExpression's RBS signature declares expr as a non-nil DI::EL::Expression. The capture-expression probe specs only exercise storage and probe.capture_expressions? — they never evaluate the expression — so a typed stub keeps the type contract honest without changing what the tests verify.
7cf4914 to
5b9e104
Compare
Address review comments (Copilot): the YARD @param/@return types for `logger` documented Datadog::Core::Logger, but DI::Component wraps the core logger in a DI::Logger facade (component.rb:71) before passing it to ProbeNotificationBuilder and CaptureExpressionEvaluator. The RBS sigs already declare DI::Logger; this aligns the YARD docs with actual usage. - Fixed in lib/datadog/di/probe_notification_builder.rb:18 (@param, initializer) - Fixed in lib/datadog/di/probe_notification_builder.rb:42 (@return, attr_reader) - Fixed in lib/datadog/di/capture_expression_evaluator.rb:28 (@param, initializer) - Fixed in lib/datadog/di/capture_expression_evaluator.rb:52 (@return, attr_reader)
Address review comment (Copilot): the ProbeNotificationBuilder comment described capture_expression_evaluator as "Lazily-constructed", but it is built unconditionally in the initializer. Update the comment to match the implementation. The Instrumenter's evaluator (instrumenter.rb:87) is genuinely lazy (`||=`) and its comment is left unchanged. - Fixed in lib/datadog/di/probe_notification_builder.rb:50
Address review comment (codex): @duration compiles to `(context.duration * 1000)`. The entry-time capture-expression path added in this PR builds the Context with duration: nil, so any capture expression referencing @duration (even isUndefined(@duration), whose argument is evaluated first) raised NoMethodError on `nil * 1000` and the key was dropped with an evaluation error — unlike @return/@exception, which resolve to nil at entry. Guard the multiplication so @duration resolves to nil (undefined) at entry and still yields duration*1000ms at exit. duration: 0 stays 0 (0 is truthy). - Fixed in lib/datadog/di/el/compiler.rb (@duration expansion) - Added mechanism coverage in spec/datadog/di/el/duration_spec.rb (entry nil -> nil / isUndefined true; exit 0.5 -> 500.0 / isUndefined false) - Added e2e coverage in spec/datadog/di/integration/instrumentation_spec.rb (method probe, evaluateAt: ENTRY, @duration capture expression captured as null value with no evaluation error) Verified: rspec (mechanism 4, e2e 1, EL 147, capture-expression 11, notification/instrumenter/limits/dispatch 154, integration+builder 81), StandardRB clean, Steep clean.
|
@codex review |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Remove the sentence describing how the exit-time ProbeNotificationBuilder consumes the entry-time capture-expression block, per review direction. The remaining comment still documents what the fields hold and when they are nil.
Rename "stash" to "store" and drop the clause describing how the notification builder emits the block under captures.entry, per review direction. The comment still states what the entry-time path does and that the Context carries the result to the notification builder.
PR #5945 (Rescue all exceptions in DI and SymDB) converted DI/SymDB catch-all rescues to `rescue Exception` + Datadog::DI.reraise_if_fatal, so non-StandardError exceptions do not escape into customer code while fatal exceptions (SystemExit, SignalException, NoMemoryError) still propagate. That merge covered the code present at the time; this PR later added two new catch-all rescues that still used `rescue => e` (StandardError only): - lib/datadog/di/capture_expression_evaluator.rb: per-expression rescue (runs customer expression code) — now rescue Exception + reraise_if_fatal, with require_relative "fatal_exceptions" - lib/datadog/di/instrumenter.rb: entry-time capture-expression rescue — now rescue Exception + reraise_if_fatal, matching the sibling condition-evaluation rescue above it Added evaluator coverage mirroring #5945's tests: a non-StandardError (NotImplementedError) is caught and recorded as an evaluation error; a fatal exception (SystemExit) is re-raised out of #evaluate. Verified: rspec (evaluator + instrumenter + integration + duration = 174, 0 failures), StandardRB clean, Steep clean.
This PR added two Datadog.logger.debug calls in ProbeBuilder (master had none), reaching for the global logger from module_function code — a Component Pattern violation (component code must use injected dependencies, not globals). Both log paths originate from build_from_remote_config (the second via parse_evaluate_at), and the sole production caller, Component#parse_probe_spec_and_notify, already has the injected DI::Logger facade in scope. - probe_builder.rb: build_from_remote_config now takes a required `logger:` kwarg and threads it into parse_evaluate_at; both Datadog.logger.debug calls become logger.debug - component.rb: pass the component's injected logger at the call site - probe_builder.rbs: update the two signatures - specs: inject a logger double at every build_from_remote_config call site; the two probe_builder_spec assertions now expect on the injected logger rather than the Datadog.logger global Verified: rspec (probe_builder, component, integration instrumentation + notification builder, remote = 137, 0 failures), StandardRB clean, Steep clean.
|
@codex review |
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
# Conflicts: # lib/datadog/di/instrumenter.rb # spec/datadog/di/probe_spec.rb
Replace em-dash and arrow characters introduced by this PR's comments with ASCII equivalents (-- and ->).
Replace string-name double("settings", ...) with
instance_double(Datadog::Core::Configuration::Settings, ...) in the
CaptureLimits.resolve and Probe#snapshot_serializer_limits specs so the
settings interface is verified.
The backend permits duplicate captureExpressions names and forwards them unchanged. ProbeBuilder now collapses entries sharing a name, keeping the last occurrence, so each name is evaluated once instead of being evaluated and then overwritten when the snapshot is serialized. The collapse is logged at debug.
What does this PR do?
Adds capture-expression support to Ruby log probes in Dynamic Instrumentation. A capture expression is a named DSL expression attached to a log probe; at probe-fire time, the expression is evaluated against the probe scope and its serialized value is emitted in the snapshot payload under
captures.….captureExpressions.<name>. Capture expressions are an alternative tocaptureSnapshot: true: they let the user select exactly which values to capture instead of the whole local/argument scope.Motivation:
Feature parity with other languages.
Behavior choices for Ruby (cross-tracer comparison drove each pick):
DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE.Limitations:
Method probes (still) do not capture locals, therefore it is not possible to capture method locals via capture expressions also.
Positional arguments are (still) reported as
arg1etc. to the backend and users must specify that to capture them. Works but unintuitive.Change log entry
Yes. Dynamic Instrumentation log probes now support capture expressions as an alternative to capturing the full local/argument scope. Also fixes probe-level capture-limit overrides being silently dropped.
Manual Verification
UI:

Line probe:

Method probe - positional arg:

Method probe - keyword arg:
