How do I trace a Pydantic AI agent deployed to a Kubernetes deployment?
Rius traces a Pydantic AI agent on Kubernetes with one rius.init() call at process start and Agent.instrument_all(True), with no wrappers. Pydantic AI emits the run, model request and tool spans itself, and Rius computes cost from them. Call client.shutdown() when the pod receives SIGTERM, so the last spans leave and the Agents page shows a clean stop rather than a crash.
How it works
- Where init goes.
rius.init()installs the global OpenTelemetry tracer provider.Agent.instrument_all(True)then tells Pydantic AI to emit its spans through that provider. Call both once, in the module your container starts, before any agent runs. Do not callinit()per request. - What arrives. Each agent run is a span named
invoke_agentfollowed by the agent name, and it is the root only when nothing wraps it, such as anobservearound your handler. Each model request is achatgeneration with tokens and cost. Each tool call is anexecute_toolspan. See the Pydantic AI integration for the full mapping. - How spans leave. Spans queue in memory and export in batches from a background thread, so the network is never on your agent’s hot path. Kubernetes stops a pod with
SIGTERM, and Python’s default response ends the process without running its exit hook, so you handle the signal. The reliability page covers the pipeline in full. - Heartbeat. The heartbeat is on by default and needs credentials. Each process pings under its own instance ID, so each pod shows up as its own instance on the Agents page.
Set it up
- Add the packages to your image. No extra is needed for Pydantic AI.
pip install glassflow-rius pydantic-ai- Call
init()at module level in your entry point, then switch on Pydantic AI instrumentation. Scope each run withrius.session()and your conversation id so one conversation is one session.
import signal
import sys
import rius
from pydantic_ai import Agent
client = rius.init(service_name="my-agent") # reads RIUS_API_KEY from the environment
Agent.instrument_all(True)
def on_sigterm(signum, frame):
client.shutdown() # drain queued spans, send the stopped heartbeat
sys.exit(0)
signal.signal(signal.SIGTERM, on_sigterm)
support_agent = Agent(
"openai:gpt-5-mini",
instructions="Answer support questions; check accounts with the tool.",
)
@support_agent.tool_plain
def account_status(email: str) -> str:
"""Fetch a customer's account standing by email."""
return lookup(email)
def handle_ticket(conversation_id: str, ticket: str) -> str:
with rius.session(conversation_id):
result = support_agent.run_sync(ticket)
return result.output-
Name the agent. Two names matter:
service_nameininit(), orRIUS_SERVICE_NAME, is theservice.nameof the agent.RIUS_AGENT_NAMEis the name the agent is grouped under on the Agents page, and it defaults to the service name.- The agent’s own name in Pydantic AI sets the span name. Pass
name="support_agent"toAgentfor a root span namedinvoke_agent support_agent.
Set the same service name on every replica so the pods group as one agent. Set
RIUS_MAIN_AGENT_VERSIONper rollout, for example to your image tag. It is the version the Agents page compares. -
Pass the key through a Secret instead of hard-coding it. Without credentials the heartbeat cannot run, and the agent is marked no heartbeat with no instances. Store the API key in a Secret named
riuswith the keyapi-key, and reference it from the container. Kubernetes sendsSIGTERMand waitsterminationGracePeriodSeconds(30 by default) beforeSIGKILL. A batch export can take up to 30 seconds by default, so give the pod enough time.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-agent
spec:
replicas: 2
selector:
matchLabels:
app: my-agent
template:
metadata:
labels:
app: my-agent
spec:
terminationGracePeriodSeconds: 60
containers:
- name: agent
image: registry.example.com/my-agent:1.4.0
env:
- name: RIUS_SERVICE_NAME
value: my-agent
- name: RIUS_API_KEY
valueFrom:
secretKeyRef:
name: rius
key: api-key-
Shut down cleanly. The SDK installs no signal handlers, so the handler above drains pending spans and sends the final
stoppedheartbeat. If your server already handlesSIGTERM, such as uvicorn, callclient.shutdown()in its shutdown hook instead, for example a FastAPI lifespan. Start the process in exec form so it receives the signal as PID 1. ASIGKILLsends nostoppedping and loses the spans still queued, and the instance reportsgone. Never callflush()inside a request handler, because it blocks until the export completes. -
Verify. Send one request that calls a tool, then open the trace in the console. You should see the
invoke_agentroot,chatgenerations with model, tokens and cost, andexecute_toolspans.
What you get
- A trace for every agent run, with spans named after your agent, models and tools, and tokens plus a cost estimate on each model request.
- One agent on the Agents page for all replicas, with one instance per pod and a version you can compare across rollouts.
- A clear difference between a pod that stopped cleanly and one that crashed or was killed, once
shutdown()runs on stop. - Conversations grouped as sessions, from the
rius.session()scope around each run. - Costs are list-price estimates in US dollars from LiteLLM’s public model-price catalog, not billed amounts.
- Pydantic AI’s own
InstrumentationSettings, such asinclude_content, still decides what prompts and tool arguments the spans carry.
When something else fits
- If your runtime freezes the process between calls, such as Lambda-style functions, flush at the end of every invocation and turn the heartbeat off. The reliability page has the details.
- If you run a different framework in the pod, the Integrations page lists the setup for each one.
- If you can’t add the Rius SDK to the image, Pydantic AI also works with a plain OpenTelemetry setup pointed at the ingest endpoint.