How do I record tool calls with the OpenTelemetry Python SDK?
Rius records a tool call from the OpenTelemetry Python SDK when the span carries gen_ai.operation.name set to execute_tool and a gen_ai.tool.name. The SDK and exporter do not set these for you, so you add them on each tool span. Rius then shows the span as a tool call in trace views and kind filters.
How it works
A tracer provider and an OTLP exporter only ship spans. They do not mark any span as a tool call. Rius classifies a span from one of two attributes, and you can set either:
gen_ai.operation.namewith the valueexecute_toolopeninference.span.kindwith the valueTOOL
Set one and Rius derives the other. Setting both, as the Rius SDKs do, is the clearest choice.
The attributes a tool span carries:
gen_ai.tool.name: the tool’s own name, never the span name.gen_ai.tool.call.id: the id of the model’s tool call this execution answers.gen_ai.tool.type: for examplefunction,extensionordatastore.gen_ai.tool.call.argumentsandgen_ai.tool.call.result: the input and output. Rius moves them toinput.valueandoutput.value, so search and filter on those keys.error.type: a low-cardinality error class, such astool_error, on a span that ended in an error.
Name the span the way the GenAI conventions ask, the operation followed by the tool: execute_tool get_weather. A tool called over MCP is an execute_tool span too. Add mcp.method.name with the value tools/call so Rius can tell MCP tools from local ones.
Tool spans carry no tokens, so they have no cost. Rius computes cost from the model name and token counts on the spans for model calls.
Set it up
- Install
opentelemetry-sdkandopentelemetry-exporter-otlp-proto-http, then register a tracer provider with the OTLP exporter. Without a registered provider, spans go nowhere. Point the exporter at Rius withOTEL_EXPORTER_OTLP_ENDPOINTset tohttps://ingest.eu.console.rius-glassflow.com,OTEL_EXPORTER_OTLP_PROTOCOLset tohttp/protobuf, andOTEL_EXPORTER_OTLP_HEADERSset toauthorization=Bearer ri_.... SetOTEL_SERVICE_NAMEto your agent’s name, because Rius groups traces by service name. The Vanilla OpenTelemetry SDKs page covers this setup in full.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
# With no arguments, the exporter reads the OTEL_EXPORTER_OTLP_* variables
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)- Wrap each tool function in a span that sets the tool attributes. Start it inside a root span for the agent run, and the tool call nests under that run in one trace. The names
get_weather,tool_call_idandrun_agentbelow are your own.
import json
from opentelemetry import trace
tracer = trace.get_tracer("my-agent")
def get_weather(city: str, tool_call_id: str) -> str:
with tracer.start_as_current_span("execute_tool get_weather") as span:
span.set_attributes({
"openinference.span.kind": "TOOL",
"gen_ai.operation.name": "execute_tool",
"gen_ai.tool.name": "get_weather",
"gen_ai.tool.type": "function",
"gen_ai.tool.call.id": tool_call_id,
"gen_ai.tool.call.arguments": json.dumps({"city": city}),
})
try:
result = "sunny, 21 C" # call your real tool here
except Exception:
span.set_attribute("error.type", "tool_error")
raise
span.set_attribute("gen_ai.tool.call.result", result)
return result
def run_agent(query: str) -> str:
with tracer.start_as_current_span("invoke_agent my-agent"):
return get_weather("Berlin", "call_1")- Run the agent once, open Traces in the console, and open the trace. The tool span appears under the run with its tool name, input and output. The span attribute reference lists every attribute Rius reads, including the message shape for model calls.
What you get
- Each tool call shows as its own span, with the tool name, input, output and timing, nested under the agent run.
- The tool span has the Tool kind, so the All kinds menu in the Spans tab finds it. The Tool Loop alert counts only spans of kind TOOL, so only tool spans marked this way can trigger it.
- A failed tool call carries
error.type, so it shows in error analysis. - An MCP tool call carries
mcp.method.name, so context attribution can report the MCP share of a trace.
The Trace LLM calls with OpenTelemetry guide shows how to send the model calls that request these tools from the same setup.
When something else fits
- If your tools run through a library that already emits tool spans, such as the OpenInference or OpenLLMetry instrumentors, check what it sets before adding spans by hand. Rius maps their conventions onto the same attributes at ingest.
- If you use the Rius SDK for Python,
observewith the TOOL kind creates theexecute_toolspan for you, so you do not set the attributes yourself.