The flight recorder¶
The audit ledger answers "who did X to Y, and when?" — one durable row per operation. The flight recorder answers the next question down: "what did the connector actually send, and what came back?" For a governed dispatch, it captures the real vendor request/response traffic as an ordered set of spans and attaches them to that dispatch's audit row. The audit row stays the slim, append-only record of account; the trace is the deeper detail hanging off it, and it is captured with secrets stripped so it is safe to read back.
This guide covers what a trace records, how redaction keeps it safe, who can read it (operators and the agent itself), and how an operator turns capture on — because it is off by default.
Prerequisites, roles, and maturity
- A running backplane and a connected client (Connect clients).
- Reading a trace needs the operator role. Turning capture on or off — a per-tenant policy or a per-target override — is a governance decision and needs tenant_admin.
- The flight recorder extends the append-only audit ledger (which is GA). Capture itself is an operator opt-in: nothing is recorded until you enable it, and it can be turned off globally at any time.
What a trace records¶
Each captured dispatch produces one trace: an ordered list of spans, one per meaningful step the backplane took. Three kinds of span are instrumented, on the same single dispatch path the operation already runs — there is no parallel execution path:
| Span | What it captures |
|---|---|
| Vendor call | the outbound request a connector made — method, URL (stripped to its request line so query-string and userinfo secrets never leak), status code, duration, and the redacted, capped request/response headers and bodies. Typed connectors are instrumented alongside generic ones. |
| Composite sub-step | a composite that fans out into child dispatches joins the one parent trace, so a multi-step operation reads as a single ordered story rather than several disconnected traces. |
| JSONFlux reduction | how a set-shaped response was reduced: input rows, kept fields, output size, and the result-handle id. |
Traces are bounded so a chatty operation can never blow up storage or a reader's context. Each span body is capped (oversize truncates with a marker, it never errors), each trace is capped in total size, and a dispatch with very many spans collapses the overflow into counted per-kind groups. Capture is also best-effort by construction: it can never fail, block, or materially slow a dispatch, and the trace is persisted only after the audit row has committed. If recording breaks, the operation still returns exactly as it would have.
Redaction is fail-closed¶
A trace is only safe to read back because a secret never reaches it in the first place. Every header and body is redacted at capture, before a byte is stored, and the engine is built to fail closed:
- Header allowlist. Only enumerated known-safe headers survive.
Every
Authorization,Cookie,Set-Cookie,X-*-Token, CSRF, session, and signed-URL header is stripped unread. An allowlist is used deliberately over a blocklist, so an unknown vendor header is dropped rather than leaked. - Body-path redaction. Declarative per-connector path rules scrub known secret-bearing fields from request and response bodies, with the credential-shape detector reused underneath as a second layer.
- Hard-excluded operation families. Credential-read, session-mint, and token operations never record bodies at all, regardless of policy.
- Redaction-uncertainty signalling. Any span the engine cannot prove it fully redacted — an unparseable or binary body, malformed JSON, a body truncated mid-value — is flagged uncertain, and the whole trace inherits that flag. Uncertainty always resolves toward less exposure, never more (see the read surfaces below).
Who reads a trace, and how¶
Operators — REST and the console¶
An operator reads the trace for one audit row two ways over the same tenant-scoped read:
# The audit row id comes from the audit ledger (see the audit guide).
curl -H "Authorization: Bearer $TOKEN" \
https://meho.example.com/api/v1/audit/<audit_id>/trace
…or open the audit row in the console drawer and read its Flight recorder pane (next to Lineage). Both render exactly what was stored — the read surface never re-processes, un-redacts, or writes. Two things worth knowing:
- The operator plane sees everything the tenant captured, including
a redaction-uncertain trace — but the uncertainty is surfaced, as a
banner in the pane and a
redaction_uncertainfield in the REST body, so you can see when a trace was withheld from agents. - Absence is unambiguous. An audit row that does not exist, or lives
in another tenant, returns
404(never403— existence never leaks across tenants). An audit row that exists but has no captured trace — because capture was off, or best-effort recording skipped it — returns a200empty state.
The agent — its own trace as a result handle¶
An agent can read the trace of a dispatch it ran, but only through the
narrow-waist idiom it already uses for any large result: the trace is
materialized as a set-shaped result handle and paged with the
unchanged result_query meta-tool. No new tool is registered, no
vendor-specific name reaches the agent surface, and no raw payload enters
agent context.
The agent read is gated separately, and closes on doubt
Agent readability is a per-tenant setting independent of operator access, and it is the conservative default. A trace whose redaction could not be proven complete is withheld from the agent handle entirely — while the operator plane keeps full access to it. A secret-bearing or redaction-uncertain span therefore never reaches an agent-visible handle.
Turning capture on¶
Capture is a governance decision, so it lives on the operator plane — REST and CLI only, with no MCP tool — and it is off until an operator enables it. At dispatch time the resolver checks, highest precedence first: the global kill switch (below), then any per-target override, then the per-tenant default — which is off unless enabled, and is the true fallback. On any error it resolves to off: the recorder never captures more than it can prove it should.
Per-tenant policy (tenant_admin):
meho tenants flight-recorder-policy set \
--enabled \
--agent-readable true \
--retention-days 14
--enabledflips the tenant's capture default on or off.--agent-readable true|false|inheritcontrols whether agents may read their own traces (inheritfollows the capture default).--retention-days N(bounded 1–365) or--clear-retentionsets how long traces are kept before the reaper deletes them.
Per-target override. A single target can force capture on or off
regardless of the tenant default — useful to record just one noisy or
sensitive system — through the target's own update path (the
flight_recorder_capture field on meho targets import --update, or the
target PATCH route). It is tri-state: force on, force off, or clear back
to inherit.
Global kill switch. FLIGHT_RECORDER_ENABLED sits at the top of that
precedence and is read fail-open: set it off and the deployment captures
nothing — overriding every tenant and target setting — without failing a
single dispatch. It is the blunt instrument for turning the whole
subsystem off.
A policy change takes effect on the next dispatch — the resolver caches for a minute but each change invalidates that cache, so you never wait out a TTL or restart a pod.
Retention¶
Traces are transient by design. A reaper deletes expired traces —
headers and spans together — on a bounded sweep; the default window is
short and per-tenant configurable through --retention-days above. The
audit row is never touched by the reaper: the durable record of
account outlives the detailed trace hanging off it.
What can go wrong here¶
| Symptom | What it means | Fix |
|---|---|---|
GET …/trace returns 200 but an empty trace |
Capture was off for that dispatch, or best-effort recording skipped it — not an error. | Enable capture on the tenant (or the target) and re-run; past dispatches cannot be recorded retroactively. |
GET …/trace returns 404 |
The audit row does not exist in your tenant (a wrong id, or a cross-tenant row) — deliberately indistinguishable from a genuinely missing row. | Confirm the audit_id and that you are authenticated to the right tenant. |
An operator sees a trace but an agent's result_query cannot |
Either agent-readability is off for the tenant, or the trace was flagged redaction-uncertain and withheld from agents by design. | Check --agent-readable; a redaction-uncertain trace stays operator-only on purpose. |
| A span body ends in a truncation marker | The body exceeded the per-span cap and was truncated — working as designed, and it forces the trace's redaction-uncertain flag. | Nothing; the trace is intact up to the cap, and the truncation is why it is operator-only. |
| Nothing is ever captured, tenant policy notwithstanding | The global FLIGHT_RECORDER_ENABLED kill switch is off. |
Set it on at the deployment level; the per-tenant policy applies underneath it. |
Next: Runbooks.