Skip to Content

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 call init() per request.
  • What arrives. Each agent run is a span named invoke_agent followed by the agent name, and it is the root only when nothing wraps it, such as an observe around your handler. Each model request is a chat generation with tokens and cost. Each tool call is an execute_tool span. 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

  1. Add the packages to your image. No extra is needed for Pydantic AI.
pip install glassflow-rius pydantic-ai
  1. Call init() at module level in your entry point, then switch on Pydantic AI instrumentation. Scope each run with rius.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
  1. Name the agent. Two names matter:

    • service_name in init(), or RIUS_SERVICE_NAME, is the service.name of the agent. RIUS_AGENT_NAME is 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" to Agent for a root span named invoke_agent support_agent.

    Set the same service name on every replica so the pods group as one agent. Set RIUS_MAIN_AGENT_VERSION per rollout, for example to your image tag. It is the version the Agents page compares.

  2. 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 rius with the key api-key, and reference it from the container. Kubernetes sends SIGTERM and waits terminationGracePeriodSeconds (30 by default) before SIGKILL. 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
  1. Shut down cleanly. The SDK installs no signal handlers, so the handler above drains pending spans and sends the final stopped heartbeat. If your server already handles SIGTERM, such as uvicorn, call client.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. A SIGKILL sends no stopped ping and loses the spans still queued, and the instance reports gone. Never call flush() inside a request handler, because it blocks until the export completes.

  2. Verify. Send one request that calls a tool, then open the trace in the console. You should see the invoke_agent root, chat generations with model, tokens and cost, and execute_tool spans.

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 as include_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.

Still stuck? Get help

Last updated on