Skip to content

Traces

A trace is one agent run. Spans nest underneath it in the order they executed, so reading a trace top to bottom tells you what the agent actually did — not what the prompt asked it to do.

Span kind Emitted when Key attributes
run Root span, once per run agent, revision, environment, status
model Each model call model, input/output tokens, stop reason, latency
tool Each tool call tool name, MCP server and version, side-effect class, status
retrieval Each MCP retrieval call result count, filters applied
custom You call ctx.trace.span() whatever you set
Terminal window
velerion traces show run_01HQ8W3ZK7 --tree
run support-triage rev 42 prod ok 4.1s 6,240 tok
├─ model claude-opus-5 1.2s 1,880 tok stop: tool_use
├─ tool postgres.query 0.4s ok
│ velerion/postgres@2.4.1 read
├─ model claude-opus-5 1.9s 3,910 tok stop: tool_use
├─ tool escalate 0.5s error PagerDuty 429
├─ model claude-opus-5 0.1s 450 tok stop: end_turn
└─ custom record_triage 0.1s ok

Two things to read off this immediately. The escalate tool returned a 429 and the run still completed — the model saw the error and carried on, which is usually right but means a failed page will not show up as a failed run. And 63% of the tokens are in the second model call, because the query result was appended to the context in full.

tool({
name: 'record_triage',
input: z.object({ ticketId: z.string(), triage: Triage }),
async run({ ticketId, triage }, ctx) {
return ctx.trace.span('record_triage', async (span) => {
span.setAttribute('ticket.id', ticketId);
span.setAttribute('triage.urgency', triage.urgency);
const result = await db.tickets.update(ticketId, triage);
span.setAttribute('db.rows_affected', result.rowCount);
return { ok: true };
});
},
});

Attribute values are truncated at 4 KB and must be primitives. Set attributes you would filter or group by; anything you only want to read once belongs in a log line.

Terminal window
# Runs that called a tool which failed, in the last hour.
velerion traces list --agent support-triage --since 1h \
--filter 'tool.status = "error"'
# Slow runs, grouped so you can spot a pattern.
velerion traces list --since 24h --filter 'run.duration_ms > 8000' --json \
| jq -r '.[] | "\(.revision) \(.duration_ms) \(.id)"' | sort -n -k2

Supported operators are =, !=, >, <, contains and exists. Filters run server-side against indexed attributes; custom attributes are indexed from the first run that sets them, so a brand new attribute will not match historical traces.

Velerion emits OpenTelemetry. Point it at any OTLP collector and traces arrive alongside the rest of your services:

Terminal window
velerion env set OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.internal:4317 --env prod
velerion env set OTEL_EXPORTER_OTLP_HEADERS='authorization=Bearer ...' --env prod

Export is additive: traces continue to be retained in Velerion for the 30-day window regardless. If your collector is unreachable, spans are dropped rather than buffered — export is not a durable pipeline and should not be the only copy of anything you need.