[APMSVLS-501] feat(bottlecap): add bottlecap-test-mode binary - #1216
Draft
lucaspimentel wants to merge 3 commits into
Draft
[APMSVLS-501] feat(bottlecap): add bottlecap-test-mode binary#1216lucaspimentel wants to merge 3 commits into
lucaspimentel wants to merge 3 commits into
Conversation
lucaspimentel
force-pushed
the
lpimentel/bottlecap-test-mode
branch
from
August 26, 2026 16:58
149d9d4 to
f8da804
Compare
Moves start_trace_agent out of the Lambda binary into bottlecap::startup so both [[bin]] targets can share it, and splits it into: - build_trace_agent, which returns an unspawned TraceAgent plus a TraceAgentPipeline handle struct - start_trace_agent, a thin wrapper that spawns the agent bottlecap-test-mode needs the unspawned agent so it can attach its /flush RouterExtension before spawning; the Lambda binary keeps calling start_trace_agent and its call site is unchanged. Placed at the crate root rather than under traces/ because it wires trace, stats, proxy, lifecycle, tags, appsec, and flushing together.
A second [[bin]] target that runs the APM trace-processing surface as a long-lived HTTP server with no Lambda lifecycle. Listens on 127.0.0.1:8126 and exposes the standard tracer endpoints (/v0.4/traces, /v0.5/traces, /v0.6/stats, /info) plus POST /flush for deterministic harness-driven flushing. Configured by the same DD_* env vars the Lambda binary reads. Optional periodic flushing via DD_SERVERLESS_FLUSH_STRATEGY (decoupled from managed-instance mode). Gated behind the `test-mode` cargo feature (required-features), so it is not built in default or fips builds. Build with `cargo build --bin bottlecap-test-mode --features test-mode`. Intended for the cross-agent parity harness (APMSVLS-496) and for local dev workflows that need a tracer endpoint without standing up a Lambda. APMSVLS-501 🤖 Co-Authored-By: Claude Code <[email protected]>
lucaspimentel
force-pushed
the
lpimentel/bottlecap-testmode-binary
branch
from
August 26, 2026 20:15
5f17230 to
14cfb71
Compare
6 tasks
|
🔗 Commit SHA: 4efbc0d | Docs | View more details | Give us feedback! |
flush_blocking_final expects on the metrics aggregator handle, so a dead aggregator task panicked the connection task and the harness saw a dropped connection instead of a status it could act on. The five flushers also bound only their individual HTTP calls, so stacked retries could leave a request outstanding far longer than a harness should wait. Runs the flush in a spawned task and caps it at 30s: 204 on success, 500 if the task panics, 504 after aborting a timed-out flush. Restores the hardening that previously lived in the trace agent's hardcoded /flush handler, now on the consumer side where the route lives.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1201. Review #1201 first.
Overview
Adds a second
[[bin]]target,bottlecap-test-mode, that runs the APM trace-processing surface as a long-lived HTTP server with no AWS Lambda Extension lifecycle. ReusesTraceAgent,FlushingService, and the trace/stats/proxy flushers built bybottlecap::startup::build_trace_agent, which this PR extracts from the Lambda binary (see below); the only Lambda-binary code it duplicates isinit_ustrandenable_logging_subsystem(~20 lines, called out in the design doc).Endpoints on
127.0.0.1:8126:/v0.4/traces,/v0.5/traces,/v0.6/stats,/info— unchanged from the Lambda binary'sTraceAgentrouter.POST /flush— new, registered by aFlushRouterExtensionimpl attached viaTraceAgent::with_router_extension(...)(the seam from [APMSVLS-501] refactor(bottlecap): preparatory work for "test-mode" binary #1201). CallsFlushingService::flush_blocking_final()in a spawned task bounded at 30s:204 No Contenton success,500if the flush panics,504after aborting a timed-out flush.flush_blocking_finalexpect()s on the metrics aggregator handle, so without the spawn a dead aggregator would panic the connection task and the harness would see a dropped connection rather than a status it can act on; the per-flusher HTTP timeouts also bound only individual calls, so stacked retries could otherwise outlive any sensible harness wait.Configuration: same
DD_*env vars as the Lambda binary. Notable inputs:DD_APM_DD_URL— redirects trace intake (the parity harness points this at the fake-intake from [APMSVLS-497][APMSVLS-498] test: add fake-intake for APM payload-level tests #1194).DD_SERVERLESS_FLUSH_STRATEGY— opt-in periodic flush ticker, decoupled from managed-instance mode.DD_TESTMODE_FUNCTION_ARN— overrides the stub ARN used for tag generation (defaults toarn:aws:lambda:us-east-1:000000000000:function:testmode).API key is hardcoded to
"stub-key"(no secrets resolver path); the parity harness fake-intake ignores auth.bottlecap::startupextractionThis PR also promotes
start_trace_agentout ofsrc/bin/bottlecap/main.rsinto a new top-level library module,bottlecap::startup, and splits it into:build_trace_agent— returns an unspawnedTraceAgentplus aTraceAgentPipelinestruct with namedpubfields (flushers, trace-channel sender, shutdown token, aggregator/concentrator handles).start_trace_agent— thin wrapper that spawns the agent. The Lambda binary's call site is unchanged.bottlecap-test-modeneeds the unspawned agent so it can attach its/flushRouterExtensionbefore spawning, which is why the split exists. Placed at the crate root rather than undertraces/because it wires trace, stats, proxy, lifecycle, tags, appsec, and flushing together: cross-cutting orchestration, not a trace-domain API.This extraction was originally part of #1201 and moved here after review feedback: with no second binary in that PR,
build_trace_agenthad no caller and the public module looked unmotivated. Landing it next to its first consumer makes the shared-library placement self-evident. Behavior is unchanged for the Lambda binary;trace_agent.rschanges are two retargeted rustdoc links.Feature gating
Gated behind the
test-modecargo feature viarequired-features = ["test-mode"], the same feature that gatesInvocationProcessorHandle::noop()(added in #1201). The binary is therefore not built indefaultorfipsbuilds, including bycargo build --workspace. Build/run with:No CI job currently builds or lints with
--features test-mode, so this binary is invisible to CI as things stand. Tracked in Follow-ups below.Shutdown ordering
signal::ctrl_c()cancels the shutdown token first (drives axum'sgraceful_shutdownso any in-flight/v0.4/tracesrequest drains through the trace aggregator), then runsflush_blocking_final(). The periodic flush task selects on the same token so it doesn't leak when the listener stops.Why a second binary
The overlap between Lambda-mode and test-mode is small (~20 lines:
init_ustr, logging, config load), and the rest is intentionally different (no telemetry listener, no LWA, no logs agent, no proxy, no DogStatsD UDP, no event-bus-driven lifecycle). A second[[bin]]makes the test-mode surface explicit and structural, enforced by the compiler instead of by a runtime branch. Rejected alternatives (env-var branch, auto-detect, CLI flag, single binary with mode gating) are in the design doc.Design doc:
lucas-pimentel/docs/bottlecap-test-mode.md(local; happy to land it in-repo if reviewers prefer).Testing
cargo check --bin bottlecap-test-mode --features test-modecargo clippy --workspace --all-targets --features default -- -D warnings(existing surface unchanged)cargo clippy --workspace --all-targets --features default,test-mode -- -D warningscargo fmt --all -- --checkcargo test -p bottlecap --lib— 548 passed (unchanged from [APMSVLS-501] refactor(bottlecap): preparatory work for "test-mode" binary #1201; the extraction is a pure move):8200:POST /v0.4/traces(real msgpack span) → 200, buffered, periodic flush fired at 2s, intake receivedPOST /api/v0.2/traces(485 bytes,DD-API-KEY: stub-key).POST /flush→ 204. (Predates the timeout/panic guard in4efbc0dd; the 500 and 504 paths were verified separately against an extracted copy of the handler, not against the running binary.)POST /v0.5/traces(malformed) → 500 (correct rejection from the existing v0.5 deserializer).GET /info→ 200 with the standard endpoints list.TRACE_AGENT | Shutdown signal received, shutting down→Aggregator service stopped→ clean exit.No new unit tests in this PR. The seam (
RouterExtension) and the no-op handle (InvocationProcessorHandle::noop()) are both covered by tests added in #1201; end-to-end coverage for test-mode lands as part of the parity harness (#1194 and the futureapm-agent-parity-rsrepo).Follow-ups
Known gaps, called out so they are not mistaken for oversights. Neither blocks the binary from working; happy to fold either into this PR if reviewers prefer.
--features test-mode. Existing jobs run--features defaultand--no-default-features --features fips, andcargo build --allskipsrequired-featurestargets, so nothing in CI compiles this binary. A clippy/build step passing--features test-modewould keep it green; without it, a change to library code can break this target without any job failing.POST /flushguard in4efbc0ddhas no committed test. The 204/500/504 paths were verified manually, but the test-mode binary has no#[cfg(test)]module. Testing it in place needs the handler factored out of the closure, orFlushingServicemade injectable, so it is deliberately deferred rather than bolted on.🤖 Generated with Claude Code