How do I trace an agent built with AutoGen and deployed to a FastAPI service?
Rius traces an AutoGen agent in a FastAPI service when you call rius.init once at startup and wrap the handler in one observe call. In Python, a long-running server needs no manual flush, because spans export in the background and the exit hook drains the rest on a clean shutdown. The service name you pass to init is the name the agent appears under, and AutoGen’s own agent names show on the spans inside each trace.
How it works
AutoGen emits its own OpenTelemetry spans for agent creation, agent runs and tool calls. It sends them through the global tracer provider, which rius.init() installs. Model calls go through the OpenAI client from autogen-ext[openai], so the openai integration records them as generations with model, tokens and cost. See the AutoGen integration page for the full coverage and the Integrations overview for how auto-instrumentation turns on.
In a web service this gives you three things to decide:
- Where init goes. Call
rius.init()once, at module level, before the first request is handled. With a default (global) init, every installed integration turns on automatically. The AutoGen page’s example calls it at module level. - How spans leave. Spans are queued in memory and exported in batches from a background thread, so a request never waits on the network. In Python, a long-running server needs nothing more, and the exit hook drains pending spans on a clean shutdown.
- What the agent is called.
service_namenames the agent.RIUS_AGENT_NAMEoverrides the grouping name and defaults to the service name. Inside a trace, thenameyou giveAssistantAgentappears on itscreate_agentandinvoke_agentspans, and thenameyou giveobservenames the root span.
One observe wrapper on the entrypoint groups agent creation and invocation into a single trace per unit of work. Do not wrap AutoGen tool functions with observe. The framework runs tools in a detached context, so the wrapper’s span splits off as its own single-span trace. The framework’s execute_tool spans already cover tools.
Set it up
-
Install the SDK with the
openaiextra, plus AutoGen:pip install "glassflow-rius[openai]" "autogen-agentchat" "autogen-ext[openai]" -
Initialize once at module level, put the agent inside one
observe-wrapped async function, and await it from your FastAPI route. This is the example from the AutoGen page, with the client kept so the lifespan can shut it down:from contextlib import asynccontextmanager import rius from fastapi import FastAPI from autogen_agentchat.agents import AssistantAgent from autogen_ext.models.openai import OpenAIChatCompletionClient from rius import SpanKind client = rius.init(api_key="ri_...", service_name="my-agent") def account_status(email: str) -> str: """Fetch a customer's account standing by email.""" return lookup(email) @rius.observe(name="handle-ticket", kind=SpanKind.AGENT) async def handle_ticket(ticket: str) -> str: agent = AssistantAgent( name="support_agent", model_client=OpenAIChatCompletionClient(model="gpt-5-mini"), tools=[account_status], system_message="Answer support questions; check accounts with the tool.", ) result = await agent.run(task=ticket) return str(result.messages[-1].content) @asynccontextmanager async def lifespan(app: FastAPI): yield client.shutdown() # drain spans, send the final stopped heartbeat app = FastAPI(lifespan=lifespan) @app.post("/tickets") async def create_ticket(ticket: str) -> dict: return {"answer": await handle_ticket(ticket)} -
Pick the names.
service_name(heremy-agent) is what the agent is called in Rius, so choose a stable one per service. Keep the AutoGen agentnamemeaningful too, because it shows on thecreate_agentandinvoke_agentspans. -
Handle shutdown. Do not call
flush()inside a request handler: it blocks until the export completes, which is the stall the background exporter exists to avoid. Flush at process boundaries only. For a clean stop, the lifespan above callsclient.shutdown()after theyield. It drains the queue, stops the export thread and sends the finalstoppedheartbeat, which is how Rius tells a clean exit from a crash. -
If you run workers under gunicorn, you do nothing at fork time. The exporter re-creates its background thread in each forked child. Only a hard kill of a worker can lose its last batch, so call
flush()there if workers are killed hard. Each worker gets its own heartbeat identity, so the agent shows one instance per worker. -
To group a conversation, scope each run with
rius.session()and reuse your conversation id. Details are on the AutoGen page.
For delivery behavior in other runtimes, see Reliability.
What you get
- One trace per request, with your
observeroot as anAGENTspan, thencreate_agentandinvoke_agentspans from AutoGen. - A
ChatCompletiongeneration for each model call, with model, tokens and a list-price cost estimate in US dollars that Rius computes. You never send a cost value. - An
execute_toolspan for each tool call, with no wrappers on your tool functions. - The agent under the name you set in
service_name, with instances from heartbeats (on by default, and they need credentials). - A clean stop that shows as stopped instead of gone, when
shutdown()runs before the process ends. An unhandledSIGTERMor aSIGKILLsends nostoppedping, so the instance reports gone.
To check it, send one request that calls a tool and open the trace in the console. You should see the root span, the invoke_agent and execute_tool spans, and the ChatCompletion generation with model, tokens and cost.
When something else fits
- If the service runs in a Lambda-style runtime rather than a long-lived server, call
client.flush()at the end of every invocation and turn the heartbeat off. The Reliability page covers this. - If the API process is TypeScript rather than Python, the AutoGen integration does not apply, because it is Python only. See the Integrations matrix for what each language covers.