Skip to content

feat(payload): v1.0 (idx/ETP) trace agent payload - hardening, wire-format validation, and fingerprint corpus - #1929

Merged
ajgajg1134 merged 13 commits into
mainfrom
trace-agent-v1
Sep 21, 2026
Merged

ajgajg1134 merged 13 commits into
mainfrom
trace-agent-v1

Conversation

@ajgajg1134

@ajgajg1134 ajgajg1134 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Add trace-agent payload generation for /v1.0/traces (idx/ETP), with service-graph workloads, error injection, span links, and events. chunks_per_payload is an exact batch size: a budget too small for the complete batch rejects it without dropping or adding chunks. Cache construction fails with an actionable error after 1024 consecutive rejected blocks.

Preserve configured error attributes, validate complete tracer captures and generated wire output, and separate configuration, encoding, and tests for review.

Notes for reviewers

Start with lading_payload/src/trace_agent/v1/README.md for batching and workload constraints. config.rs contains validation, encoder.rs contains the wire schema, v1.rs contains generation, and test.rs/golden.rs contain the invariants and fixture checks.

The wire format is idx/ETP MessagePack for /v1.0/traces: integer-keyed payload/chunk/span maps, a payload-scoped streaming string table (empty string at index zero), and flat key/type/value attribute triples. Reference implementations are the Agent idx encoder/decoder, Saluki decoder, and dd-trace-go encoder.

The golden request bodies came from apm-v1-trace-smoke using dd-trace-go v2.11.0-dev.1. Its discovery shim advertised v1.0 and native span events and forwarded requests to a recording sink. The fixtures cover nested services, errors, links, and scalar event attributes.

The test-only decoder checks schema-level meaning. Golden round trips include every chunk of both captures, including errors and cross-chunk string references. Generated-payload tests verify exact batch sizes, span counts, errors, links/events, and determinism. Equivalent encodings can differ in attribute order, integer widths, and therefore streaming string indices, so raw byte equality is not a replacement for structural comparison. Golden tests are not a substitute for an ingestion check against the real agent.

The service graph is independent of the OTLP generator: explicit names/resources/types, first-service roots, static scalar attributes, and no dictionary pools, per-call attributes, max_repeat, or explicit root selection. Port topology and rates semantically; do not assume matching attribute cardinality. Shared traversal/configuration can be considered separately from this encoder.

The v1 fingerprint intentionally changes with exact batching: single-chunk bodies repeat metadata and restart the string table, and geometric sizing probes no longer consume RNG draws. Corpus entropy changes from 7.0414 to 6.6439 bits/byte; the other 12 fingerprint baselines are unchanged.

Validation

  • ci/fmt
  • ci/custom_lints (pinned ast-grep 0.39.5)
  • cargo clippy -p lading-payload --all-targets --all-features
  • cargo nextest run --workspace --exclude sheepdog (508 passed)
  • ci/fingerprint (13 passed; v1 fingerprint also verified reproducible)
  • New example passes lading config-check
  • Impossible 100-chunk / 1 KiB batch returns an actionable error through the rebuilt payload tool
  • Full ci/validate: shellcheck and formatting pass, but all-feature compilation is blocked locally by missing macOS FUSE. Workspace tests above ran without optional FUSE.

Extends the trace agent payload generator with a v1.0 variant, targeting
the /v1.0/traces endpoint. The v1.0 format is a structured tracer payload
(streaming string table, trace chunks, spans with attributes, links, and
events), so unlike v0.4 it is generated from a user-declared service
graph: a set of services, each declaring operations, each of which may
call operations on other services. One payload is one tracer POST.

The generator emits spans the trace-agent's normalizer leaves untouched
(non-empty service/name/resource, non-zero span IDs, timestamps within
signed 64-bit bounds), so a target's output can be compared against what
was sent without normalization noise.

Blocks are single tracer payloads whose chunk count is scaled toward the
requested block size; an undersized request writes nothing and is
treated as a rejected block, matching the block cache's adaptation.
…ng blocks

Sizing a block to fit max_bytes probed candidate chunk counts by
regenerating every chunk from scratch, redoing generation - the dominant
cost - several times per block. Chunks are now generated exactly once
into a pool and each probe re-encodes a prefix of it, matching the v0.4
approach. A stale last_span_count on the early-return paths and the
floor*2 overflow are fixed along the way.

Per-span work moves out of the generation hot path: operations are
resolved once at construction into interned (Arc) service, name,
resource, span_type, component, env, and version handles plus
key-sorted attribute vectors, so building a span clones cheap handles
instead of re-deriving sorted attributes from the configuration
HashMap. The suboperations clone that existed only to appease the
borrow checker is gone.

Validation hardening: duplicate operation detection and suboperation
reference checks now run in Config::valid (config-check sees them) via
an FxHashSet, and deny_unknown_fields on Service, Operation, and
SubOperation makes a mistyped field fail loudly instead of silently
dropping the call graph. The entry-point selection no longer expects;
it reports an error. Determinism and span-nesting invariants are now
proptests over arbitrary seeds rather than single-seed unit tests.
…tures

The hand-written byte-literal tests only proved the encoder agrees with
itself; a wrong field number or attribute layout would pass all of them.
These golden tests close that gap with two fixtures captured from a real
dd-trace-go v2.11.0-dev.1 tracer (via the apm-v1-trace-smoke application
posting to a recording sink):

- golden_tracer_payload.bin: three services with nested parent/child
  spans and error spans, six chunks per payload.
- golden_links_events_payload.bin: the same configuration with a span
  link (16-byte trace ID, attributes, flags) and a span event carrying
  one attribute of each scalar type.

A schema-aware test decoder, transcribed from the reference encoder and
decoder (dd-trace-go payload_v1.go, saluki decoders/datadog) rather than
from this module's encoder, decodes the fixtures and this encoder's
output into the same structures and requires them to agree. The decoder
accepts both wire styles the references produce: compact and
fixed-width integers, and the omit-default style this encoder uses
alongside the tracer's always-present chunk fields.
Real tracers mark a failed span with the error bit plus error.type and
error.message string attributes - that is what the golden captures show
dd-trace-go sending and what the receiving agent's error tracking reads.
Error injection now matches that shape instead of attaching
http.status_code: 500 to every span regardless of span type.

INTENTIONAL fingerprint change, per the ci/fingerprint policy: the
corpus config sets error_rate 0.1, so roughly one span in ten now
carries different attributes.

trace_agent_v1: d7380752e9daf11655a97ac2548c37113d6efe0865f4a6f87d8c8e637f5fa6db
          -> c9070ba10cd00c859be56cbf1600a10f475a4e547b73c85f495cb701cc1d130d
entropy: 7.0359 -> 7.0414 (increased; the constant status-code string is
replaced by two longer synthetic error attributes).

The regenerated fingerprint.txt is committed alongside the code change,
as the policy requires. Also adds ci/fingerprints/trace_agent_v1/ (the
determinism corpus for the new payload) and
examples/trace-agent-v1.yaml (a worked service-graph example,
counterpart to trace-agent-v04.yaml).
ajgajg1134 and others added 4 commits September 11, 2026 14:06
… agent

Import ValueWriteError instead of using 4-segment paths, drop the
`as _` import of Serialize, and apply cargo fmt.
The golden tests validate lading's hand-rolled v1.0 encoder against real
dd-trace-go captures, but carried two pieces of avoidable bulk: a
hand-written base64 decoder and ~180 lines of hand-written MessagePack
marker matching.

Use the base64 crate as a dev-dependency (already resolved in the
lockfile) and delegate primitive MessagePack framing to rmp::decode,
which the encoder already depends on for rmp::encode.

The schema layer stays hand-transcribed from the reference
implementations -- field identifiers, the streaming string table, and the
[key, type, value] attribute layout -- so the tests remain an independent
check on the encoder rather than a mirror of it. Marker parsing is rmp's
own well-tested concern, so sharing it creates no blind spot.

Verified by mutating a span field identifier in the encoder: the two
golden round-trip tests fail while all 16 inline byte-literal tests pass,
confirming the coverage that matters is intact.

golden.rs: 753 -> 629 lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@datadog-datadog-us1-prod

This comment has been minimized.

Comment thread lading_payload/src/trace_agent/v1/encoder.rs Outdated
Comment thread lading_payload/src/trace_agent/v1/encoder.rs Outdated
Comment thread lading_payload/src/trace_agent/v1/encoder.rs Outdated
Comment thread lading_payload/src/trace_agent/v1/golden.rs
Comment thread lading_payload/src/trace_agent/v1/golden.rs Outdated
Comment thread lading_payload/src/trace_agent/v1/README.md
Comment thread lading_payload/src/trace_agent/v1.rs Outdated
Comment thread lading_payload/Cargo.toml Outdated
Respond to open PR review comments:

- Give the v1.0 encoder its own Error type with precise variants and roll
  it into the crate error via thiserror #[from], matching the weighted and
  unit module precedent. Replaces the stringy Validation(String) mapping
  for u32 cast failures with a LengthOverflow variant.
- Collapse the map_err chains to ? now that the error type converts with
  From, and wrap write_bool's bare io error into the write variant.
- Collapse the C-style free functions into private write methods on
  TracerPayload, TraceChunk, Span, SpanLink and SpanEvent, bundling the
  writer and string table into a private Encoder struct so no write step
  threads state through its signature.
- Replace the golden decoder's String error with a thiserror DecodeError
  enum carrying typed variants for each schema failure.
- Switch span and resolved-operation handles from Arc to Rc: the v1
  generator never crosses threads (mirroring v0.4's existing Rc use), so
  the atomic refcount traffic on the per-span hot path was pure overhead.
- Hoist rmp and base64 into the workspace root Cargo.toml per the
  project preference for dependency declarations.
@ajgajg1134
ajgajg1134 marked this pull request as ready for review September 18, 2026 16:57
@ajgajg1134
ajgajg1134 requested a review from a team as a code owner September 18, 2026 16:57
@ajgajg1134
ajgajg1134 requested a review from blt September 18, 2026 16:59

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

ℹ️ 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 thread lading_payload/src/trace_agent/v1.rs
Comment thread lading_payload/src/block.rs Outdated
Comment thread lading_payload/src/trace_agent/v1.rs Outdated
Comment thread Cargo.toml Outdated
Probe the maximum block budget directly before rejecting a block cache
configuration, reserve chunk capacity fallibly so absurd chunks_per_payload
values error instead of panicking, switch the v1 example to a fixed
timestamp anchor, and move base64 features to the point of use.

@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: 202da9f637

ℹ️ 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 thread lading_payload/src/block.rs Outdated
ajgajg1134 and others added 2 commits September 18, 2026 13:42
Bump rustls to 0.23.45 (RFC 8446 handshake-message advisory
GHSA-2mjx-qc3c-rqvc) and chacha20 to 0.10.2 (yanked 0.10.0), both
within existing semver ranges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ejection

A single attempt at maximum_block_size can miss for serializers whose
payload size varies with the RNG draw (v1.0 trace payloads with optional
suboperations), rejecting a feasible configuration based on seed luck.
Retry the direct probe up to 1024 times so the false-rejection
probability is negligible for any feasible configuration.
@ajgajg1134
ajgajg1134 merged commit a7bb0ea into main Sep 21, 2026
31 of 32 checks passed
@ajgajg1134
ajgajg1134 deleted the trace-agent-v1 branch September 21, 2026 20:20
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