DI: capture expressions#5845
Conversation
Typing analysisNote: Ignored files are excluded from the next sections.
|
|
BenchmarksBenchmark execution time: 2026-06-15 22:36:47 Comparing candidate commit 7cf4914 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 45 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>
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>
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>
Probe-level overrides for max_capture_depth, max_capture_attribute_count, max_capture_string_length, and max_capture_collection_size from remote config were partially or fully ignored on every snapshot-path serializer call site: - probe_notification_builder.rb @return: depth and attribute_count flowed through, length and collection_size did not. - probe_notification_builder.rb self (method probe and line probe): none of the four limits were passed; serializer fell back to settings for all of them, silently dropping every probe-level override. - instrumenter.rb entry-args (serialize_args): depth and attribute_count flowed through, length and collection_size did not. - context.rb serialized_locals (serialize_vars): same gap as serialize_args. Adds Probe#snapshot_serializer_limits(settings) returning the four-limit keyword-arg hash with per-field fallback to settings. All five snapshot-path sites now splat it into the serializer call. Serializer#serialize_args and serialize_vars gain matching length / collection_size kwargs that thread to the underlying serialize_value. Adds Probe#snapshot_serializer_limits unit tests covering the no-override, all-override, and mixed-override cases, plus an integration test exercising a max_capture_string_length override end-to-end through entry args, @return, and self's instance variable. Stale doc comments on Probe#max_capture_collection_size and Probe#max_capture_string_length (which claimed the snapshot serializer read settings directly) are removed. Co-Authored-By: Claude <noreply@anthropic.com>
…el limit fallback
Address review feedback on the capture-expressions PR:
1. CaptureLimits.resolve now consults probe.max_capture_collection_size and
probe.max_capture_string_length, matching the customer-facing documentation
("fall back to the probe-level capture and then to the tracer's default
settings, independently per field"). Previously those two probe-level
attributes were stored but never read.
2. ProbeNotificationBuilder now accepts and forwards telemetry to
CaptureExpressionEvaluator. Component wires telemetry through at
construction. Previously the evaluator's rescue block and timeout
counter were no-ops because @telemetry was always nil.
3. CaptureExpressionEvaluator#evaluate uses the correct telemetry.inc
signature: (namespace, metric_name, value). The previous 2-argument
form would have raised ArgumentError once telemetry was actually
wired through.
4. Added end-to-end ProbeNotificationBuilder#build_snapshot tests for:
- line probe with capture_expressions emits captureExpressions block
- method probe with capture_expressions emits entry/return blocks
- method probe with capture_expressions and an exception emits throwable
- captureSnapshot=true wins and drops captureExpressions
- per-expression evaluation errors flow into top-level evaluationErrors
5. Added branch coverage for ProbeBuilder.build_capture_expressions /
build_capture_limits type-check raises (non-array captureExpressions,
non-hash entry, non-hash capture).
6. Documented DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE and the
programmatic max_time_to_serialize_ms setting in GettingStarted.md.
7. capture_expression_spec.rb now uses instance_double(Datadog::DI::Probe)
instead of plain double() so interface drift is caught.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Match the dd-trace-rb convention of one class per file. The DI codebase otherwise has Probe in probe.rb, Serializer in serializer.rb, etc. capture_expression.rb was the only DI file with two classes. - lib/datadog/di/capture_limits.rb — new, CaptureLimits only - lib/datadog/di/capture_expression.rb — CaptureExpression only, requires capture_limits for the limits attribute type - sig split into capture_limits.rbs and capture_expression.rbs - spec/datadog/di/capture_limits_spec.rb — new, CaptureLimits tests - capture_expression_spec.rb — CaptureExpression tests only - boot.rb requires capture_limits before capture_expression - capture_expression_evaluator.rb and probe_builder.rb add explicit capture_limits requires (they were getting it transitively via capture_expression.rb) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…re telemetry on notification builder Addresses review feedback on the capture-expressions PR: 1. Component pattern violation: CaptureExpressionEvaluator was calling Datadog.logger.debug on the rescue path despite already taking injected settings/serializer/telemetry. Logger is now a required constructor parameter; ProbeNotificationBuilder forwards it (and is itself given logger via Component#initialize, matching the existing Instrumenter signature). 2. Nil-defaulted keyword arguments: telemetry was `telemetry: nil` on both CaptureExpressionEvaluator and ProbeNotificationBuilder, but production always supplies a real telemetry component (Component passes it through). The default was test convenience, not a real optional dependency. Both telemetry: kwargs are now required. 3. Telemetry assertions: capture_expression_evaluator_spec now uses instance doubles for logger and telemetry and asserts the two telemetry calls (timeout -> telemetry.inc(NAMESPACE, "capture_expression_timeout", 1); rescue -> telemetry.report(exc, description: "DI capture-expression evaluation failed")) plus the corresponding logger.debug call on the rescue path. The two failing-edge tests previously relied on telemetry's nil default, so the rescue and timeout branches were never verified. Co-Authored-By: Claude <noreply@anthropic.com>
Add YARD docs to every new public attribute, constructor parameter, and module function added by the capture-expressions PR so a reader can determine the type, shape, and nil-meaning of each value from the source file alone, without opening other files. - capture_expression.rb: @param tags on initialize; attr_reader comments giving type and nil semantics for name/expr/limits. - capture_limits.rb: @param tags on initialize and self.resolve; attr_reader comments naming the integer-or-nil contract per field and what nil means at each level of the resolve chain. - capture_expression_evaluator.rb: attr_reader comments naming the concrete types of settings/serializer/logger/telemetry and how each is consumed at evaluate-time. - probe.rb: @param/@return on snapshot_serializer_limits, naming the resolved Hash{Symbol => Integer} shape (no nils in the output even though probe-level fields may be nil). - probe_builder.rb: full YARD docs for build_capture_expressions and build_capture_limits, including @raise enumeration. - probe_notification_builder.rb: @param tags on initialize; attr_reader comments for the new logger/telemetry/capture_expression_evaluator surfaces. - serializer.rb: @param/@return added to serialize_args and serialize_vars, naming the new length:/collection_size: nil semantics added by this PR. Co-Authored-By: Claude <noreply@anthropic.com>
Code comments in the capture-expressions PR referenced design docs in
an internal planning repo (paths like projects/capture-expressions/design/decisions.md)
and used bare decision identifiers (D1, D3, D4, D5) that only resolve
inside that planning repo. The references would 404 for any
DataDog/dd-trace-rb reader.
For each occurrence, either the surrounding text already conveyed the
intent (drop the "see <private-path>" sentence) or the cross-tracer
comparison was sourced inline (Python/Java/Go DI) so the reference was
redundant. No information loss for someone reading only the public
source.
Files updated:
- capture_expression_evaluator.rb class docstring (two paths + "see D4")
- probe.rb rate_limit comment ("planning repo" sentence)
- probe_builder.rb mutual-exclusion comment (one path + "(D1)")
- probe_notification_builder.rb mutual-exclusion comment (one path + "(D1)")
- probe_notification_builder.rb evaluation-errors merge comment (one path)
Co-Authored-By: Claude <noreply@anthropic.com>
…n_spec Replace `untyped` placeholders in the capture-expressions RBS sigs with the concrete types already established in the rest of DI: - settings: Datadog::Core::Configuration::Settings (was untyped on CaptureExpressionEvaluator, CaptureLimits.resolve, Probe#snapshot_serializer_limits) - logger: DI::Logger (was untyped on CaptureExpressionEvaluator, ProbeNotificationBuilder; Component#initialize wraps the Core::Logger into DI::Logger before passing it down, matching Instrumenter / ProbeManager sigs) - telemetry: Core::Telemetry::Component (was untyped on CaptureExpressionEvaluator and ProbeNotificationBuilder; the earlier fix commit already made the constructors non-nil-required, so the sigs drop the trailing `?`) Steep still passes on lib/datadog/di/ after the tightening. Also remove a trailing blank line in spec/datadog/di/capture_expression_spec.rb flagged by Layout/TrailingEmptyLines on CI. Co-Authored-By: Claude <noreply@anthropic.com>
…nature
The earlier commit that made `logger` a positional arg and `telemetry:`
a required kwarg on ProbeNotificationBuilder updated the unit spec but
missed this integration spec, which uses `described_class.new` rather
than `ProbeNotificationBuilder.new` (so the earlier grep didn't catch it).
All 6 examples were failing with:
ArgumentError: wrong number of arguments (given 2, expected 3;
required keyword: telemetry)
Adds logger/telemetry doubles via the same as_null_object pattern used
in the unit spec.
Co-Authored-By: Claude <noreply@anthropic.com>
…tional telemetry
Adds five integration tests covering the customer-observable behaviors
the PR claims, which were previously only verified at unit level:
1. method probe mutual exclusion — both captureSnapshot=true and
captureExpressions set on the probe; asserts the snapshot carries
the `arguments` block and `captureExpressions` is absent from both
entry and return blocks.
2. method probe timeout — sets max_time_to_serialize_ms=0; asserts
captureExpressions = {"arg1" => {notCapturedReason: "timeout"}} on
both entry and return.
3. method probe with raising expression — uses len(undefined) which
raises ExpressionEvaluationError; asserts the key is omitted from
captureExpressions and an entry appears in the snapshot's top-level
evaluationErrors array.
4. line probe with capture expression — uses the existing line-probe
integration test class; asserts captures.lines[40].captureExpressions
contains the resolved local variable value.
Writing these tests surfaced a regression from the earlier
"make telemetry required" commit: Component.build(...) legitimately
accepts a nil telemetry kwarg and threads nil through to
ProbeNotificationBuilder, which then constructed CaptureExpressionEvaluator
with telemetry: nil. The earlier fix removed the &. guards on the two
telemetry call sites (timeout inc, rescue report), causing NoMethodError
in the integration tests where no telemetry component is configured.
Restores telemetry as a truly-optional dependency on both
CaptureExpressionEvaluator and ProbeNotificationBuilder:
- `telemetry: nil` default
- `telemetry&.inc(...)` / `telemetry&.report(...)` at the call sites
- RBS sigs `Core::Telemetry::Component?` (nullable)
- attr_reader doc comments name the nil case and how it is handled
The unit specs (which pass `instance_double(...).as_null_object`) are
unaffected — &. on a non-nil null-object still dispatches.
Co-Authored-By: Claude <noreply@anthropic.com>
8b413eb to
1bf6c26
Compare
The capture_expression_evaluator reads settings.dynamic_instrumentation.max_time_to_serialize_ms but the RBS interface for the dynamic_instrumentation namespace did not declare it, so steep flagged it as Ruby::NoMethod on lib/datadog/di/capture_expression_evaluator.rb:69. The option is defined in lib/datadog/di/configuration.rb as type :int with default 200, matching the Integer signature used for the other max_* options in the interface. Verified locally: bundle exec steep check lib/datadog/di/capture_expression_evaluator.rb reports no errors.
Capture expressions are evaluated once, at method return — the only point where args, kwargs, self, @return, and exception are all observable. The previous code emitted the return-time captured_block under BOTH the entry and return keys of the snapshot's captures map. The snapshot schema's entry/return split exists for captureSnapshot, which freezes args before super (entry) and reads self/@return after (return). For captureExpressions there is no entry-time evaluation, so putting the return-time hash under the entry key mislabeled exit-time data as entry-time state. A customer expression like `arg1` on a method that mutates its argument would appear under entry with the post-mutation value — directly contradicting what entry means under captureSnapshot. Stop emitting the entry block for method-probe captureExpressions. Line-probe captureExpressions are unchanged (single block, no entry/return distinction). captureSnapshot is unchanged. Tests updated to assert the entry key is absent for method probes with capture_expressions only. Verified locally: bundle exec rspec spec/datadog/di/probe_notification_builder_spec.rb bundle exec rspec spec/datadog/di/integration/instrumentation_spec.rb bundle exec steep check lib/datadog/di/probe_notification_builder.rb
…rd combine_args Three follow-ups from the capture-expressions review: 1. Telemetry counter renamed from `capture_expression_timeout` to `capture_expressions_skipped_by_timeout`. The metric is per-expression (a single probe fire with N timed-out expressions increments by N). The new name reflects the unit. Comment added at the increment site. 2. New test in capture_expression_evaluator_spec exercising the realistic mid-loop case: one expression evaluates successfully, then the deadline check at the top of the next iteration fires. The existing "exhausted before evaluating any" context (budget = 0) only covered the trivial degenerate case. The new test stubs Process.clock_gettime with and_wrap_original so non-:nanosecond callers (Time.now via :float_second) still hit the real impl. 3. Skip serializer.combine_args in the method-probe return path when captureSnapshot is also set on the probe. Mutual exclusion means the combined hash is discarded at build time; the call was wasted work. The guard now mirrors the build-time precedence. Verified locally: bundle exec rspec spec/datadog/di/capture_expression_evaluator_spec.rb bundle exec rspec spec/datadog/di/integration/instrumentation_spec.rb bundle exec rspec spec/datadog/di/probe_notification_builder_spec.rb bundle exec steep check lib/datadog/di/instrumenter.rb lib/datadog/di/capture_expression_evaluator.rb All green.
Capture expressions on method probes now respect the probe's evaluateAt field, matching Python, .NET, and PHP. Before this commit the value was parsed off the RC payload but ignored: every capture-expression block went under captures.return, irrespective of evaluateAt. Probe.evaluate_at — new attribute. Accepts :entry / :exit / nil; nil coerces to :exit (matching the libdatadog ProbeCommon JSON-parse default of EvaluateAt::Exit). Unknown symbols raise ArgumentError at construction. Line probes ignore the value (single firing point at the TracePoint callback). ProbeBuilder.parse_evaluate_at — new helper. Maps the RC payload's 'ENTRY' / 'EXIT' strings to :entry / :exit. 'DEFAULT' (sent by the Java backend tooling) maps to :exit. Absent or unrecognized values coerce to :exit and emit a debug log; the runtime never raises on an unknown evaluateAt because such payloads should still install as conventional EXIT-timed probes. Instrumenter#hook_method — for an evaluate_at: :entry method probe, evaluate the capture expressions against the pre-super scope (self / args / kwargs only, since @return / @duration / @exception are nil at this point) and stash the result on the exit-time Context. Mirrors the existing serialized_entry_args pattern. Context — new entry_capture_expressions and entry_capture_evaluation_errors fields, populated by the entry hook and consumed by ProbeNotificationBuilder. ProbeNotificationBuilder#build_snapshot — for method probes: evaluate_at: :entry emits under captures.entry only, consuming the pre-computed block from Context (does not re-evaluate against exit-time scope); evaluate_at: :exit (default) emits under captures.return only, evaluating against exit-time scope. Never both. Line probes emit under captures.lines.<n>.captureExpressions as before; evaluateAt is N/A there. Specs: Probe — evaluate_at keyword acceptance, default coercion, and ArgumentError on unknown values. ProbeBuilder — evaluateAt parsing for ENTRY / EXIT / DEFAULT / absent / unknown. ProbeNotificationBuilder — evaluate_at: :entry consumes the pre-computed block, merges entry-time evaluationErrors, emits empty {} when nothing was stashed. Integration — uses InstrumentationSpecTestClass#mutating_method to exercise the entry-vs-exit distinction: an :entry probe captures the pre-mutation arg value, an :exit probe captures the post-mutation value, demonstrating the timing semantics end-to-end. Steep clean. 860 / 860 DI specs pass (8 pre-existing pending).
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.
| # @param logger [Datadog::Core::Logger] logger forwarded to the internal | ||
| # CaptureExpressionEvaluator for per-expression evaluation failures. |
| attr_reader :serializer | ||
|
|
||
| # Logger; passed through to CaptureExpressionEvaluator at construction. | ||
| # @return [Datadog::Core::Logger] |
| # Lazily-constructed sub-component that evaluates probe.capture_expressions | ||
| # during build_snapshot when probe.capture_expressions? is true. |
| # @param settings [Datadog::Core::Configuration::Settings] | ||
| # @param serializer [Datadog::DI::Serializer] | ||
| # @param logger [Datadog::Core::Logger] | ||
| # @param telemetry [Datadog::Core::Telemetry::Component, nil] nil is |
| # Logger used for debug-level reporting of per-expression evaluation | ||
| # failures. | ||
| # @return [Datadog::Core::Logger] | ||
| attr_reader :logger |
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".
| entry_context = Context.new( | ||
| probe: probe, settings: settings, serializer: serializer, | ||
| target_self: self, | ||
| locals: serializer.combine_args(args, kwargs, self), | ||
| ) |
There was a problem hiding this comment.
Make @duration nil-safe for entry capture expressions
For method probes with evaluateAt: ENTRY, any capture expression that references @duration (even isUndefined(@duration)) is evaluated against this entry context before a duration exists. The EL compiler expands @duration to context.duration * 1000, so the nil duration raises and the capture key is omitted with an evaluation error instead of behaving like the documented nil/undefined entry-time value. Please either make the @duration expansion nil-safe or avoid evaluating it in the entry context.
Useful? React with 👍 / 👎.
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:
Capture expressions are already shipped in Python, Java, .NET, Node.js, PHP, and Go DI. Ruby was the gap. This PR closes that gap so the Live Debugger UI can offer the feature on Ruby services once the corresponding
dd-sourceversion_matrix.gorow is updated.Behavior choices for Ruby (cross-tracer comparison drove each pick):
captureSnapshotandcaptureExpressionsare set, matching Python/Java/Go.capturefallback: per-fieldexpr ?? probe ?? settings, matching Node.js (most ergonomic).sampling.snapshotsPerSecondon the probe.DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE. Remaining expressions emit a{notCapturedReason: "timeout"}stub so the truncation is visible in the snapshot UI.{ expr: <name>, message }appended to the snapshot'sevaluationErrorsarray; the failed key is omitted fromcaptureExpressions.captureExpressionsis emitted undercaptures.entrywhen the probe'sevaluateAtisENTRY, and undercaptures.returnotherwise (defaultEXIT, matching the libdatadog ProbeCommon JSON-parse default that PHP also shares). The evaluator runs once per fire — at the entry hook against the pre-super scope (args / kwargs / self only;@return/@duration/@exceptionare nil there) forevaluateAt: ENTRY, and at the exit hook against the full exit-time scope for the default. Matches Python, .NET, and PHP.All four per-field limits (
maxReferenceDepth,maxFieldCount,maxCollectionSize,maxLength) flow through the documentedexpr → probe → settingsfallback chain end-to-end for capture expressions. As part of this work the same four probe-level overrides now also flow through the snapshot serialization path (entry args,@return,self, and line-probe locals), where probe-levelmaxLength/maxCollectionSizewere previously silently dropped and theselfserialization had ignored all four overrides.Change log entry
Yes. Dynamic Instrumentation log probes now support capture expressions — named expressions whose values are emitted in the snapshot under
captureExpressions, as an alternative to capturing the full local/argument scope. Also fixes probe-level capture-limit overrides (maxLength,maxCollectionSize,maxFieldCount,maxReferenceDepth) being silently dropped when serializing entry args,self,@return, and line-probe locals undercaptureSnapshot.