Skip to content

Drop APM traces when APM tracing is disabled and keep LLM Observability working - #12473

Open
ncybul wants to merge 8 commits into
masterfrom
nicole.cybul/llmobs-apm-tracing-disabled
Open

ncybul wants to merge 8 commits into
masterfrom
nicole.cybul/llmobs-apm-tracing-disabled

Conversation

@ncybul

@ncybul ncybul commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

What Does This Do

Two fixes for running LLM Observability without APM tracing (solves #10051):

  1. DD_APM_TRACING_ENABLED=false now drops APM traces. The standalone sampler was only selected when ASM/IAST/SCA was enabled; every other configuration — including LLM Observability — fell through to the default sampler, which keeps traces, so APM traces kept flowing. On distributed traces the drop was defeated a second way: an extracted sampling priority is locked onto the span context at construction, so a request arriving with x-datadog-sampling-priority: 1 silenced the drop before it was ever decided locally.
  2. DD_TRACE_ENABLED=false with DD_LLMOBS_ENABLED=true no longer throws. LLMObsSystem.start() installed the real SDK implementation even with no tracer installed, and AgentTracer.get().buildSpan() returns null on the no-op tracer, so every LLMObs.start*Span() call threw a NullPointerException out of application code.

Motivation

DD_APM_TRACING_ENABLED=false is documented as working in all SDKs. LLM Observability users who don't want APM tracing had no working way to turn it off: the APM flag was a no-op, and DD_TRACE_ENABLED=false broke the LLMObs SDK.

Additional Notes

Approach. This is a reduced port of #10989, which had gone stale. That PR introduced a ProductTraceSource.LLMOBS bit plus a StandaloneProduct enum and a unified StandaloneSampler replacing AsmStandaloneSampler. Two things changed since: 0x20 is now taken by AI_GUARD, and no other tracer allocates an LLMObs _dd.p.ts bit — in dd-trace-js the products are exactly APM/ASM/DSM/DJM/DBM/AI_GUARD, and in dd-trace-py exactly the same set. LLM Observability doesn't need one: its spans ship to the LLM Observability intake (/evp_proxy/v2/api/v2/llmobs), which is independent of APM sampling priority, so the correct behavior is to drop the APM traces and let the LLMObs events through.

That leaves the fix as the selection logic that other tracers already have — compare getProductRateLimiter in dd-trace-js (1/minute for appsec/iast, RateLimiter(0), i.e. drop-all, otherwise) and LLMObsProcessor.process_trace in dd-trace-py (returns None for the whole APM trace when APM tracing is disabled):

  • ASM enabled → unchanged: AsmStandaloneSampler, 1 APM trace per minute for service catalog / billing.
  • otherwise → ForcePrioritySampler(SAMPLER_DROP, DEFAULT).

Because a DEFAULT-mechanism priority cannot overwrite a priority that is already set, products that force-keep their own traces are unaffected — AI Guard (ai_guard.keep) and ASM (asm.keep) keep working exactly as before, which AIGuardSamplingTest covers.

Inherited sampling priorities. That same lock is what defeated the drop on distributed traces: DDSpanContext locks an extracted priority at construction, and DEFAULT is not one of the mechanisms SamplingMechanism.canAvoidSamplingPriorityLock exempts, so the drop-all vote was silently refused on exactly the traces the flag exists to suppress. ContextInterpreter.samplingPriorityOrDefault now leaves the extracted priority UNSET when APM tracing is disabled and no product claimed the trace (no _dd.p.ts), letting the local sampler decide. This is the same shape as dd-trace-js' standalone onSpanExtract hook (packages/dd-trace/src/standalone/index.js), which resets the upstream priority under the same condition.

It sits in ContextInterpreter because that is the one point every propagation style funnels through, so Datadog, W3C, B3, Haystack and XRay headers are all covered — including Haystack, whose defaultSamplingPriority() override manufactures a SAMPLER_KEEP out of headers that carry no priority at all.

Gating on _dd.p.ts is what keeps this from discarding a decision another product asked for: ASM- and AI-Guard-marked traces are already exempt from sampler override in TraceCollector.setSamplingPriorityIfNecessary, and they keep their upstream priority here too.

ASM standalone behavior is unchanged. AsmStandaloneSampler votes with SamplingMechanism.APPSEC, which canAvoidSamplingPriorityLock already exempts when APM tracing is disabled — and on that path DDSpanContext assigns rather than compare-and-swaps — so it already overrode an inherited priority before this change and reaches the same decision after it.

Testing.

  • SamplerTest: APM traces are dropped when APM tracing is disabled, with and without LLM Observability enabled — and the dropped trace is still handed to the writers, which is what keeps LLM Observability spans flowing to their own intake.
  • LLMObsSystemTest: the SDK stays no-op when tracing is disabled.
  • DatadogHttpExtractorTest.ApmTracingDisabled: an upstream SAMPLER_KEEP is dropped when no product marked the trace, and preserved when _dd.p.ts is set.
  • Existing AIGuardSamplingTest, ApmTracingDisabledChunkMarkerTest and the apm-tracing-disabled smoke tests pass unchanged.

Contributor Checklist

Claude session: ba8ff7f0-b73e-41d6-a806-3603ab1c7329
Resume: claude --resume ba8ff7f0-b73e-41d6-a806-3603ab1c7329

🤖 Generated with Claude Code

…ty working

Selecting a standalone sampler only when ASM/IAST/SCA is enabled left every
other DD_APM_TRACING_ENABLED=false configuration on the default sampler, which
keeps traces, so APM traces kept flowing. Drop them instead, as dd-trace-js and
dd-trace-py do; LLM Observability spans are unaffected because they ship to the
LLM Observability intake, which is independent of APM sampling priority.

DD_TRACE_ENABLED=false installed the real LLM Observability SDK with no tracer
to back it, and the no-op tracer's buildSpan() returns null, so every
LLMObs.start*Span() call threw out of application code. Keep the SDK no-op.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@ncybul ncybul added type: bug fix Bug fix tag: ai generated Largely based on code generated by an AI or LLM comp: core Tracer core comp: mlobs ML Observability (LLMObs) labels Sep 11, 2026
@datadog-datadog-prod-us1

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.03 s 13.94 s [-0.5%; +1.8%] (no difference)
startup:insecure-bank:tracing:Agent 13.01 s 13.05 s [-1.1%; +0.5%] (no difference)
startup:petclinic:appsec:Agent 16.97 s 17.01 s [-0.9%; +0.4%] (no difference)
startup:petclinic:iast:Agent 16.94 s 16.88 s [-0.3%; +1.0%] (no difference)
startup:petclinic:profiling:Agent 16.63 s 16.92 s [-3.1%; -0.3%] (maybe better)
startup:petclinic:sca:Agent 16.76 s 16.80 s [-1.2%; +0.7%] (no difference)
startup:petclinic:tracing:Agent 16.08 s 16.00 s [-0.3%; +1.3%] (no difference)

Commit: a2a2a57d · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

Comment thread dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/LLMObsSystem.java Outdated
Comment thread dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java Outdated
ncybul and others added 2 commits September 14, 2026 11:53
The two new Groovy smoke tests would have needed the `tag: override groovy
enforcement` label. What they proved end-to-end — a dropped APM trace is still
handed to the writers, which is how LLM Observability spans still reach their
intake — is now asserted in SamplerTest instead.

Co-Authored-By: Claude Opus 5 <[email protected]>

@sabrenner sabrenner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

makes sense for agent obs, probably just needs java tracer signoff!

@ncybul
ncybul marked this pull request as ready for review September 15, 2026 17:59
@ncybul
ncybul requested review from a team as code owners September 15, 2026 17:59
@ncybul
ncybul requested review from mhlidd and removed request for a team September 15, 2026 17:59

@datadog-datadog-prod-us1 datadog-datadog-prod-us1 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

A sampled parent can lock a keep priority before the new sampler runs. The sampler then fails to drop that normal APM trace when APM tracing is off.

Open Bits AI session

🤖 Datadog Autotest · Commit 5923093 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment thread dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java Outdated

@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: 592309351a

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

Comment on lines +40 to +43
if (isAsmEnabled(config)) {
log.debug(
"APM tracing is disabled, but ASM is enabled. Only 1 APM trace per minute will be sent.");
return new AsmStandaloneSampler(Clock.systemUTC());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve standalone ASM sampling after remote activation

When DD_APM_TRACING_ENABLED=false and AppSec starts in its default ENABLED_INACTIVE state, isAsmEnabled(config) is false, so this permanently selects the drop-all sampler. Remote Configuration can later activate AppSec, but it changes AppSecSystem state rather than the immutable Config activation used here, and the sampler is not rebuilt; consequently non-event traces remain dropped instead of providing the one-trace-per-minute service-catalog and billing signal required by standalone ASM. The sampler selection needs to accommodate runtime AppSec activation rather than treating ENABLED_INACTIVE as permanently off.

Useful? React with 👍 / 👎.

@ncybul ncybul Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I replaced that branch with the new ApmTracingDisabledSampler, which constructs both samplers up front and picks between them per sampling decision based on ActiveSubsystems.APPSEC_ACTIVE.

Comment thread dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/LLMObsSystem.java Outdated
Comment thread dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java Outdated

@mhlidd mhlidd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One comment about expected behavior

Comment thread dd-trace-core/src/main/java/datadog/trace/common/sampling/Sampler.java Outdated
@ncybul
ncybul force-pushed the nicole.cybul/llmobs-apm-tracing-disabled branch from 7860cdc to 80e580e Compare September 16, 2026 20:26

@mhlidd mhlidd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM following addressing Codex and Copilot comments 👍

@ncybul
ncybul requested a review from a team as a code owner September 18, 2026 17:06
@ncybul
ncybul requested review from claponcet and removed request for a team September 18, 2026 17:06

@datadog-datadog-prod-us1 datadog-datadog-prod-us1 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

The new drop sampler cannot replace manual keep decisions. APM traces can still leave the process when APM tracing is off. Remote AppSec activation and shutdown after single-span sampling also have trace-loss paths.

Open Bits AI session

🤖 Datadog Autotest · Commit fe764e7 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

// catalog, so with it off we can drop every APM trace. Traces a product does want
// (asm.keep, ai_guard.keep) are force-kept on the span, which no sampler can override.
log.debug("APM tracing is disabled. APM traces will be dropped.");
return new ForcePrioritySampler(PrioritySampling.SAMPLER_DROP, SamplingMechanism.DEFAULT);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 The drop sampler does not override manual keep decisions

Applications can send APM trace data after users turn APM tracing off.

Assertion details
  • Input: Set DD_APM_TRACING_ENABLED=false without ASM. Then set manual.keep, use the sampling-priority API, or enable the latency trace interceptor.
  • Expected: APM tracing off must drop ordinary APM traces. Only product-owned ASM and AI Guard decisions must keep traces.
  • Actual: The DEFAULT drop vote cannot replace a locked USER_KEEP decision. The writer then exports the APM trace.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest · Open Bits AI session

// and the trace is never dispatched. That is harmless for a sampled-out APM trace, but
// with APM tracing disabled the dropped trace also carries the LLM Observability spans,
// which have to reach their intake regardless of the APM sampling decision.
consumeFromSecondaryQueue();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The flush barrier omits pending span-sampling work

Shutdown can lose pending LLM Observability spans when single-span sampling rules are active.

Assertion details
  • Input: Enable LLM Observability and single-span sampling rules with APM tracing off. Finish a trace and close the writer before the span-sampling worker forwards it.
  • Expected: Flush must process all earlier span-sampling work before it reports completion.
  • Actual: A dropped trace can remain in the span-sampling queue when the primary flush marker runs. The secondary-queue sweep sees no trace. Close then stops the sampling worker and loses the LLM Observability event.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest · Open Bits AI session

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: core Tracer core comp: mlobs ML Observability (LLMObs) tag: ai generated Largely based on code generated by an AI or LLM type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants