How do I trace an agent built with CrewAI and deployed to a Kubernetes deployment?
Rius traces a CrewAI agent in a Kubernetes deployment with one rius.init() call at process start and observe wrappers around your entrypoint and tools. Call client.shutdown() when the pod receives SIGTERM so queued spans leave the process and the instance reports a clean stop. Name the agent with service_name or RIUS_AGENT_NAME, and Rius groups every replica under that name.
How it works
CrewAI 1.x drives the OpenAI client, so the openai integration captures its model calls as generations with model, tokens and cost. When LiteLLM is installed, CrewAI uses it instead and the litellm integration captures the calls. Crews and tasks produce no spans of their own, so you wrap the entrypoint and tool bodies with observe. The CrewAI integration page covers what lands in the trace, and the Integrations page lists the other frameworks.
In a Kubernetes deployment, a few points matter:
- Where init goes. Call
rius.init()once, at module level, before your code builds anAgentor callskickoff(). A defaultinit()turns on every installed integration, so it has to run before the first model call. - How spans leave. Spans queue in memory and export in batches from a background thread, so tracing never sits on your agent’s hot path.
- How the pod ends. Kubernetes sends
SIGTERM, and Python’s default response ends the process without running its exit hook. The SDK installs no signal handlers, so an unhandledSIGTERMor aSIGKILLsends no finalstoppedping and the instance reportsgone. Rius treatsgoneas the crash signal. Callingshutdown()is what produces a clean stop. - Replicas. Each process has its own instance identity. With the heartbeat on and credentials set, each replica shows as its own instance under the agent.
Set it up
-
Add the SDK and CrewAI to your image.
pip install "glassflow-rius[openai]" crewai -
Put the settings in the container environment, not in code.
RIUS_API_KEYcarries the key, read from a Secret,RIUS_SERVICE_NAMEsets the service name andRIUS_AGENT_NAMEsets the name the agent is grouped under. It defaults to the service name. The heartbeat needs credentials, so without a key an agent shows as having no heartbeat and lists no instances. SetCREWAI_TRACING_ENABLED=falsetoo, because CrewAI asks about its own tracing on first run and a container has no terminal to answer. Store the API key in a Secret namedriuswith 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 - name: CREWAI_TRACING_ENABLED value: "false" -
Initialize once at startup, shut the client down on
SIGTERM, wrap the entrypoint as anAGENTspan and trace each tool body with theTOOLkind. Put@tooloutermost andobserveon the function body.import signal import sys import rius from crewai import Agent, Crew, Task from crewai.tools import tool from rius import SpanKind client = rius.init(service_name="my-agent") # reads RIUS_API_KEY def on_sigterm(signum, frame): client.shutdown() # drain queued spans, send the stopped heartbeat sys.exit(0) signal.signal(signal.SIGTERM, on_sigterm) @tool("account_status") @rius.observe(kind=SpanKind.TOOL) def account_status(email: str) -> str: """Fetch a customer's account standing by email.""" return lookup(email) support_agent = Agent( role="Support specialist", goal="Answer support tickets accurately and concisely.", backstory="You resolve product and account questions.", tools=[account_status], llm="gpt-5-mini", ) @rius.observe(name="handle-ticket", kind=SpanKind.AGENT) def handle_ticket(ticket: str) -> str: task = Task( description=f"Resolve this support ticket: {ticket}", expected_output="A short, direct answer.", agent=support_agent, ) return Crew(agents=[support_agent], tasks=[task]).kickoff().raw -
If your server already handles
SIGTERM, 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. Never callflush()inside a request handler, because it blocks until the export completes. The Reliability page covers batching and shutdown. -
To compare releases on the Agents page across rollouts, set
RIUS_MAIN_AGENT_VERSIONto your image or release version. -
Scope each kickoff with
rius.session()and your own conversation or job id if you want runs grouped into sessions.
What you get
- A root
AGENTspan namedhandle-ticketfor each run, with the model calls beneath it as generations with model, tokens and cost. - Your named
TOOLspans in the trace. Tool-based alerts only see tools traced with theTOOLkind. - One agent entry under the name you chose, with each replica listed as an instance while its heartbeat arrives.
- A clean stop for pods that call
shutdown(), andgonefor pods that are killed hard. - Costs as list-price estimates in US dollars, computed by Rius from the model and token counts.
Generation spans carry the generic name ChatCompletion, and the crew and task structure is not visible in the trace. Wrap per-task entrypoints with observe if you need to see which task a call belongs to.
When something else fits
- If your pods run short jobs, such as a Kubernetes Job, flush explicitly before exit instead of relying on shutdown timing.
- If you also need streamed calls to carry token counts, enable usage on the underlying client, as the CrewAI page notes.
- If a different framework runs in your pods, start from the Integrations page, which links to each framework’s setup.