Skip to content

Correlate audit events to the trace that caused them, in all five languages - #3

Merged
brentrager merged 1 commit into
mainfrom
audit-trace-correlation
Aug 15, 2026
Merged

Correlate audit events to the trace that caused them, in all five languages#3
brentrager merged 1 commit into
mainfrom
audit-trace-correlation

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

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 ride in a new envelope, outside the hash

{"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 earlier 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
  • TS and Rust re-run the entire 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

Lang Mechanism
TS optional peer dep behind a guarded dynamic import
Rust off-by-default otel feature (verified absent from cargo tree --no-default-features)
Python guarded import + [otel] extra
Go API only — pinned v1.35.0 because CI pins Go 1.22 and v1.36+ declare go 1.23 (verified under GOTOOLCHAIN=go1.22.12)
.NET no new packageActivity.Current suffices

No 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 AuditEvent first, 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 tracing span before Context::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.

…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-bot

changeset-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f63c7c8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/audit Minor

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

@brentrager
brentrager merged commit f05a9ab into main Aug 15, 2026
1 check passed
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