Skip to content

Quickstart

Tracing ships in the metrana package; nothing extra to install. Set METRANA_API_KEY as for logging. A tracer is anchored either to a training run, whose traces carry the policy step, or to a project alone, for inference. The two snippets below are the two cases.

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, with its credentials and settings
messages = [{"role": "user", "content": "How many legs do three spiders have?"}]
with tracing.step(100): # the policy step every trace in the block belongs to
with tracing.agent_trace("rollout", task_input=messages[-1]["content"]) as trace:
with tracing.llm_call("policy", prompt=messages) as llm:
reply = model(messages) # your call
llm.completion(reply.content)
llm.token_count = reply.usage.total_tokens
with tracing.tool_call("calculator", input={"expr": "3 * 8"}) as tool:
tool.output({"value": 24})
trace.final_output("24")
trace.set_attributes({"reward": 1.0, "correct": True})
tracing.close()
metrana.close()

What happened, in order:

  1. tracing.init() built a tracer for the run metrana.init opened.
  2. agent_trace opened a trace rooted at a SUB_AGENT node named rollout, at policy step 100. Its task input was sent with the open.
  3. llm_call opened a child span with its prompt on the open; leaving its block sent the completion and the token count on the close. tool_call did the same with its input and output.
  4. Leaving the trace’s block sent its close: the final output, the attributes, status OK, and the totals — 2 spans, and the LLM call’s tokens.
  5. tracing.close() flushed the stream and stopped the tracer; metrana.close() did the same for the run.

Had an exception escaped any block, that span would have closed with status ERROR and an exception attribute, and the exception would still have propagated to your code.

A serving process has no run and no step, so there is no metrana.init: the tracer is initialized against the project, and each request roots at the model call.

import metrana.tracing as tracing
tracing.init(workspace_name="my-team", project_name="agents", run_name=None) # project only
def handle(messages):
with tracing.model_trace("chat", prompt=messages, attributes={"model": "claude-y"}) as trace:
reply = model(messages)
trace.completion(reply.content)
trace.token_count = reply.usage.total_tokens
return reply
...
tracing.close() # at shutdown

The root is the model call itself, so its prompt and completion are the trace’s own bodies. Tool calls the model made nest beneath it as in the training case. See Inference traces.

Traces land asynchronously. A few seconds later, with the query extra installed:

from metrana.query import QueryClient, AttrFilter
client = QueryClient(organization="my-org", workspace_name="my-team", project_name="agents")
# The run's traces ...
traces = client.fetch_spans(runs="grpo-7b-v2", filter=AttrFilter("reward") >= 1.0, attributes=["reward"])
trace_id = traces.iloc[0]["trace_id"]
tree = client.fetch_trace_spans(trace_id, run="grpo-7b-v2") # root first, then spans by id
bodies = client.fetch_span_payloads(trace_id, tree["span_id"].tolist(), run="grpo-7b-v2")
# ... and the project's own inference traces: no run named.
chats = client.fetch_spans(filter=AttrFilter("model") == "claude-y")
prompt = client.fetch_span_payloads(chats.iloc[0]["trace_id"], run=None)

See Searching traces and Reading a trace.