Skip to content

Reading a trace

Once fetch_spans has found a trace, two calls read it whole: the tree, then the bodies.

tree = client.fetch_trace_spans(trace_id, run="grpo-7b-v2") # run=None for a project trace
span_id parent_span_id ordinal kind op_name status token_count
0 0 0 0 sub_agent rollout ok 812
1 1 0 0 llm_call policy ok 620
2 2 0 1 tool_call search ok 0
3 3 2 0 artifact results.json ok 0
4 4 0 2 sub_agent verifier ok 192
5 5 4 0 llm_call verifier.policy ok 192

Every span of the trace, root first and then by span id, which is creation order. The columns are those of fetch_spans, and attributes= adds attribute columns the same way. Build the tree by attaching each row to its parent_span_id (0 is the root) and ordering siblings by ordinal. A span with status running has not been closed: still in progress, or its producer stopped before closing it.

from metrana.query import TracePayloadKind
payloads = client.fetch_span_payloads(trace_id, tree["span_id"].tolist(), run="grpo-7b-v2")
payloads[(1, TracePayloadKind.LLM_PROMPT)].body # the prompt, as text (JSON if it was logged as a list)
payloads[(1, TracePayloadKind.LLM_COMPLETION)].body
payloads[(0, TracePayloadKind.FINAL_OUTPUT)].body # the root's own bodies live on span 0
payloads[(0, TracePayloadKind.ATTRIBUTES)].body # {"reward": 0.73, "rollout": "A"}

fetch_span_payloads returns a SpanPayloads mapping (span_id, kind) to a SpanPayload, for the spans asked (span_ids is one id or several; the default 0 is the root) and the kinds asked (kinds=, every kind logged when empty). A kind the span never logged is simply absent.

Kind Where it appears Body
LLM_PROMPT, LLM_COMPLETION LLM-call spans, and a trace rooted at an LLM call (span 0) text
TOOL_INPUT, TOOL_OUTPUT Tool-call spans, and a tool-rooted trace text
THOUGHT Thought spans text
TASK_INPUT, FINAL_OUTPUT Span 0 of an agent-shaped trace text
INPUT, OUTPUT Any node: sub-agents, iterations, functions, @op calls text
ATTRIBUTES Any node a dict of the logged attributes

Text bodies are strings; what was logged as a list or dict comes back as its JSON, so json.loads restores it. SpanPayload also carries data_length (the whole body’s size as the logger stated it), truncated (ingestion cut it at the 16 MiB cap) and complete.

Payloads are read in windows of 16 MiB and stitched, so a long completion arrives whole; at most 100 spans are read per request, which the method batches for you.