Skip to content

Tracing SDK overview

metrana.tracing records what an agent did: each invocation — in post-training, each rollout — becomes a trace, a tree of spans for its model calls, tool calls, thoughts, loop iterations and sub-agents, with the prompts, completions, inputs and outputs those steps carried. Traces show up next to the run’s metrics, so a reward curve can be opened into the rollouts that produced it.

import metrana
import metrana.tracing as tracing
metrana.init(organization="my-org", workspace_name="my-team", project_name="agents", run_name="grpo-7b-v2")
tracing.init() # traces of that run; credentials and settings from it
for step in range(num_steps):
with tracing.step(step):
for task in batch:
with tracing.agent_trace("rollout", task_input=task) as trace:
with tracing.llm_call("policy", prompt=messages) as llm:
llm.completion(reply)
llm.token_count = usage.total_tokens
with tracing.tool_call("search", input=query) as tool:
tool.output(results)
trace.final_output(answer)
trace.set_attributes({"reward": reward})
metrana.log("reward/mean", batch_reward)
tracing.close()
metrana.close()

Spans nest by their with blocks, and every one is enqueued the moment its block is entered, so a running rollout is already visible with its spans open. The tracer and the logger are a pair: tracing.init() after metrana.init, tracing.close() before metrana.close().

Tracing rides the same native engine as the logger, over its own stream to the ingestion service. Every span open and close is one update in that stream; the engine batches, compresses (zstd, by default for traces), retries, fails over to the disk spool and applies backpressure exactly as it does for metrics — see Lifecycle & reliability. What you write is the tree; the engine caps, packs and delivers it.

your agent ──with span:──▶ [open enqueued] ─── your code runs ─── [close enqueued: bodies, attributes, status]
└──────────── one stream, in order ────────────▶ Metrana

Two facts shape the API:

  • Inputs ride the open, outputs ride the close. A prompt, a tool’s input or the trace’s task is known when the span starts, so it is sent with the open and a running span already shows it; what the span produced is sent with the close. Attributes are sent with the close, so they can grow while the span runs.
  • Order is guaranteed by construction. The backend resolves two writes of one span by arrival order, so a close must never overtake its open. The SDK enqueues the open on entry and the close on exit, on one tracer, and the ingestion service delivers a stream in the order sent — no call sequence can produce the wrong order.
training run
├── optimizer step 100
│ ├── rollout A agent_trace — SUB_AGENT root, reward 0.73
│ │ ├── policy llm_call: prompt in, completion out
│ │ ├── search tool_call: input in, output out
│ │ │ └── results.json artifact
│ │ └── verifier sub_agent, with its own llm_call and tool_call
│ ├── rollout B
│ └── rollout C
│ ├── iteration one pass of the perceive-act loop
│ └── iteration
└── optimizer step 101

Every node has a kind (see Spans and kinds), a name you list and search by, an input and an output recorded as the bodies of its kind, attributes you filter by in the portal and the query SDK, a status (OK, or ERROR when an exception escaped its block), and a token count. The root also carries the totals the trace list sorts on: span count, iteration count and token total, computed from the tree at close, plus an error_count attribute for the spans that failed beneath it.

Instrumented code runs wherever it is called: with tracing switched off (METRANA_TRACING_DISABLED=1), with nothing initialized, or for the traces a sample_rate leaves out, the constructors hand out inert nodes that record nothing — see Switching off, and sampling.