Correlate audit events to the trace that caused them, in all five languages - #3
Merged
Conversation
…guages
An audit record could not be joined to the request that produced it — no
trace_id existed anywhere, in any language. "Who did this, and what was the
system doing at the time" was two separate investigations.
The ids travel in a NEW WIRE ENVELOPE, deliberately OUTSIDE the hashed payload:
{"event":{…sealed event…},"traceId":"…","spanId":"…"}
Audit events are hash-chained, and spec/parity-corpus.json is the byte-exact
contract both the TS and Rust serializers must reproduce — the file itself warns
not to "fix" a divergence by editing hashes. Putting traceId inside the event
would have changed EVERY hash, either breaking cross-store verification or
forcing a versioned canonicalizer migration. Brent chose the envelope: the chain
and every existing fixture stay byte-identical, at the cost of traceId not
itself being tamper-evident. That tradeoff is deliberate and worth restating if
anyone later proposes moving it inside.
In every language the hash is computed BEFORE the envelope exists, so trace
context cannot structurally reach the preimage.
Evidence the chain is untouched, checked on raw bytes (not grep — RTK fabricated
a grep count today):
- spec/parity-corpus.json sha256 684135c9… identical to HEAD, 8 expectedHash
entries unedited
- every hash/canonical/schema source in all five languages byte-identical
- both TS and Rust re-run the WHOLE corpus INSIDE an active span and assert
byte-exact canonical output and hashes; Go, Python and .NET assert the same
over their fixtures in both states
OTel stays optional everywhere: TS optional peer dep behind a guarded dynamic
import, Rust an off-by-default `otel` cargo feature (verified absent from
`cargo tree --no-default-features`), Python a guarded import plus an `[otel]`
extra, Go the API only. .NET needed no new package — Activity.Current is enough.
Go pins otel v1.35.0 because CI pins Go 1.22 and v1.36+ declare go 1.23;
verified by building under GOTOOLCHAIN=go1.22.12.
No active span means NO ids — omitted entirely, never an all-zero id or empty
string.
Mutation-checked in all five languages; the mutation that moves the ids inside
the hashed event is caught everywhere. It also exposed a Rust test too weak to
notice: it deserialized into AuditEvent first, which silently DROPPED the
injected field — now it asserts on raw wire bytes. One Rust mutation survived
and is reported rather than hidden: reading the tracing span before
Context::current() is redundant with tracing-opentelemetry 0.33. Kept as
insurance (same call as @smooai/fetch); its companion mutation proves the other
half is load-bearing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: f63c7c8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
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.
An audit record could not be joined to the request that produced it — no
trace_idexisted anywhere, in any language. "Who did this, and what was the system doing at the time" was two separate investigations.The ids ride in a new envelope, outside the hash
{"event":{…sealed event…},"traceId":"…","spanId":"…"}Audit events are hash-chained, and
spec/parity-corpus.jsonis the byte-exact contract both the TS and Rust serializers must reproduce — the file itself warns not to "fix" a divergence by editing hashes. PuttingtraceIdinside the event would have changed every hash, either breaking cross-store verification or forcing a versioned canonicalizer migration.Brent chose the envelope: the chain and every existing fixture stay byte-identical, at the cost of
traceIdnot itself being tamper-evident. That tradeoff is deliberate and worth restating if anyone later proposes moving it inside.In every language the hash is computed before the envelope exists, so trace context cannot structurally reach the preimage.
Evidence the chain is untouched
Checked on raw bytes, not grep (RTK fabricated a grep count earlier today):
spec/parity-corpus.jsonsha256684135c9…identical to HEAD; 8expectedHashentries uneditedOTel stays optional everywhere
otelfeature (verified absent fromcargo tree --no-default-features)[otel]extraGOTOOLCHAIN=go1.22.12)Activity.CurrentsufficesNo active span means no ids — omitted entirely, never an all-zero id or empty string.
Mutation testing
Mutation-checked in all five languages; the mutation that moves ids inside the hashed event is caught everywhere.
It also exposed a Rust test too weak to notice: it deserialized into
AuditEventfirst, which silently dropped the injected field. It now asserts on raw wire bytes.One Rust mutation survived and is reported rather than hidden: reading the
tracingspan beforeContext::current()is redundant with tracing-opentelemetry 0.33. Kept as insurance (same call as@smooai/fetch, where the failure mode is a silent production no-op); its companion mutation proves the other half is load-bearing.All gates exit 0 in every language.