Conversation
…ured When any sampling rule or default rate is configured, Sampler.Builder.forConfig returns a RuleBasedTraceSampler, which delegates to an inner RateByServiceTraceSampler for spans that match no rule. That inner sampler was never registered to receive the rates published by the trace agent: WriterFactory registers the sampler only when it is a RemoteResponseListener, and RuleBasedTraceSampler neither implemented that interface nor forwarded onResponse to its fallback. The fallback therefore stayed at its initial rate of 1.0 for the life of the JVM, keeping every rule miss and stamping _dd.agent_psr=1.0 with the AGENT_RATE mechanism. Remote configuration compounded this: a change to the trace sample rate or sampling rules rebuilds the sampler in CoreTracer.ConfigSnapshot, but the response listener is registered once, against the initial sampler, and is never re-registered, so the rebuilt sampler was disconnected as well. Two changes: - RuleBasedTraceSampler implements RemoteResponseListener and forwards agent rates to its fallback sampler. - A single RateByServiceTraceSampler instance is shared by every sampler the tracer builds. Sampler.Builder.forConfig takes it as an argument, and CoreTracer reuses the instance from the initial sampler whenever remote configuration triggers a rebuild, so rebuilt samplers stay connected to the agent and keep the rates already learned. Go, Node, Python, Ruby and .NET all apply agent rates on a rule miss; Java was the only tracer that did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
|
@DataDog review |
There was a problem hiding this comment.
🟡 Changes recommended
Remote configuration can create an unregistered agent-rate fallback when the tracer starts without a local sampler, so unmatched spans still ignore agent-published rates.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This pull request reconnects Datadog Agent adaptive sampling rates to rule-based samplers and preserves them across remote-configuration rebuilds.
Changes:
- Adds polymorphic
Sampler.agentSampler()delegation. - Registers the resolved agent sampler with
DDAgentApi. - Reuses the agent sampler during sampler rebuilds.
- Adds regression coverage for sampling, writer registration, and remote configuration.
File summaries
| File | Summary |
|---|---|
dd-trace-core/src/test/java/datadog/trace/core/TracingConfigPollerTest.java |
Tests remote-config sampler rebuilding and agent-rate behavior. |
dd-trace-core/src/test/java/datadog/trace/core/DDCoreJavaSpecification.java |
Adds shared rate-response test data. |
dd-trace-core/src/test/java/datadog/trace/common/writer/WriterFactoryTest.java |
Tests agent-sampler listener registration. |
dd-trace-core/src/test/java/datadog/trace/common/sampling/RuleBasedSamplerAgentRatesTest.java |
Tests rule-based forwarding and fallback behavior. |
dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java |
Reuses the agent sampler during configuration rebuilds. |
dd-trace-core/src/main/java/datadog/trace/common/writer/WriterFactory.java |
Registers the delegated agent sampler. |
dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java |
Adds the agent-sampler contract and builder support. |
dd-trace-core/src/main/java/datadog/trace/common/sampling/RuleBasedTraceSampler.java |
Delegates agent-rate handling to its fallback sampler. |
dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java |
Identifies itself as the agent sampler. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
More details
The sampler contract keeps one agent-rate sampler registered and reuses it when remote configuration rebuilds the rule sampler. Read-only source review identifies no concrete regression in the changed paths.
🤖 Datadog Autotest · Commit 297d2d6 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 297d2d6573
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
More details
The change keeps one agent-rate sampler during rule changes. The writer registers that same sampler, so learned rates continue to apply when no rule matches.
🤖 Datadog Autotest · Commit 297d2d6 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
…logic Adds Sampler.agentSampler() so any composite sampler can expose the instance that receives agent-published rates, replacing CoreTracer's hardcoded instanceof cascade. WriterFactory now registers that instance directly with DDAgentApi instead of RuleBasedTraceSampler forwarding RemoteResponseListener calls to its fallback. Also collapses duplicate null-default and rate-map-building logic across Sampler.Builder, RuleBasedTraceSampler, and test helpers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
297d2d6 to
616f4a7
Compare
What Does This Do
Reconnects the Datadog Agent's adaptive sampling rates to the tracer when sampling rules are configured.
Introduces
Sampler.agentSampler()— a default interface method returning theRateByServiceTraceSamplerinstance that should receive agent-published rates (nullif the sampler doesn't use them):RateByServiceTraceSampler.agentSampler()returnsthis— it's its own source of agent rates.RuleBasedTraceSampler.agentSampler()delegates to its fallback sampler'sagentSampler()(if the fallback is aSampler), so agent rates keep reaching the innerRateByServiceTraceSamplerit wraps.WriterFactoryregisters whichever samplersampler.agentSampler()returns withDDAgentApi, instead of checkingsampler instanceof RemoteResponseListener.RuleBasedTraceSamplerno longer needs to implementRemoteResponseListenerand forwardonResponseitself.CoreTracercapturessampler.agentSampler()once at construction and reuses that same instance on every sampler rebuild (Sampler.Builder.forConfig(config, traceConfig, agentSampler)), replacing the oldinstanceof RateByServiceTraceSampler/instanceof RuleBasedTraceSamplercascade (agentSamplerOf) with a single polymorphic call.Motivation
When any sampling rule or a default rate is configured,
Sampler.Builder.forConfigreturns aRuleBasedTraceSamplerthat delegates to an innerRateByServiceTraceSamplerfor spans matching no rule. That inner sampler was never subscribed to agent rates, so its rate table stayed at its constructor default of1.0for the life of the JVM: every rule miss was kept, stamped_dd.agent_psr=1.0with theAGENT_RATEmechanism, regardless of what the agent computed.Remote configuration compounds this. A change to the trace sample rate or sampling rules rebuilds the sampler in
CoreTracer.ConfigSnapshot, butaddResponseListenerruns once at writer construction againstinitialSamplerand is never re-run, so rebuilt samplers were disconnected too. Three broken cases:In practice this is masked whenever a default rate is set, because
buildthen appends anAlwaysMatchesSamplingRuleand the fallback is unreachable. It becomes visible when rules are configured without a catch-all: the service loses its adaptive backstop entirely and a traffic spike is ingested at 100% until someone intervenes.Every other tracer applies agent rates on a rule miss:
prioritySampleris never rebuiltconfigure()leaves_samplersaloneset_sampling_rulesonly reassigns rulesThis PR takes the Go/Node/Python shape: never rebuild the agent rate sampler, so nothing has to be re-registered or replayed and no learned rates are lost.
The behavior dates to #1102 (Nov 2019), which introduced rule based sampling. The
instanceofregistration predates it by ~22 months (c1f9f4fc, Jan 2018), whenRateByServiceSamplerwas the onlyRemoteResponseListenerand the check was correct. Adding a wrapper sampler silently stopped it matching. Remote config rebuilding arrived later still in #5466 (Jun 2023).Additional Notes
agentSampler()is a first-class part of theSamplercontract (default-null) rather than a set ofinstanceofspecial cases scattered acrossWriterFactoryandCoreTracer. Any current or future compositeSamplergets the right behavior for free by overriding it, and callers never need to know the concrete sampler type.RuleBasedTraceSampler'sfallbackSamplerfield is typedPrioritySampler, notSampler, soagentSampler()still needs oneinstanceof Samplercheck to recover the agent-rate instance through the fallback — this is a legitimate type-narrowing check, not a leftover of the old approach, and is covered byagentSamplerIsNullWhenFallbackDoesNotUseAgentRates.Sampler.Builder.forConfigand 5-argumentRuleBasedTraceSampler.buildare unchanged in behavior — they still default to a freshRateByServiceTraceSamplerwhen no agent sampler is supplied — so external callers and tests are unaffected.RateByServiceTraceSamplerstamps_dd.agent_psr=1.0with mechanismAGENT_RATEeven when no agent response has ever been received, making "the agent said keep everything" indistinguishable from "no rates ever arrived". Go, Node, Python and .NET all guard this (suppressing the tag or using theDEFAULTmechanism until the first response). This masked the present bug in telemetry. Changing it affects span tags across all configurations and needs its own discussion plus system-tests alignment.WriterFactorydecides what to register withDDAgentApionce, at writer construction, from whatever sampler shape exists at that moment. If the tracer starts with priority sampling disabled/forced and no rules (sosampler.agentSampler()isnullat construction), nothing gets registered — and if Remote Config later introduces sampling rules or a trace sample rate, the rebuiltRuleBasedTraceSamplerfalls back to a brand-new, still-unregisteredRateByServiceTraceSampler. This predates this PR (the same gap exists in the prior commit, just reached through the oldinstanceof/2-arg-forConfigpath); closing it means always eagerly constructing and registering one canonicalRateByServiceTraceSamplerregardless of the initial sampler's shape, which is a bigger change than this fix's scope.Testing
10 new tests:
RuleBasedSamplerAgentRatesTest(8) — agent rates forwarded to the fallback; matched rules unaffected by agent rates; a default rate still bypasses the fallback; a supplied agent sampler is used as the fallback and returned directly when no rules exist; rates learned before a rebuild still apply after it;agentSampler()isnullwhen the fallback isn't aSampler; an unreported service is still kept at 1.0 until the agent responds.WriterFactoryTest(2) —WriterFactoryregisterssampler.agentSampler()withDDAgentApiwhen non-null, using a realJavaTestHttpServerround-trip; skips registration (and still handles responses cleanly) whenagentSampler()returnsnull.TracingConfigPollerTest.samplerRebuiltByRemoteConfigStillAppliesAgentRates(1, pre-existing from the original fix, unchanged in intent) — drives a real remote config update through the poller, asserts the rebuilt sampler wraps the same agent sampler instance, and that a span matching no rule is dropped at the rate the agent published.Both behavioral tests were confirmed to fail against the unfixed code and pass with the fix.
Full
:dd-trace-core:test: 4465 tests, 1 failure —PendingTraceBufferTest.bufferFullYieldsImmediateWrite, the same order-dependent flake in this class noted in earlier runs of this suite (a different method in the same class failed then), unrelated to this change.spotlessCheckclean.Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueUse
solvesinstead, and assign the PR milestone to the issueJira ticket: [PROJ-IDENT]
🤖 Generated with Claude Code