Skip to content

Add datadog-mock-intake crate and serve trace-agent tests with it - #174

Draft
lucaspimentel wants to merge 4 commits into
mainfrom
lpimentel/add-mock-intake
Draft

lucaspimentel wants to merge 4 commits into
mainfrom
lpimentel/add-mock-intake

Conversation

@lucaspimentel

@lucaspimentel lucaspimentel commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Adds a shared datadog-mock-intake crate: an in-process mock Datadog APM intake served with hyper that accepts the APM endpoints serverless components flush to, decodes msgpack/protobuf payloads on arrival, and exposes typed query methods plus raw request capture for test assertions.

  • POST /api/v0.2/stats
  • POST /api/v0.2/traces
  • POST /api/v0.1/pipeline_stats

Rewires the trace-agent integration tests onto the crate, replacing the test-local MockServer.

Motivation

The mock intake was originally built in datadog-lambda-extension (#1385) as a bottlecap test fixture and then promoted to a feature-gated library module plus a standalone mock-intake binary. It has already proven useful there:

  • It is the payload-level harness behind bottlecap's APM/DSM integration tests (19 APM + 1 DSM), which exercise the full flush path against a real HTTP intake instead of asserting on in-memory buffers.
  • Its stats failure injection (MOCK_INTAKE_FAIL_STATS_FIRST_N) is what lets those tests cover tracer retry behavior against 500 responses, including the guarantee that exactly the first N attempts are rejected.
  • As a standalone binary, it has served for local debugging against a live tracer and the test-mode trace processor: one summary line per request with hits grouped by the full stats aggregation key, plus JSON dumps of every decoded payload, make it possible to see exactly what a tracer flushed without wiring up a real backend.

The APM/DSM integration tests in datadog-lambda-extension (bottlecap) and datadog-trace-agent each carried their own intake fixture. Extracting the bottlecap mock intake into this shared crate lets both test suites use one implementation. A follow-up PR in datadog-lambda-extension (#1392) consumes this crate (stacked on this PR's revision), deletes the bottlecap-local copy, and supersedes #1385.

Additional Notes

  • Ported from the bottlecap mock intake (axum 0.8) to hyper 1.6 to match this workspace's existing HTTP stack; no axum dependency is introduced.
  • Behavior preserved from the original: endpoint set, gzip/zstd decoding, the 2 MiB wire-body limit (413 before decode), 404/405 routing, stats failure injection, request summaries, and JSON dumps.
  • Additions needed by the trace-agent tests: pipeline_stats_url() accessor and raw request capture (CapturedRequest, requests_for_path()). Typed query methods return only accepted, successfully decoded payloads; raw capture records completed POST attempts including rejected ones.
  • New workspace dependencies (rmp-serde, flate2, zstd) are already covered in LICENSE-3rdparty.csv; dd-rust-license-tool check passes.

Describe how to test/QA your changes

  • cargo test -p datadog-mock-intake: 23 tests, including ports of the original bottlecap intake's unit tests plus new transport regression tests (body limit, 404/405, raw-capture semantics, zstd decoding).
  • cargo test --workspace: 432 passed, 0 failed.
  • cargo clippy --workspace --all-targets: clean. dd-rust-license-tool check: passes.
  • Trace-agent integration tests now assert on the intake's decoded capture instead of re-decoding captured bytes; the DSM forwarding test sends a valid msgpack payload and verifies both unchanged byte forwarding and intake acceptance.

In-process mock Datadog intake that accepts the APM endpoints serverless
components flush to (/api/v0.2/stats, /api/v0.2/traces,
/api/v0.1/pipeline_stats), decodes msgpack/protobuf payloads on arrival,
and exposes typed query methods plus raw request capture for test
assertions. Ported from the bottlecap fake intake and served with hyper;
preserves the 2 MiB wire-body limit, gzip/zstd decoding, stats failure
injection, request summaries, and JSON dumps.
Replace the test-local MockServer with the shared datadog-mock-intake
crate. Payload assertions now use the intake's decoded capture instead of
re-decoding captured bytes, positive assertions wait on decoded payload
arrival, and the DSM forwarding test sends a valid msgpack pipeline-stats
payload so it verifies both unchanged byte forwarding and intake
acceptance.
@lucaspimentel lucaspimentel changed the title Add datadog-mock-intake crate and serve trace-agent tests with it Add datadog-mock-intake crate and serve trace-agent tests with it Sep 25, 2026
The datadog-lambda-extension standalone binary and its environment
variables are now named mock-intake / MOCK_INTAKE_*; align the crate
documentation.
…-intake

Guards decompress() against decompression-bomb-style memory blow-up by
capping gzip/zstd output at 64 MiB. Also skips building the diagnostic
JSON dump when no dump_dir is configured (the default), avoiding a
wasted allocation on every request, and marks DSM pipeline-stats
payload fields #[serde(default)] so a payload missing an optional
field still decodes.
🤖
@lucaspimentel

Copy link
Copy Markdown
Member Author

@DataDog review

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

Two of the most critical issues are in datadog-mock-intake: accepted connection tasks are not cancelled on MockIntake drop, allowing traffic to leak across tests; and typed payload captures can record requests out of arrival order due to async decoding. Additionally, the no-stats integration test asserts against decoded stats_payloads() rather than raw captures, meaning a malformed stats POST would silently pass the assertion.

Open Bits AI session

🤖 Bits Code Review · Commit a074193 · @DataDog review to ask questions


let io = TokioIo::new(stream);
let state = std::sync::Arc::clone(&state);
tokio::spawn(async move {

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 Dropping the intake leaves accepted connections running

Each accepted connection runs in a detached task, while dropping MockIntake aborts only the listener. An established HTTP/1 keep-alive connection can therefore continue sending requests after the intake is dropped, violating its shutdown contract and allowing state or traffic to leak across tests. The connection tasks need coordinated cancellation or tracking.

Was this helpful? React 👍 or 👎
🤖 Bits Code Review · @DataDog review to ask questions · Open Bits AI session

Comment on lines +200 to 205
let stats_payloads = mock_intake.stats_payloads();
assert!(
stats_reqs.is_empty(),
"Expected no stats request to mock server, received {} request(s)",
stats_reqs.len()
stats_payloads.is_empty(),
"Expected no stats request to mock intake, received {} request(s)",
stats_payloads.len()
);

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 Assert against raw stats requests

The negative assertion checks only successfully decoded stats payloads. If the agent sends a malformed stats POST, the intake records the request but excludes it from stats_payloads(), so this test incorrectly passes despite outbound stats traffic.

Suggested change
let stats_payloads = mock_intake.stats_payloads();
assert!(
stats_reqs.is_empty(),
"Expected no stats request to mock server, received {} request(s)",
stats_reqs.len()
stats_payloads.is_empty(),
"Expected no stats request to mock intake, received {} request(s)",
stats_payloads.len()
);
let stats_requests = mock_intake.requests_for_path("/api/v0.2/stats");
assert!(
stats_requests.is_empty(),
"Expected no stats request to mock intake, received {} request(s)",
stats_requests.len()
);

Was this helpful? React 👍 or 👎
🤖 Bits Code Review · @DataDog review to ask questions · 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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant