Files
zooqueenandHanzo Dev e570a16f6a telemetry: spans and the request record ship over native ZAP
The request boundary already holds everything a server span and a request
record contain, so both are structs filled from data it has: the trace, span
and parent ids it minted or joined, the route template as the name, start and
duration, an outcome-derived status, and the caller's org when a gateway
parked one. They are appended under one lock, drained once per tick and at
shutdown, and shipped as JSON in a ZAP envelope — internal/o11y declares the
batch shapes itself, no SDK, no protobuf, nothing a consumer inherits.

The keys are the collector's vocabulary, not ours. It reads a path out of
url.path / http.target / http.route / path in that order, so those are the
keys this fills; http.path, which an earlier draft shipped, fills nothing.
The contract/ module proves it the only way that stays true: it starts the
collector's real receivers, serves one real request, and asserts on the
structs those receivers produced. It is its own module because the collector
already requires zip — the proof would otherwise be a cycle.

The address is the switch, spelled O11Y_SPANS_ADDR / O11Y_LOGS_ADDR /
O11Y_METRICS_ADDR — three names because the collector binds one listener per
signal and promises no adjacency between them. The metrics name replaces
O11Y_METRICS_ZAP_ENDPOINT, which deployments had already stopped matching;
the socket preference is deleted because the collector only ever binds TCP,
so that branch could not run. Nothing stated means nothing starts: no
exporter, no node, no dial, no goroutine.

An app's own logging stays its own. Only zip's request line ships to the log
ear; teeing a program's every log line off the box is one decision for
luxfi/log to make once, for everything that logs.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 21:49:59 -07:00

31 lines
1.5 KiB
Go

// Package contract holds one proof: what a zip app exports is what the
// collector decodes.
//
// # Why it is a module of its own
//
// The proof needs hanzoai/o11y, and zip must not require it. Two reasons, both
// measured rather than assumed. It is circular — o11y already requires zip, so
// zip requiring o11y leaves neither end able to release without the other. And
// it is expensive for everyone else: adding o11y to zip's own module takes its
// go.sum from 147 lines to about 720 and its module graph past a thousand
// modules, a cost paid by every program built on zip in order to run a test that
// belongs to zip alone.
//
// A module here pays neither. Nothing imports this package, `go build ./...` and
// `go test ./...` in zip do not descend into it, and no consumer of zip inherits
// a line of it. CI runs it as its own step, which is where the cost belongs.
//
// # What it proves
//
// The two ends re-declare one contract: zip's internal/o11y package and the
// collector's pkg/zapreceiver and pkg/zaplogreceiver each state the batch shapes
// in their own types, so that a collector can be deployed without rebuilding
// every service that reports to it. Two declarations need something holding them
// together, and transcribing field names into a test only proves that a
// transcription matches itself.
//
// So this test runs the real receivers, has a real zip app answer a real
// request, and reads what the receivers decoded. A field renamed on either side
// fails it, and a field zip invents fails it too — it arrives as nothing.
package contract