Sitelet https://github.com/DataDog/dd-trace-java/pull/12249
Skip to content

Flush LLM Obs intake writer synchronously in serverless environments - #12249

Open
purple4reina wants to merge 2 commits into
masterfrom
rey.abolofia/llmobs-serverless-writer-fix
Open

Flush LLM Obs intake writer synchronously in serverless environments#12249
purple4reina wants to merge 2 commits into
masterfrom
rey.abolofia/llmobs-serverless-writer-fix

Conversation

@purple4reina

@purple4reina purple4reina commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

The DD_INTAKE_WRITER_TYPE branch of WriterFactory.createWriter() — which is what actually carries LLM Obs spans, since Agent.java defaults the writer type to MultiWriter:DDIntakeWriter,DDAgentWriter whenever LLM Obs is enabled — never set alwaysFlush. In Lambda, the execution environment can freeze as soon as the handler returns, before this writer's periodic flush timer next fires, so spans were silently dropped most of the time.

This surfaced as ml_obs.trace rarely/never being emitted for java Lambda functions in serverless-e2e-tests.

What changed

Set alwaysFlush on the DDIntakeWriter branch the same way the DDAgentWriter branch already does (config.isAgentConfiguredUsingDefault() && ServerlessInfo.get().isRunningInServerlessEnvironment()), and drop the DDAgentWriter branch's separate LLM Obs DDIntakeWriter + MultiWriter — it duplicated the track the intake branch already builds, so every span was being sent twice (visible as ml_obs.trace reporting exactly 2x the invocation count).

Validation

Built and deployed custom Lambda layers to a sandbox stack to isolate each half of this fix:

  • Pre-fix code (no alwaysFlush on the intake branch): 0/8 invocations delivered an LLM Obs span.
  • Same code + only the alwaysFlush change: 8/8 invocations delivered exactly one span each, confirmed via CloudWatch logs (Successfully sent 1 traces ... LLMOBS/v2).
  • Confirmed via live invocation of the original MultiWriter fix that it sent every span twice (two independent LLMOBS/v2 payloads per invocation).

The DDAgentWriter branch of WriterFactory never wired up an LLM Obs
DDIntakeWriter, so LLM Obs spans were silently dropped whenever the
tracer picked DDAgentWriter (e.g. in Lambda/serverless mode with CI
Visibility disabled). Broadcast to a dedicated LLM Obs DDIntakeWriter
via MultiWriter, flushed synchronously in serverless environments
just like the primary writer.
@purple4reina
purple4reina requested a review from a team as a code owner August 20, 2026 20:45
@purple4reina
purple4reina requested review from ValentinZakharov and removed request for a team August 20, 2026 20:45
@dd-octo-sts

dd-octo-sts Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@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: 4196c2186d

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

.alwaysFlush(alwaysFlush)
.flushIntervalMilliseconds(flushIntervalMilliseconds)
.build();
remoteWriter = new MultiWriter(new Writer[] {remoteWriter, llmObsWriter});

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 Keep span post-processing single-threaded

When LLM Obs and AppSec API Security are both enabled, wrapping two independent RemoteWriters causes their TraceProcessingWorker threads to post-process the same DDSpan list concurrently. This violates SpanPostProcessor's single-thread guarantee, and AppSecSpanPostProcessor can consequently call the explicitly non-thread-safe ApiSecuritySampler.sampleRequest, extract schemas, close the WAF context, and release the same sampling permit twice. Ensure post-processing occurs once before fan-out, or otherwise prevent the LLM writer from post-processing the shared trace again.

Useful? React with 👍 / 👎.

.alwaysFlush(alwaysFlush)
.flushIntervalMilliseconds(flushIntervalMilliseconds)
.build();
remoteWriter = new MultiWriter(new Writer[] {remoteWriter, llmObsWriter});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route only LLM traces to the intake writer

perf: When LLM Obs is enabled in a regular high-throughput APM service, MultiWriter.write publishes every completed trace to this second worker even when it contains no LLM span; only later does LLMObsSpanMapper.map scan the trace and allocate a filtered list before discarding it. This duplicates queue traffic and post-processing for all APM traces, and ordinary traffic can fill the LLM writer's 1024-entry queue and cause genuine LLM traces to be dropped. Filter before enqueueing into the LLM writer rather than broadcasting indiscriminately.

AGENTS.md reference: AGENTS.md:L77-L77

Useful? React with 👍 / 👎.

// this, they're silently dropped by the agent/extension instead of reaching LLM Obs
// intake. Flush this track synchronously too when the primary writer is (i.e. in a
// serverless environment), so spans aren't lost when the execution environment freezes.
if (config.isLlmObsEnabled()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid duplicating LLM intake in configured MultiWriters

When dd.writer.type is configured as MultiWriter:DDAgentWriter,DDIntakeWriter with LLM Obs enabled, the outer MultiWriter recursively constructs both children: this condition adds an LLM intake writer inside the DDAgentWriter child, while the explicit DDIntakeWriter child also adds its existing LLM track. Every LLM span is therefore submitted to the same intake twice, producing duplicate LLM Obs records for configurations that previously sent one copy. Deduplicate the automatically added track when an enclosing writer already supplies LLM intake.

Useful? React with 👍 / 👎.

@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

When LLM Observability and AppSec API Security run together outside serverless mode, both writer threads can post-process the same trace. This can close one AppSec request context twice and release its limit permit twice.

Open Bits AI session

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

.alwaysFlush(alwaysFlush)
.flushIntervalMilliseconds(flushIntervalMilliseconds)
.build();
remoteWriter = new MultiWriter(new Writer[] {remoteWriter, llmObsWriter});

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 Post-process each trace only one time

This race can damage the AppSec post-processing limit and allow more work than its set limit.

Assertion details
  • Input: Enable LLM Observability and AppSec API Security with DDAgentWriter outside serverless mode. Process a request that stays open for API Security post-processing.
  • Expected: The system must post-process each trace one time before it sends the trace to both writers.
  • Actual: MultiWriter sends the same mutable trace to two worker threads. Each worker calls the global span post-processor. Both workers can process and close the same AppSec request context. Both workers can also release its limit permit.

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

@datadog-datadog-prod-us1

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Aug 20, 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.87 s 14.78 s [-0.1%; +1.3%] (no difference)
startup:insecure-bank:tracing:Agent 13.67 s 13.70 s [-1.3%; +0.9%] (no difference)
startup:petclinic:appsec:Agent 16.96 s 16.74 s [+0.5%; +2.2%] (maybe worse)
startup:petclinic:iast:Agent 16.88 s 16.93 s [-1.1%; +0.5%] (no difference)
startup:petclinic:profiling:Agent 16.74 s 16.69 s [-0.8%; +1.5%] (no difference)
startup:petclinic:sca:Agent 16.77 s 16.43 s [+1.1%; +3.1%] (significantly worse)
startup:petclinic:tracing:Agent 16.11 s 16.15 s [-1.0%; +0.5%] (no difference)

Commit: 487006b7 · 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.

The DDAgentWriter branch's dedicated LLM Obs DDIntakeWriter duplicated the
LLM Obs track that the DD_INTAKE_WRITER_TYPE branch already builds whenever
llmObsEnabled is true (Agent.java defaults to
MultiWriter:DDIntakeWriter,DDAgentWriter for that case), so every span was
sent twice. Verified live on Lambda: the pre-fix build sent LLMOBS/v2
payloads twice per invocation.

The original bug this branch was fixing -- spans silently dropped in
Lambda -- was actually caused by the DDIntakeWriter branch never setting
alwaysFlush, so the periodic flush timer rarely beat the execution
environment freezing after the handler returns. Verified live: without
alwaysFlush, 0/8 invocations delivered a span; with it, 8/8 did, one send
each.
@purple4reina purple4reina changed the title Fix LLM Obs spans silently dropped when using DDAgentWriter (Lambda/serverless) Flush LLM Obs intake writer synchronously in serverless environments Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant