Skip to content

fix(stats): send trace stats with their traces in on-demand mode - #1489

Draft
lucaspimentel wants to merge 7 commits into
mainfrom
lpimentel/stats-force-flush-end
Draft

lucaspimentel wants to merge 7 commits into
mainfrom
lpimentel/stats-force-flush-end

Conversation

@lucaspimentel

@lucaspimentel lucaspimentel commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Overview

In on-demand mode, Bottlecap sends traces on every flush but holds back the newest ~20s of trace stats until a later flush. In Lambda, that later flush can be minutes or hours away (the next invocation or shutdown), and the stats are lost if the shutdown flush fails. This PR sends stats in the same flush as the traces they describe.

Why it matters. Stats drive RED metrics, service pages and monitors, and they are computed from 100% of spans while traces are sampled. Today the sampled data arrives on time and the complete data arrives late, or not at all. Sparse or bursty functions are hit hardest: the stats for their last burst wait for shutdown, the riskiest flush (AWS allows 2s before killing the extension).

Root cause. Every on-demand flush called the stats flusher with force = false, so libdatadog's SpanConcentrator kept its newest two buckets (10s each in bottlecap) for late spans; only the shutdown flush forced them out. That trade-off suits a long-running agent whose timer flushes again in seconds, but in Lambda the sandbox can freeze right after a flush. Stats are the only telemetry type held back this way: logs, metrics and traces are flushed in full every time.

Evidence. In serverless-e2e-tests lambda-features, the statsv2 scenario reads trace.aws.lambda.hits short of the span count on most first reads, typically by 1,000 to 2,000 of 50,000, while the span count is already complete. Over 14 days of CI Visibility data (Oct 19 to Nov 2), about 80% of non-outlier first reads were short, evenly across runtime families. A deficit of that size matches the last ~20s of each function's traffic.

Change.

  • FlushingService::with_forced_stats() makes the blocking and non-blocking flushes force the stats flusher. The default is unchanged.
  • The on-demand FlushingService in main.rs enables it, covering the End, Periodic and Continuous flush decisions, including the interval ticks of the end,<ms> strategy.
  • A stats flush forces only its first concentrator pull. A forced pull returns every span added since the previous pull, so forcing every pull would keep a flush going, one stats request per pull, for as long as spans keep arriving.
  • The invocation's own aws.lambda span is in the concentrator before the End flush: on_platform_runtime_done is awaited end to end, send_processed_traces enqueues each Add synchronously, and Add and Flush share one FIFO channel.
  • Managed Instance mode keeps the hold-back, because its background timer keeps flushing. bottlecap-test-mode is unchanged.

Trade-offs.

  • A span arriving after its bucket was flushed is not dropped: it is counted, up to one flush interval later, in the oldest open bucket.
  • The same 10s bucket can be sent in more than one partial payload. Correct counts rely on the backend summing the partials, which the e2e results support: invocations sharing a bucket send it repeatedly, yet first-read hits match the span count exactly.
  • More stats requests under the end strategy: an unforced flush sends nothing until a bucket ages out (about one request per 10s bucket), while a forced End flush sends one per invocation. The request runs concurrently with the trace request in the same flush; its effect on post-runtime duration at high volume is being measured. Under default at high traffic, flushes are every 20s, so the change is negligible.

Testing

  • New fake-intake integration tests in tests/apm_integration_test.rs cover forced blocking and non-blocking flushes sending current stats alongside their traces, the hold-back that Managed Instance keeps, and the partial and late-bucket trade-offs above.
  • stats_flusher::tests::forced_flush_ends_while_spans_keep_arriving pins that a forced flush sends one stats request while spans keep arriving.
  • cargo nextest run --workspace passes, as do cargo fmt --check and cargo clippy --workspace --all-targets --features default with -D warnings.
  • e2e, inconclusive so far: a sandbox layer built from an earlier commit of this branch ran lambda-features with first-read hits equal to spans in all 18 runtimes, but that read came 11.6 minutes after the invocations ended, late enough that the unfixed hold-back would also have converged. A fresh deployment read at the usual ~2 minute timing is still needed.

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Pipelines

✨ Unblock PR with BitsAI

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 3 Pipeline jobs failed

DDCI - DataDog/datadog-lambda-extension | integration-suite: [payload-size-e2e] — 🔧 Needs a code fix, caused by this PR

View more details · View in DDCI

DataDog/datadog-lambda-extension | e2e-test-status (amd64) — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DDCI - DataDog/datadog-lambda-extension | bottlecap (arm64, fips)

View more details · View in DDCI

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 6c5a247 | Docs | View more details | Give us feedback!

@lucaspimentel
lucaspimentel marked this pull request as ready for review November 3, 2026 16:36
Copilot AI balanced review requested due to automatic review settings November 3, 2026 16:36
@lucaspimentel
lucaspimentel requested review from a team as code owners November 3, 2026 16:36
@lucaspimentel
lucaspimentel requested a review from lym953 November 3, 2026 16:36
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T02:28:51.255107Z 6c5a247 New commits
🔒 Security Review ✅ Completed 2026-11-06T02:29:33.552013Z 6c5a247 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread bottlecap/src/flushing/service.rs Resolved
@lucaspimentel

Copy link
Copy Markdown
Member Author

@DataDog review

Copilot AI 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.

🟡 Changes recommended

A critical forced-stats flush issue can keep accumulating batches while spans arrive, delaying stats delivery and blocking flush completion until ingestion leaves a gap.

1 open issue
What changed in this PR

This PR makes on-demand Lambda flushes send extension-computed trace stats without holding back recent buckets, reducing delays when the sandbox freezes. Managed Instance mode retains its existing behavior.

Changes:

  • Adds opt-in forced stats flushing across blocking, non-blocking, and retry paths.
  • Enables forced stats flushing for on-demand execution.
  • Adds fake-intake tests for forced delivery and default hold-back behavior.
File Description
bottlecap/​tests/​apm_integration_test.rs Tests current-bucket stats delivery through both flush paths.
bottlecap/​src/​flushing/​service.rs Adds and propagates the forced-stats option.
bottlecap/​src/​bin/​bottlecap/​main.rs Enables forced stats for on-demand mode.

Review effort: Balanced

Comment thread bottlecap/src/bin/bottlecap/main.rs Resolved

@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: 7d006a4d7a

ℹ️ 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 bottlecap/src/bin/bottlecap/main.rs Resolved
Comment thread bottlecap/src/flushing/service.rs Resolved
dsm_processor.clone(),
);
)
.with_forced_stats();

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 Preserve cardinality limits across partial bucket flushes

For high-throughput functions whose invocations repeatedly end within the same 10-second stats bucket, forcing every on-demand flush removes that bucket and lets later spans recreate it with fresh per-bucket cardinality state. Each invocation can therefore admit another full set of resource, endpoint, peer-tag, and additional-tag values, so the combined payloads for one logical bucket can far exceed the configured limits that normally collapse excess groups and bound stats cardinality. Preserve the limiter state across partial flushes, or avoid splitting a live bucket in a way that resets its limits.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted tradeoff: partial flushes can admit more distinct values across payloads for the same logical bucket, while each in-memory bucket remains bounded. Holding those stats back would reintroduce the delivery delay this PR fixes. Preserving cardinality tracking across partial flushes would need support in libdatadog rather than duplicate tracking in the extension.

@datadog-datadog-prod-us1-2 datadog-datadog-prod-us1-2 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.

Bits Code Review: PASS

More details

The on-demand setting reaches blocking, non-blocking, and retry stats flushes while preserving Managed Instance bucket hold-back. Static review identified no actionable defect; test execution was blocked by toolchain and dependency availability.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Bits Code Review · Commit 7d006a4

@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: ea687e2801

ℹ️ 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 bottlecap/src/flushing/service.rs Resolved
Comment thread bottlecap/src/flushing/service.rs Resolved
Comment on lines +163 to +166
let mut force_pull = force_flush;
loop {
let stats = guard.get_batch(force_flush).await;
let stats = guard.get_batch(force_pull).await;
force_pull = false;

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 Drain a stable snapshot during the final forced flush

This first-pull-only behavior also applies to flush_blocking_final(), where there is no next flush to collect data arriving after the initial forced concentrator command. During shutdown, trace/OTLP handlers and their forwarding tasks are canceled but not awaited before the final flush, so an Add can be queued after the first forced pull; the next pull is now non-forced, holds that span in a current bucket, returns empty, and lets shutdown discard it. Establish an ingestion barrier before the final forced snapshot, or otherwise retain final-flush semantics that cannot leave concurrently queued current buckets behind.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The missing shutdown ingestion barrier is already tracked in APMSVLS-679. That work coordinates listener completion and accepted-ingest draining before the final flush, with a bounded shutdown wait. This specific late-addition case belongs in its regression coverage. Keeping that broader shutdown fix separate from this PR.

@lucaspimentel
lucaspimentel force-pushed the lpimentel/stats-force-flush-end branch from eab7a02 to 87112eb Compare November 5, 2026 18:01
On-demand flushes kept the newest two 10s stats buckets back until a later flush. After an end-of-invocation flush the sandbox can freeze until the next invocation or shutdown, so the stats for spans that were already sent arrived minutes later, or were lost if the final flush failed. Stats are the only telemetry held back this way.

On-demand mode now forces the stats flush on every flush, so stats leave together with their traces. A span that arrives after its bucket was flushed is still counted, in the oldest open bucket. Managed Instance mode keeps the hold-back because its timer keeps flushing.
A forced concentrator pull returns every span added since the previous pull, so a flush that forced every pull kept going, one stats request per pull, for as long as spans kept arriving.
The on-demand flush tests now route the trace through the trace aggregator the FlushingService flushes, and assert that the same flush sends the trace.
Cover the two trade-offs of forcing stats: a second flush within one bucket sends that bucket again as a partial payload, and a span older than every open bucket still counts in the next flush.
@lucaspimentel
lucaspimentel force-pushed the lpimentel/stats-force-flush-end branch from 87112eb to 6c5a247 Compare November 6, 2026 02:26
@lucaspimentel
lucaspimentel marked this pull request as draft November 6, 2026 19:43
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.

2 participants