Observability: tracing runs with Logfire
Instrumentation is a capability that records every model call and tool call of a run as OpenTelemetry spans, so you can see what the agent did, in what order, and how long each step took.
Last updated: 28 Sep, 2026 · Pydantic AI 2.51
Reading the message list, from the requests-and-responses lesson, shows one run. Instrumentation records the same events as spans that a viewer such as Pydantic Logfire draws as a timeline, across every run in production. The desk you built through Harness Coder: a ready-made coding agent makes several model and tool calls per ticket, and a trace is how you see them.
Turning instrumentation on
Instrumentation is a capability. InstrumentationSettings chooses where the spans go; a tracer_provider you build sends them wherever you point it:
from pydantic_ai.capabilities import Instrumentation
from pydantic_ai.models.instrumented import InstrumentationSettings
# send spans wherever `tracer_provider` points; omit it to use the global one
settings = InstrumentationSettings(tracer_provider=provider)
agent = Agent("test", capabilities=[Instrumentation(settings)])Capturing the spans of a run
An in-memory exporter keeps the spans in the program, so a run of the test model shows every span it produced, with no token and no network:
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from pydantic_ai import Agent
from pydantic_ai.capabilities import Instrumentation
from pydantic_ai.models.instrumented import InstrumentationSettings
spans = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(spans))
agent = Agent("test", capabilities=[Instrumentation(InstrumentationSettings(tracer_provider=provider))])
@agent.tool_plain
def order_status(order_id: str) -> str:
"""Where an order is."""
return "shipped"
agent.run_sync("Where is A-1001?")
for span in spans.get_finished_spans():
print(span.name)What the spans show
- invoke_agent agent is the whole run, the parent of everything else.
- chat test is a request to the model, named after the model. It appears twice: once to decide the tool call, once to write the answer after the tool returned.
- execute_tool order_status is the tool running, named after the tool.
- The order is completion order: the child spans finish before the parent, which is why
invoke_agentprints last.
Sending traces to Logfire
In production you send the spans to a viewer instead of a list. Pydantic Logfire is the one built for this: two lines set it up, and every agent from then on is traced with its cost and timing:
import logfire
logfire.configure() # reads the token from the environment
logfire.instrument_pydantic_ai() # trace every agent from here onRun logfire auth once to sign in; logfire.pydantic.dev is where the project and its token are made. This step needs that token to export, so it is shown, not run, and carries no output.
Instrumentation on vs off
| Off | On | |
|---|---|---|
| Model and tool calls | Run, but leave no record | Recorded as spans with timing |
| Where they go | Nowhere | The tracer provider you set, such as Logfire |
| Cost of the run | Unchanged | Unchanged; spans are cheap and drop if nothing collects them |
Where you use tracing
- Finding which tool call made a ticket slow.
- Seeing the exact prompt and tools sent to the model on a run that went wrong.
- Watching cost per run add up across real traffic.
logfire.configure() without a token records nothing rather than failing. If your traces never appear, check that a token or a tracer provider is set before the first run, not that instrumentation is on.Related
- Previous: Evals: scoring the agent on many tickets
- Next: Integrations: from stand-in to real backends
- See also: Evals: scoring the agent on many tickets
- Reference: Logfire integration
- Add a second tool and a ticket that uses it, and count the spans.
- Print
span.parentnext tospan.nameto see the tree. - Swap the in-memory exporter for
logfire.configure()and runlogfire authto see the run in the web viewer.
Little by little, you're building something great.