Every Verdict Carries A Trace
A readiness pass on the stand that started with less than half green, the defects it caught before production, telemetry built from almost nothing, and a QA gate that rejects any verdict without a trace id.
๐ Hi, I'm Anton - a software engineer. These days I work on a platform: contracts, generated code, data paths and the tooling around them. Running notes live on my GitHub: github.com/brilliant-almazov.
The readiness pass, the fixes, the case file, the verdict row and the gate are real and checked by QA. The platform's telemetry contract is in review. Three matrices and a provider stub are proposals; two reds are still open.
This is part 6 of the series on testing cross-zone, cross-datacenter and cross-provider behaviour on a laptop. Maybe you already do it better, maybe you'd do it another way - this is how it is going, and where it stops.
Less than half green
Part 5, How Others Test Cross-Zone Locally, ended on a point from Jepsen: a cut is not a test, the check of the result is. This part is about our check - what a verdict looks like, and what makes it trustworthy.
It started with a readiness pass. Before any failure is injected, a stand has to be ready: services up, routes answering, logins working. QA counted the readiness checks on the local stand. At first, less than half were green.
That is not a bad number. It is the reason a local stand exists: everything red there was red before production, not in it.
What the stand caught before production
Three of the reds are worth telling.
Registration returned 500. A step in the image build stripped developer tools to keep the image small, and took with it a package that the service needed at run time. The fix was one line. Registration in the monolith is now accepted, and the confirmation mail is caught by a local mail catcher instead of going anywhere.
The admin was unreachable. Its router was on the wrong port, and the admin UI returned 502. Both are fixed; the UI answers 200.
The admin let in a fake developer user. Without a session cookie, the admin's auth check fell through to a development user instead of refusing. It now answers 401. I describe it only at this level, and it is closed.
None of the three would have been found by unit tests. Each needed the whole thing up, wired the way production is wired.
Telemetry from almost nothing
The stand's observability was almost empty, and a verdict without telemetry is an opinion. What was added:
-
envandservicelabels on every metric series, with a switch; - Loki and a log shipper;
- tracing in the environment router, with
traceparentwritten to the access log; - a check command that says whether all of the above is there.
Every call now carries a trace id from the environment router, exported over OTLP to the trace store. Two services still write no spans of their own, so their part of a trace is a gap; that is open.
The platform side
The platform change that makes this uniform is in review, not shipped. By design:
- one environment name,
APP_ENV, feeds theenvlabel of metrics, the path in the configuration and secret store, anddeployment.environment.nameof traces; - identity labels
service,env,instance,versionare set once, at the metrics exporter, so every series carries them without any service adding them; - generated dashboards get
envandserviceselectors first; - a log contract for every line:
{
"timestamp": "<ISO 8601>",
"message": "<text>",
"service": "<service>", "env": "<env>", "instance": "<instance>", "version": "<version>",
"trace_id": "<from the context>", "span_id": "<from the context>"
}
Written to stdout, never sampled.
A case is a file
A test case on the stand is one YAML file. It says what is tested, how badly a failure hurts, how it behaves in regression, and how to undo it:
id: SC-<NNN>
title: <what is checked>
weight: red | orange | yellow
regression: must-pass | may-drift | known-broken
surface: <host variable>
account: <role>
preconditions: [<fixture>]
steps:
- { method: POST, path: "<host>/<path>", body: <verbatim> }
expectation: { status: <code>, error_class: <machine reason>, fields: [...], read_after_write: true }
rollback: snapshot โ act โ restore
A declarative runner executes the file and writes every request and response to an io.jsonl journal.
The regression field is the honest one. known-broken says out loud that a case is red on purpose, so a red run is not a surprise and a green one is.
A verdict is a row
The result of a case is one row, and the row has a fixed shape:
| NN ยท case | โ
โ ๐ก โธ | "<status> โ what came, what was expected, where the decision is wrong" | trace_id <32 hex> |
The third column is a sentence with three parts, not a status code. A partial result carries its share: "N of M = X %". The fourth column is the trace.
No trace, no verdict
The QA gate rejects a verdict row without a trace_id. Not a warning - the row does not go in.
Where a service gives no trace at all, the row may carry an X-Request-Id with a reason - and that gap is closed later by tracing at the proxy, not by writing more text. The rule is the point: anyone reading a verdict can open the exact request that produced it.
One more piece is planned: the scenarios stay declarative YAML on the same runner, and a separate trace-analysis tool, in Rust with Polars, pulls the traces from the trace store, checks the expectations, flags anomalies - spans that hang, retries without a bound, errors hidden behind a 2xx - and hands the agent only a compact summary.
Images and data
Two smaller things keep the stand honest. Each service's image source is chosen per service, dev or tag:<version>, built read-only from a sibling clone, with one extra layer for stand fixes on top.
The data is synthetic, generated from the archives of earlier runs - no production dataset. The generator is deterministic and idempotent: a repeat run adds nothing.
Proposals
Three forms from QA are proposals, not built:
- a role ร flow matrix - roles root, admin, specialist, user, no token and a broken token, against login, token, the webhook-to-cache binding, reading the main project, writing, and admin; each cell holds the status the design expects - 200, 403 with a machine reason, 401 - and a trace;
- a daemon ร queue failure matrix - a row per service, daemon (from its manifest), queue and action (stop, kill, delay, restart); recorded: what piled up, what was lost (any loss is โ), dead-letter and retries per the contract, time to drain. No thresholds - the owner set none; facts only;
- a local provider stub - network aliases on the providers' hostnames, which are literals in config and code; TLS from a local CA mounted in the trust store; daemons on an internal network with no way out; provider callbacks posted from the stub to the service's webhook. Its acceptance line: "went to the stub, 0 outbound connections", with the stub's counter and a trace.
Still red
Two reds are open, named as open:
- the binding from a webhook through the monolith to the cache is not measured yet;
- two services write no spans of their own.
What I do not claim
- Not that the stand is all green. It started less than half green; the admin block is mostly green now, two reds are open.
- Not that the platform's telemetry contract is shipped. It is in review.
- Not that the matrices or the provider stub exist. They are proposals.
- Not that every service traces itself. Two do not yet.
What of this is method
- Ready before broken. A readiness pass comes before any chaos.
- A verdict is a sentence and a trace. What came, what was expected, where the decision is wrong - and the request that proves it.
- No trace, no verdict. The gate refuses, not the reviewer.
Cross-anything on a laptop - Part 6. Next: Clouds On A Laptop.
If your test reports link to traces, who checks the link is there? If you mark cases known-broken, how long do they stay that way? If you found a defect only once the whole stand was up, what was it? What would your QA gate refuse first?











Top comments (0)