Skip to content

DEBUG-5433 DI: implicit enablement via remote configuration#5525

Merged
p-datadog merged 51 commits into
masterfrom
enablement
Jun 29, 2026
Merged

DEBUG-5433 DI: implicit enablement via remote configuration#5525
p-datadog merged 51 commits into
masterfrom
enablement

Conversation

@p-datadog

@p-datadog p-datadog commented Mar 28, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Restructures the Dynamic Instrumentation lifecycle to support implicit enablement via Remote Configuration. The DI component is now always built when not explicitly disabled (DD_DYNAMIC_INSTRUMENTATION_ENABLED=false) and can be started either by the env var set to true or by a remote-config signal at runtime.

Motivation

We want to reduce customer friction by making creating a probe in the Datadog UI turn DI on automatically — customers should not have to set DD_DYNAMIC_INSTRUMENTATION_ENABLED=true and restart their service first.

Change log entry

Yes. Dynamic Instrumentation can now be enabled from the Datadog UI without setting DD_DYNAMIC_INSTRUMENTATION_ENABLED or restarting the application. Setting DD_DYNAMIC_INSTRUMENTATION_ENABLED=false explicitly still disables DI and blocks UI-driven enablement.

Architecture

  • Component#start! / #stop! / #started? — runtime lifecycle, serialized by an internal mutex so concurrent RC callbacks cannot race.
  • ProbeManager#stop / #reopen — wraps the definition trace point's disable/enable so the component can re-enter the same instance across stop/start cycles.
  • Component.build now succeeds whenever settings respond to dynamic_instrumentation (no longer requires enabled). When a precondition is unmet (RC disabled, MRI required, Ruby 2.6+ required, dev environment, C extension absent), the build returns nil. Log level mirrors customer intent: warn when DD_DYNAMIC_INSTRUMENTATION_ENABLED=true was explicitly set, otherwise debug — implicit-enablement customers receive their warn from Remote.handle_rc_enablement when the RC signal lands on a nil component.
  • Components#startup! starts DI if either settings.dynamic_instrumentation.enabled is true (env-var path) or old_state.di_implicitly_enabled? is true (RC-driven enablement carried across a component rebuild via ComponentsState).
  • Datadog::DI::Remote.handle_rc_enablement(enabled) is the entry point for the RC-driven path. It's invoked from Datadog::Tracing::Remote when an APM_TRACING payload contains dynamic_instrumentation_enabled. Precedence: explicitly_disabled? (env var set to false) blocks RC enablement.
  • New APM_TRACING capability bit 38 (APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION) declared in Tracing::Remote.capabilities so the backend knows the tracer can be enabled this way.
  • DI::Remote.receivers now gates change processing on component.started? — probe changes received while DI is stopped are dropped. The next RC poll re-delivers the current backend state, so probes added in the UI before DI started arrive on a subsequent poll once start! is called.

How to test the change?

Use Debugger UI to set a probe on a running Ruby service. Requires https://github.com/DataDog/dd-go/pull/241136 + https://github.com/DataDog/web-ui/pull/286651 + tracer version faked to 2.37.0 (next planned release).

2026-06-08_16-46-47

@p-datadog p-datadog added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Mar 28, 2026
@github-actions github-actions Bot added the core Involves Datadog core libraries label Mar 28, 2026
@github-actions

github-actions Bot commented Mar 28, 2026

Copy link
Copy Markdown

Thank you for updating Change log entry section 👏

Visited at: 2026-05-19 23:34:11 UTC

@github-actions

github-actions Bot commented Mar 28, 2026

Copy link
Copy Markdown

Typing analysis

Note: Ignored files are excluded from the next sections.

steep:ignore comments

This PR introduces 7 steep:ignore comments, and clears 6 steep:ignore comments.

steep:ignore comments (+7-6)Introduced:
lib/datadog/core/telemetry/event/app_started.rb:145
lib/datadog/core/telemetry/event/app_started.rb:151
lib/datadog/di/remote.rb:156
lib/datadog/di/remote.rb:157
lib/datadog/di/remote.rb:162
lib/datadog/di/remote.rb:163
lib/datadog/di/remote.rb:165
Cleared:
lib/datadog/core/telemetry/event/app_started.rb:138
lib/datadog/core/telemetry/event/app_started.rb:144
lib/datadog/di/remote.rb:45
lib/datadog/di/remote.rb:49
lib/datadog/di/remote.rb:50
lib/datadog/di/remote.rb:52

Untyped methods

This PR introduces 8 partially typed methods, and clears 3 untyped methods and 6 partially typed methods. It increases the percentage of typed methods from 65.54% to 65.77% (+0.23%).

Untyped methods (+0-3)Cleared:
sig/datadog/core/configuration/components_state.rbs:9
└── def initialize: (telemetry_enabled: untyped, remote_started: untyped) -> void
sig/datadog/core/configuration/components_state.rbs:11
└── def telemetry_enabled?: () -> untyped
sig/datadog/core/configuration/components_state.rbs:13
└── def remote_started?: () -> untyped
Partially typed methods (+8-6)Introduced:
sig/datadog/di/component.rbs:70
└── def parse_probe_spec_and_notify: (Hash[String, untyped] probe_spec) -> Probe
sig/datadog/di/remote.rbs:22
└── def self.parse_content: (untyped content) -> Hash[String, untyped]
sig/datadog/di/remote.rbs:25
└── def self.probe_known?: (untyped probe_id, Component component) -> bool
sig/datadog/di/remote.rbs:26
└── def self.probe_in_content_known?: (untyped content, Component component) -> bool
sig/datadog/di/remote.rbs:28
└── def self.add_probe: (untyped content, Component component) -> void
sig/datadog/di/remote.rbs:29
└── def self.remove_probe: (untyped previous_content, Component component) -> void
sig/datadog/di.rbs:14
└── def self.exception_message: (Exception exception) -> untyped
sig/datadog/tracing/remote.rbs:11
└── def self.process_config: (Hash[String, untyped] config, Core::Remote::Configuration::Content content, ?Core::Remote::Configuration::Repository? repository) -> void
Cleared:
sig/datadog/di/component.rbs:60
└── def parse_probe_spec_and_notify: (Hash[String, untyped] probe_spec) -> Probe
sig/datadog/di/remote.rbs:16
└── def self.parse_content: (untyped content) -> Hash[String, untyped]
sig/datadog/di/remote.rbs:18
└── def self.add_probe: (untyped content, Component component) -> void
sig/datadog/di/remote.rbs:19
└── def self.remove_probe: (untyped previous_content, Component component) -> void
sig/datadog/di.rbs:12
└── def self.exception_message: (Exception exception) -> untyped
sig/datadog/tracing/remote.rbs:11
└── def self.process_config: (Hash[String, untyped] config, Core::Remote::Configuration::Content content) -> void

Untyped other declarations

This PR clears 2 untyped other declarations. It increases the percentage of typed other declarations from 82.63% to 82.77% (+0.14%).

Untyped other declarations (+0-2)Cleared:
sig/datadog/core/configuration/components_state.rbs:5
└── @telemetry_enabled: untyped
sig/datadog/core/configuration/components_state.rbs:7
└── @remote_started: untyped

If you believe a method or an attribute is rightfully untyped or partially typed, you can add # untyped:accept on the line before the definition to remove it from the stats.

@pr-commenter

pr-commenter Bot commented Mar 28, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-06-25 16:21:04

Comparing candidate commit 72bfce8 in PR branch enablement with baseline commit fd1e1b0 in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 48 metrics, 1 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Mar 28, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 41.30%
Overall Coverage: 89.96% (-0.05%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 72bfce8 | Docs | Datadog PR Page | Give us feedback!

@dd-octo-sts dd-octo-sts Bot added the debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database) label May 19, 2026
@p-datadog p-datadog changed the title [WIP] enablement DI: implicit enablement via remote configuration May 19, 2026
@p-datadog
p-datadog requested a review from Copilot June 10, 2026 14:52
@p-datadog

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb6e3bd060

ℹ️ 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".

Comment thread lib/datadog/di/remote.rb
Comment thread lib/datadog/core/configuration/components.rb Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restructures the Dynamic Instrumentation (DI) lifecycle to support implicit enablement via Remote Configuration (RC): the DI component can now be built in a stopped state and later started/stopped at runtime based on an RC enablement signal, without requiring DD_DYNAMIC_INSTRUMENTATION_ENABLED=true at boot.

Changes:

  • Adds a runtime lifecycle to DI (start!, stop!, started?) and wires RC-driven enablement/disablement through Datadog::Tracing::RemoteDatadog::DI::Remote.handle_rc_enablement.
  • Declares a new APM_TRACING capability bit (bit 38) for DI implicit enablement and updates RC capabilities/product registration logic accordingly.
  • Adds/updates unit + integration specs, RBS signatures, and docs to cover implicit enablement, restart behavior, and probe coverage across enablement timing.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
spec/datadog/tracing/remote_spec.rb Updates expectations to include DI enablement capability bit 38.
spec/datadog/di/remote_spec.rb Adds coverage for .handle_rc_enablement and .explicitly_disabled?, and updates receiver gating expectations.
spec/datadog/di/remote_integration_spec.rb Starts DI component in setup to reflect new lifecycle gating.
spec/datadog/di/probe_manager_spec.rb Updates TracePoint construction expectations and adds reopen coverage.
spec/datadog/di/integration/probe_coverage_spec.rb New integration matrix validating line/method probes across enablement timing.
spec/datadog/di/integration/probe_coverage_method_pre_target_class.rb Fixture for method-probe “pre-enablement load” case.
spec/datadog/di/integration/probe_coverage_method_post_target_class.rb Fixture for method-probe “post-enablement load” case.
spec/datadog/di/integration/probe_coverage_line_pre_target_class.rb Fixture for line-probe “pre-enablement load” case.
spec/datadog/di/integration/probe_coverage_line_post_target_class.rb Fixture for line-probe “post-enablement load” case.
spec/datadog/di/integration/instrumentation_spec.rb Starts DI component in setup to ensure probes install under new lifecycle.
spec/datadog/di/integration/implicit_enablement_spec.rb New end-to-end in-process integration spec for RC enable/disable behavior.
spec/datadog/di/integration/everything_from_remote_config_spec.rb Starts DI component so DI receiver processes RC changes.
spec/datadog/di/component_spec.rb Updates build semantics + adds lifecycle (start/stop/idempotence/thread) coverage.
spec/datadog/di/code_tracker_spec.rb Adds regression for code-tracker registry persistence across stop/start.
spec/datadog/di_spec.rb Adds .unsupported_reason as shared precondition source with specs.
spec/datadog/core/telemetry/integration/telemetry_spec.rb Adjusts DI fakes to satisfy new startup lifecycle calls (start!, started?).
spec/datadog/core/remote/client/capabilities_spec.rb Updates base64 capabilities and DI/SymDB registration expectations.
spec/datadog/core/configuration/components_state_spec.rb New unit spec for ComponentsState#di_implicitly_enabled?.
spec/datadog/core/configuration/components_spec.rb Updates DI component expectations (built-but-stopped), adds state/startup carry-over coverage.
sig/datadog/di/remote.rbs Adds signatures for handle_rc_enablement and explicitly_disabled?.
sig/datadog/di/probe_manager.rbs Adds stop/reopen signatures.
sig/datadog/di/component.rbs Adds lifecycle fields and start!/stop!/started? signatures; adds explicitly_enabled?.
sig/datadog/di.rbs Adds signature for .unsupported_reason.
sig/datadog/core/configuration/settings.rbs Adds signature for using_default? on DI settings.
sig/datadog/core/configuration/components_state.rbs Extends state with di_implicitly_enabled? and tightens types.
lib/datadog/tracing/remote.rb Declares bit 38 and invokes DI RC enablement handler on APM_TRACING payload.
lib/datadog/di/remote.rb Implements RC-driven enable/disable entry point and gates probe processing by started?.
lib/datadog/di/probe_notifier_worker.rb Resets stop flag on start to support stop/start cycles.
lib/datadog/di/probe_manager.rb Switches to TracePoint.new and adds reversible stop/reopen APIs.
lib/datadog/di/component.rb Always-build semantics (when settings present), adds lifecycle mutex + start/stop/started?.
lib/datadog/di.rb Adds .unsupported_reason helper for consistent precondition messaging.
lib/datadog/core/telemetry/event/app_started.rb Reports DI enabled based on started? rather than component presence.
lib/datadog/core/remote/client/capabilities.rb Registers DI RC products/receivers whenever DI settings exist (no longer gated by enabled flag).
lib/datadog/core/configuration/components.rb Starts DI during startup based on env-var or carried RC state; captures runtime state into ComponentsState.
lib/datadog/core/configuration/components_state.rb Adds di_implicitly_enabled storage and accessor.
docs/DynamicInstrumentation.md Documents env-var vs UI-driven implicit enablement and precedence rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/datadog/core/remote/client/capabilities.rb Outdated
p-datadog pushed a commit that referenced this pull request Jun 10, 2026
…al, doc fixes

Apply review fixes:

- handle_rc_enablement now calls Datadog.send(:components, allow_initialization: false)
  in both lookup sites (lookup + rescue telemetry). The RC dispatch runs on the
  remote-config thread; the default allow_initialization: true would synchronously
  build the component tree from a callback context if a signal arrives before
  Components#initialize completes.

- Trailing commas added on the two multi-line Datadog.logger.warn calls in
  lib/datadog/di/remote.rb. lib/datadog/di/ requires trailing commas per
  rules/dd-trace-rb.md.

- Replace build-dependency skip guards (`skip 'Test requires DI C extension'
  unless Datadog::DI.respond_to?(:exception_message)`) with stub-based tests
  that exercise the Components#state / @environment_logger_extra logic
  without requiring the DI C extension. The tests' subject is the Components
  integration with DI lifecycle, not DI's internals — `instance_double` with
  a state-aware double for the startup! transitions covers the same surface.
  Applies to spec/datadog/core/configuration/components_spec.rb (4 sites)
  and spec/datadog/di_spec.rb (1 site).

- Remove the Thread.pass polling loop in the
  'spawns a background thread on start! and reaps it on stop!' test.
  ProbeNotifierWorker#stop calls thread.join (or thread.kill on timeout),
  so by the time component.stop! returns the worker thread is gone — the
  polling was defensive against a deterministic operation and resembled
  the sleep antipattern in disguise.

- Update the DD_DYNAMIC_INSTRUMENTATION_ENABLED entry in docs/GettingStarted.md
  to describe the new three-valued semantics: true enables at boot, false
  blocks RC enablement, unset allows UI-driven enablement.

- Update the `Datadog.send(:components)` stubs in remote_spec.rb and
  implicit_enablement_spec.rb to match the new allow_initialization: false
  signature.
@p-datadog
p-datadog requested a review from Copilot June 10, 2026 17:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 3 comments.

Comment thread lib/datadog/di/remote.rb
Comment thread lib/datadog/core/configuration/components.rb
Comment thread lib/datadog/di.rb
@p-datadog
p-datadog marked this pull request as ready for review June 10, 2026 17:37
@p-datadog
p-datadog requested review from a team as code owners June 10, 2026 17:37
@p-datadog
p-datadog requested a review from wantsui June 10, 2026 17:37
p-ddsign added 5 commits June 22, 2026 15:24
Commit 5109f21 removed these two guards with the rationale that
"tests now skip on <2.6" — but spec/datadog/core/telemetry/
integration/telemetry_spec.rb is in spec:main (no platform skip)
and stubs Datadog::DI::Component.build to a non-nil fake to
exercise the "DI enabled" code path on Rubys without the C
extension. On Ruby 2.5 that bypasses Component.build's runtime
check; the call then reaches DI.activate_tracking, which is
defined in di/base.rb and only loaded on Ruby >= 2.6.

The previous commit's analysis was correct for production
(Component.build returns nil on 2.5, so the outer guard is
sufficient) and for di_test-using specs (those skip on <2.6).
It missed the cross-component spec that mocks past the platform
check. verify_partial_doubles is enabled, so a test-side
allow(DI).to receive(:activate_tracking) workaround does not work
on Ruby 2.5 — the production guard is the correct fix.

Failing job: Ruby 2.5 / build & test (standard) [0] —
NoMethodError: undefined method `activate_tracking' for
Datadog::DI:Module at components.rb:245.

Verified spec/datadog/di/component_spec.rb (45/45 pass) and
spec/datadog/core/telemetry/integration/telemetry_spec.rb's
"when dynamic instrumentation is fully enabled" example on
Ruby 3.2. Ruby 2.5 not runnable locally (no mise/Docker access);
the restored pattern matches the pre-5109f21277 code that
passed CI on Ruby 2.5 across many runs.
The 'when dynamic instrumentation is fully enabled' context stubs
Datadog::DI::Component.build to a non-nil fake so the enabled-DI
code path runs in CI configurations without the DI C extension.
That stub bypasses Component.build's runtime check, so
Components#startup! reaches Datadog::DI.activate_tracking — which
is loaded only from datadog/di/boot.rb under
RUBY_VERSION >= '2.6' && RUBY_ENGINE != 'jruby' (lib/datadog.rb:19).
On Ruby 2.5 the method is undefined, and the spec fails with
NoMethodError.

'DI fully enabled' is a state that cannot exist on Ruby 2.5 in
production (DI requires MRI 2.6+, per DI.unsupported_reason). Skip
the context on Ruby <2.6, matching the existing pattern in
spec/datadog/core/configuration/components_spec.rb:797.

This is the test-side fix for the failure that 51eec38 patched
in production. The production-side workaround was reverted at
51c1fef because it added respond_to?(:activate_tracking) guards
to lib/datadog/core/configuration/components.rb:245 and
lib/datadog/di/remote.rb:77 — guards that are dead in production
(the outer 'if dynamic_instrumentation' already short-circuits on
every platform where activate_tracking could be undefined) and
exist only to make this fixture work on Ruby 2.5. The fix belongs
in the fixture.
The 'Tracing before DI' registration order in
lib/datadog/core/remote/client/capabilities.rb is load-bearing: on a
combined RC dispatch (LIVE_DEBUGGING + APM_TRACING in one transaction),
the Tracing receiver must run first so handle_rc_enablement starts DI
before the DI receiver processes the probe change. The end-to-end
behavior is covered by the combined-RC-transaction test in
spec/datadog/di/integration/implicit_enablement_spec.rb, but that test
exercises the order indirectly through dispatcher behavior.

Add a direct unit assertion on capabilities.receivers order so a future
refactor that reorders registrations is caught at the registration site
rather than via a multi-step integration failure.
Two comment-density tightenings flagged in PR review (non-blocking):

- lib/datadog/core/remote/client/capabilities.rb: the registration-order
  paragraph went from 10 lines to 7. Keeps the constraint (Tracing
  registers before DI) and the silent-drop consequence; drops the
  step-by-step retelling. The full mechanism is regression-tested by
  spec/datadog/di/integration/implicit_enablement_spec.rb's
  combined-payload case.

- lib/datadog/core/configuration/components.rb #state: the using_default?
  paragraph went from 9 lines to 6. Keeps the unique rationale (why
  using_default? rather than !enabled — the Datadog.configure-mutation
  timing), drops the worked example. The "vs default_precedence?"
  rationale lives at DI::Component#explicitly_enabled?; this site's
  rationale is the orthogonal "vs !enabled" choice.
p-ddsign and others added 11 commits June 22, 2026 15:39
The skip guard added in b1ed2d6 used before(:each), but the outer
`before do` block at telemetry_spec.rb:570 runs Datadog.configure
first — and configure reaches DI.activate_tracking before the inner
before(:each) hook fires. On Ruby 2.5 the skip never ran, the
configure crashed, and `Ruby 2.5 / build & test (standard) [0]` kept
failing with NoMethodError.

before(:all) runs ahead of all before(:each) hooks in the group, so
the skip lands before the crash. Companion guard at
components_spec.rb:797 already uses before(:all) — this matches that.

Verified by reading RSpec hook ordering: before(:all) → outer
before(:each) → inner before(:each).
The outer rescue in handle_rc_enablement catches an exception from the
body (e.g. component.start! raising) and then attempts to report it via
`Datadog.send(:components, allow_initialization: false)&.telemetry&.report`.

If the components lookup itself raises (extremely unlikely in steady
state, but possible during teardown races or during early boot before
Components#initialize completes), the rescue body propagates out of
handle_rc_enablement \u2014 defeating the DI contract that DI never raises
into Tracing::Remote.process_config or further into the RC dispatch.

Wrap the telemetry lookup+report in a defensive begin/rescue that
swallows any secondary exception and logs at debug. Add a regression
test that stubs Datadog.send to succeed once then raise, and asserts
handle_rc_enablement does not propagate.

Verified: bundle exec rspec spec/datadog/di/remote_spec.rb (locally,
syntax check only; full run gated on Ruby 2.6+ MRI with C ext).
Commit ea10a35 added an inner begin/rescue around the telemetry
lookup in handle_rc_enablement's rescue clause, plus a test that
simulated the inner call raising.

Tracing both call sites shows neither can actually raise in this
codebase:

- Datadog.send(:components, allow_initialization: false) with
  allow_initialization: false always returns the (possibly nil/false)
  @components value via the COMPONENTS_READ_LOCK.synchronize block.
  No raise path on this branch.
  (lib/datadog/core/configuration.rb:196)

- telemetry.report(e, description:) does string-building from
  exception.class.name/inspect (safe on every Exception), then
  log!(event) which short-circuits on enabled? && @log_collection_enabled
  and otherwise enqueues to the worker. No raise path under realistic
  conditions; the only theoretical path is a customer Exception
  subclass overriding #backtrace to raise, which does not apply here
  because e originates from DI's own code.
  (lib/datadog/core/telemetry/logging.rb:51-69)

The guard was defending against scenarios that don't exist. Remove
the rescue and the simulation test; the outer rescue at line 82 is
sufficient.
…eason

The project-established pattern for Ruby version checks is the
Datadog::RubyVersion helper:

  lib/datadog/di/base.rb:50 -- RubyVersion.is?('>= 2.6')
  lib/datadog.rb:22         -- Datadog::RubyVersion.is?('>= 2.6')

The newly-added DI.unsupported_reason used a bare `RUBY_VERSION < '2.6'`,
which works but diverges from the helper. RubyVersion captures the
running version once as Gem::Version (handles the 3.2.10 < 3.2.3
string-comparison bug) and is the standard idiom.

Switch to Datadog::RubyVersion.is?('< 2.6').

Test impact: the tests that stub_const('RUBY_VERSION', ...) to
exercise the version branch must also stub
Datadog::RubyVersion::CURRENT_RUBY_VERSION, which is the cached
Gem::Version captured at module load time. Established pattern in
spec/datadog/core/configuration/settings_spec.rb:907-908 etc.

Updated stubs:
  - spec/datadog/di_spec.rb: 'all preconditions met',
    'Ruby older than 2.6', 'C extension is not loaded' contexts
  - spec/datadog/di/component_spec.rb: 'Ruby 2.6+ required, mocked'
    context

The error message still interpolates RUBY_VERSION (not CURRENT_RUBY_VERSION),
so both stubs are needed: one for the gate, one for the message.
The four 'returns nil and logs at debug only' tests in component_spec
asserted `expect(logger).to receive(:debug)` without a .with matcher.
That passes for any debug call, including an empty one \u2014 the test
verifies that debug was called and warn was not, but not that the
specific build-precondition reason landed in the log.

The companion 'with DD_DYNAMIC_INSTRUMENTATION_ENABLED explicitly true'
tests in each context already match the warn message content. The
debug counterparts should match the same content, since production
code emits the same string at either level
(logger.public_send(level, 'di: dynamic instrumentation is disabled: <reason>')).

Tighten each debug assertion with a .with(a_string_matching(...))
matcher that pins the specific reason:

  - 'Remote Configuration is not enabled' (RC disabled)
  - 'MRI is required' (non-MRI engine)
  - 'Ruby 2.6+ is required' (Ruby <2.6)
  - 'C extension is not available' (C ext absent)

Also rename each test to name the specific reason instead of the
generic 'logs at debug only'.
The context 'when DI component is started via RC (settings.enabled
false)' did not actually drive RC \u2014 it stubbed started?: true and
left settings.dynamic_instrumentation.enabled at default. The
combination (started + using_default? on :enabled) is exactly what
production code reads to compute di_implicitly_enabled, so the
assertion was correct, but the 'via RC' phrasing overstated the
scope: a unit test against #state cannot, by construction, drive
through Remote.handle_rc_enablement.

Rename both sibling contexts to describe the property under test
(env-var explicit set vs customer never touched the env var) rather
than the upstream driver. Add explanatory comments tying each context
to the production path it models.

No test logic changed.
The component_spec covered the sequential lifecycle (start!/stop!,
idempotence, restart) but not the @lifecycle_mutex's role in
serializing concurrent calls. The mutex comment in component.rb
claimed serialization across start!/stop!/shutdown! \u2014 the claim
was not test-protected.

The behavioral guarantee (two threads calling lifecycle methods
concurrently are serialized) is Ruby's Mutex#synchronize contract,
which we do not re-test here. The mechanism guarantee (each lifecycle
method actually wraps its body in @lifecycle_mutex.synchronize) is
what this PR claims and what these tests verify. A future refactor
that drops the synchronize call from one of the three methods would
be caught at this layer; Ruby's mutex then handles the rest.

Three mechanism tests added, one per lifecycle method:
  - start! acquires @lifecycle_mutex
  - stop! acquires @lifecycle_mutex
  - shutdown! acquires @lifecycle_mutex

Lifecycle method bodies are stubbed so the assertion targets the
mutex acquisition alone; the worker/probe-manager side effects are
already covered by the #start! and #stop! describe above.
process_config runs on the remote-config worker thread. The
telemetry-reporting call at the end used Datadog.send(:components)
with the default allow_initialization: true, which would
synchronously build the entire component tree from the RC worker
thread if @components were nil at dispatch time \u2014 lock acquisition
from the wrong thread, plus the cost of a full tree build on a hot
path.

The realistic exposure is small (the RC client thread is itself part
of the components tree, so dispatch generally cannot precede
initialization), but the asymmetry is real: PR 5525 added
DI::Remote.handle_rc_enablement on the same dispatch path and used
allow_initialization: false correctly. Bring the existing telemetry
call into line.

Switch to:
  Datadog.send(:components, allow_initialization: false)
    &.telemetry&.client_configuration_change!(env_vars)

The &. chain matches the pattern in DI::Remote.handle_rc_enablement.
When components is nil the call is a no-op, which is the right
behavior \u2014 no point reporting a configuration change to a telemetry
component that doesn't exist yet.
Empty line left behind by commit b0fcdb8 when the defensive
rescue test was removed. CI flagged:

  spec/datadog/di/remote_spec.rb:146:1: Layout/EmptyLinesAroundBlockBody:
  Extra empty line detected at block body end.

Trailing blank line removed.
…action under test

The three @lifecycle_mutex serialization tests added in f28d5a8 assert
exactly one synchronize call per lifecycle method:

  expect(mutex).to receive(:synchronize).and_call_original
  component.start!  (or .stop!, .shutdown!)

The after block ran component&.shutdown! for cleanup, which itself calls
@lifecycle_mutex.synchronize. RSpec mock verification runs after the
example's after hooks, so the cleanup's synchronize call was counted
against the expectation — yielding "expected 1 time, received 2 times"
on every Ruby version's batch [0].

Production code (start!, stop!, shutdown!) each calls synchronize
exactly once, as the tests intended to verify. The bug is in the test
fixture, not the production code.

Replace shutdown!-based cleanup with DI.remove_current_component, which
acquires DI::LOCK (a different Mutex on the DI module per
lib/datadog/di/base.rb:21) and does not touch @lifecycle_mutex. The
before block stubs all worker/probe-manager side effects, so no thread
or hook needs stopping — the only state that escapes the example is
the entry that Component.build adds to DI.current_components via
add_current_component.

The expectation remains strict (default "exactly once"); only the
teardown mechanism changes.

Co-Authored-By: Claude <[email protected]>
Addresses review comment from @mabdinur: a LIVE_DEBUGGING probe that
lands in an RC poll while DI is stopped, with the
dynamic_instrumentation_enabled=true signal arriving in a separate later
poll, was dropped and never installed until the customer edited the probe.

Root cause: DI::Remote.receivers ignores LIVE_DEBUGGING changes while the
component is stopped, so the probe never enters the probe repository. When
a later poll flips enablement, Core::Remote::Client#apply_config marks the
unchanged probe content `same` and does not re-dispatch it, and
component.start! -> probe_manager.reopen only replays what is already in
the repository. The existing fixes did not cover this: the Tracing-before-DI
receiver ordering handles the same-poll combined transaction, and the
stop!/reopen repository preservation handles toggling DI off then on after
the probe was already installed.

Fix: on the stopped->started transition, handle_rc_enablement reconciles
against the current LIVE_DEBUGGING contents in the RC repository (threaded
through from Tracing::Remote.process_config) and installs any probe not
already tracked. The DI receiver's :insert path now skips a probe the
reconcile already installed in the same dispatch, so the same-poll combined
transaction does not double-add (which would surface a false errored status).

Verified: new integration test in implicit_enablement_spec drives two real
client.sync polls (probe while stopped, then enable) and asserts the probe
installs; it fails without the fix (installed=0) and passes with it. Full
non-contrib DI suite + tracing remote spec green (862 examples). Steep and
StandardRB clean on changed files.
@p-datadog

Copy link
Copy Markdown
Member Author

@mabdinur Good catch — confirmed with a test, and you've got the mechanism exactly right. The receiver-ordering fix only covers the same-poll combined transaction, and the stop!/reopen repository preservation only covers off→on after the probe was already installed — neither covers a probe that arrived in an earlier poll while DI was stopped.

Fixed in c54c088: the enable path now reconciles against the current LIVE_DEBUGGING contents in the RC repository on the stopped→started transition and installs any probe not already tracked, exactly as you suggested. The DI receiver's :insert path skips a probe the reconcile already installed in the same dispatch, so the combined transaction doesn't double-add (which would otherwise surface a false errored status). Added an integration test that drives two real client.sync polls (probe while stopped, then enable) and checks the probe installs — it fails without the fix (installed=0) and passes with it.

p-datadog and others added 4 commits June 23, 2026 15:24
… RC registration

When DI is explicitly disabled, DI::Component.build returns nil and the
DI RC capability/product/receiver block (including bit 38) is not
registered. Bit 38 moves from Tracing::Remote::CAPABILITIES to
DI::Remote.capabilities so it is gated with the rest of the DI block.
The unset (default) case is unchanged: the component is built stopped
and DI is registered so RC can enable it.

handle_rc_enablement's nil-component branch now distinguishes explicit
disable from unsupported-environment reasons.
Reword four spec comments to drop provenance framing ("raised in PR
review", "codex review scenario", "codex P2 review comment") while
keeping the technical scenario each describes. Process narration belongs
in the PR thread, not in source.
The previous comment claimed the DI component exists "whenever the
runtime supports it". That overstates: Component.build also returns nil
on a supported runtime when DI is explicitly disabled
(DD_DYNAMIC_INSTRUMENTATION_ENABLED=false) or when Remote Configuration
is unavailable. Reword to state the actual build preconditions.
@p-datadog

Copy link
Copy Markdown
Member Author

@p-datadog p-datadog changed the title DI: implicit enablement via remote configuration DEBUG-5433 DI: implicit enablement via remote configuration Jun 29, 2026
@p-datadog
p-datadog merged commit e6cedc1 into master Jun 29, 2026
867 of 870 checks passed
@p-datadog
p-datadog deleted the enablement branch June 29, 2026 20:02
@dd-octo-sts dd-octo-sts Bot added this to the 2.37.0 milestone Jun 29, 2026
p-datadog pushed a commit that referenced this pull request Jun 30, 2026
Resolves the TODO in DI::Remote.products. With always-build (#5525) the
DI remote-config block registers the LIVE_DEBUGGING product and the
implicit-enablement capability (bit 38) whenever DI is not explicitly
disabled — including on JRuby and Ruby 2.5, where DI can never run. The
backend would then send probe configs and enable signals the tracer
silently drops.

Gate the DI registration block on a new platform check,
Datadog::DI.supported_runtime? (MRI on Ruby 2.6+), which is the
platform-support subset of DI.unsupported_reason. It deliberately omits
the C-extension check so behaviour does not depend on a build artifact
that is always present in shipped gems on supported runtimes.

Tests: capabilities_spec gains an unsupported-runtime context (DI not
registered even when enabled); di_spec covers supported_runtime? across
MRI 2.6+, JRuby, and MRI < 2.6.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos core Involves Datadog core libraries debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database) tracing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants