rabadon

overview / commands / rabadon export

rabadon export --otlp [--days N]

Emit the ledger as OpenTelemetry OTLP/JSON traces on stdout: one trace per session, one row per tool call, refusals as ERROR spans, GenAI semantic-convention token attributes where token counts exist. Drop it into Jaeger, Grafana Tempo, Honeycomb, or Langfuse's OTLP endpoint.

maturity: stable — shipping and tested

What a trace is. The session id the hook was handed, which the gate writes as sess. It used to be pipe — spelled <project>:session and not one: it is the directory. 227 of them cover nine days of the author's machine and (withheld):session alone spans 214.1 hours, because every session and every subagent that ever ran in that folder wrote into it. That shipped as a single 24,056-span "trace", which no viewer renders as anything. A line with no session id still falls back to its pipe, and one with neither to the file it was read from; every span says which of the three it got in rabadon.export.trace_basis, so a trace that is a folder's history cannot pass for a session.

What a span is. One event — every event. A tool call, though, is two events: STEP_START when the gate admits it and STEP_OK when it returns. Both hooks are handed the same tool_use_id, which the gate writes as call, so the STEP_OK becomes the interval [START.ts, OK.ts] and the STEP_START becomes its child (parentSpanId) — one top-level row per call, with nothing deleted to get it. The interval is gate-admitted to returned, so a call that waited on a human approval carries that wait: it is how long the call held the session, not how long the tool computed. A pair is refused, and both ends stay honest points, when either end is undated, when the OK predates the START, when the two ends are in different sessions, or when one end never arrived. rabadon.span.basis says pair or dur_ms on anything that got a width, and rabadon.span.start_source names the ledger line the start was read off, so the join is checkable against the bytes.

This only works forward: the ids arrived on 4 August 2026 and no earlier line has them. On the 86,881 lines already on disk, zero carry call, so those spans are exactly what they were.

Attributes on a priced event:

attributefromnote
gen_ai.system—anthropic
gen_ai.request.modelmodelthe tier that actually ran
gen_ai.usage.input_tokensin, else tokensIn
gen_ai.usage.output_tokensout, else tokensOut, else tokensthe gate's Stop ledger writes tokens alone and its value is the session's cumulative output count — cumulative, so a backend must not sum it across spans
rabadon.usd_e6usd_e6micro-dollars, integer, the record
rabadon.cost_usdusd_e6USD double, a rendering of the same number

Cost stays in rabadon's namespace on purpose: OpenTelemetry's GenAI conventions define token counts and expect a backend to derive money from them, so there is no gen_ai.usage.cost to fill and rabadon does not squat one.

Those key names are the ones rabadon-gate and rabadon-pipeline actually append to the spool. Until 1 Aug 2026 the exporter read tokensIn/tokensOut — real names, but they live in .rabadon/state.json and never on a spool line — so on a ledger of 1366 token-bearing events, zero spans carried a gen_ai.* attribute while this page advertised them. The suite was green because the fixture was hand-written in the reader's key names. native/export_test.sh arms 10 and 11 now build their fixtures by running the gate and the loop.

Drills are excluded here by the same four rules rabadon usage uses (the emit tag, a fleet-/doctor-/drill- session id, rabadon's own bench and demo pipes, and events inside a drill's 2-minute window), one shared predicate, so the refusal count you export is the refusal count you read locally. Nothing leaves the machine unless you pipe it out.

Flags: --otlp (the only format today, required) · --days N.

Exit: 0 · 2 --otlp not passed.

rabadon export --otlp | curl -s localhost:4318/v1/traces \ -H 'content-type: application/json' --data-binary @-

commands

rabadon usage

questions

Does anything leave my machine?