Skip to content

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

Draft
ajgajg1134 wants to merge 7 commits into
mainfrom
trace-agent-v1
Draft

ajgajg1134 wants to merge 7 commits into
mainfrom
trace-agent-v1

Conversation

@ajgajg1134

@ajgajg1134 ajgajg1134 commented Sep 11, 2026

Copy link
Copy Markdown

Summary

Implement generator support for emitting DD v1 message pack tracer payloads
Added a "known good" example from an application using dd-trace-go emitting v1 payloads.

Notes for reviewers

Checklist

  • ci/fmt
  • cargo clippy
  • cargo nextest run --workspace --exclude sheepdog (504 passed)
  • Fingerprint corpus regenerated + verified reproducible (ci/fingerprint policy followed)
  • New example passes lading config-check

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 3 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>
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