Skip to Content

How do I set the tool call attribute on an OpenTelemetry span in Python?

Rius reads a tool call from a span that carries gen_ai.operation.name set to execute_tool, with the tool’s name in gen_ai.tool.name and the model’s call id in gen_ai.tool.call.id. In Python you set these with span.set_attributes(...) from the standard OpenTelemetry API. Rius then shows the span as a tool step in your trace, with its input and output rendered like any other span.

How it works

Rius classifies spans by two attributes, openinference.span.kind and gen_ai.operation.name. Set either one and Rius derives the other, so execute_tool becomes TOOL. Setting both keeps the span unambiguous.

The keys that describe a tool call all go on the same span:

  • 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 that this execution answers.
  • gen_ai.tool.type: function, extension, datastore and so on.
  • gen_ai.agent.name: the agent executing the tool.
  • gen_ai.tool.call.arguments and gen_ai.tool.call.result: what the tool received and returned.
  • error.type: a low-cardinality class such as tool_error, if the tool failed.

Rius moves the arguments and result onto input.value and output.value. A native input.value or output.value wins, and the two source keys are removed either way, so search on input.value and output.value.

Name the span the way the GenAI conventions ask, the operation followed by what it acted on, for example execute_tool get_weather. Rius does not require it, but it keeps traces from mixed senders readable.

The model’s request for a tool lives on the LLM span, not the tool span. There it appears as a tool_call part inside gen_ai.output.messages, and the tool’s answer is sent back as a tool_call_response part in the next model call’s gen_ai.input.messages. The id in those parts is the one you reuse in gen_ai.tool.call.id. For the full message shape, see the span attribute reference.

Set it up

  1. Register a tracer provider that exports to Rius. This is the standard Python setup from Vanilla OpenTelemetry SDKs: install opentelemetry-sdk and opentelemetry-exporter-otlp-proto-http, set the OTEL_EXPORTER_OTLP_* variables described there, then register the provider. The variables take effect only through this provider.
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)
  1. Wrap each tool execution in its own span and set the tool call attributes. get_weather stands for your own function, and the values are examples.
import json from opentelemetry import trace tracer = trace.get_tracer("my-agent") 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.call.id": "call_1", "gen_ai.tool.type": "function", "gen_ai.tool.call.arguments": '{"city": "Paris"}', }) result = get_weather("Paris") # Attribute values must be strings, numbers, booleans or lists of them span.set_attribute("gen_ai.tool.call.result", json.dumps(result))
  1. Open the tool span while the agent run’s span is current, so it nests under it and arrives as one trace. Wrap each agent run in a single root span, because the root’s name, duration and status are what run-level views show.

  2. If the tool is called over the Model Context Protocol (MCP), also set mcp.method.name to tools/call. Rius uses it to tell MCP tools from local ones when it reports how many context tokens MCP tools took.

  3. If the tool fails, call span.record_exception(...) and set error.type. The standard exception event surfaces in error analysis.

For the wider walkthrough of sending any trace, see Trace LLM calls with OpenTelemetry.

What you get

  • The span is classified as a tool step in trace views and per-kind analytics.
  • The tool’s arguments and result render as its input and output, searchable on input.value and output.value.
  • The tool name, call id and type are stored on the span as you sent them, so you can tie an execution back to the model’s request that triggered it.
  • Failed tool runs carry error.type and an exception event, so they appear in error analysis.
  • With mcp.method.name set, MCP tools are counted separately in context attribution.

A tool span is not counted as an LLM call, even if it carries a model. The tool span needs no cost attribute, and you should never send one. Rius computes cost on the model call spans from the model name and token counts.

When something else fits

  • If you only need the model’s tool request recorded, put it in the tool_call part of gen_ai.output.messages on the LLM span and skip a separate tool span.
  • If you want conveniences such as message normalization and a raised attribute limit, use the Rius SDK instead of hand-written spans. Plain OpenTelemetry stays fully supported either way.

Still stuck? Get help

Last updated on