How to instrument an AI agent with OpenTelemetry

Agent reasoning, tool calls, and state become observable when you wrap them in OpenTelemetry spans using the GenAI semantic conventions. Here is what that instrumentation actually looks like.

B

Balagei G Nagarajan

4 MIN READ


An agent run drawn as a nested set of labeled spans: invoke_agent containing chat and execute_tool spans
that's where you open spans by hand and set the convention attributes yourself.
— from “How to instrument an AI agent with OpenTelemetry”

Key facts.

  • OpenTelemetry publishes GenAI semantic conventions with dedicated agent and framework spans and generative-AI client spans, defining standard attribute names for model, agent, and tool operations.source
  • The conventions name tool execution explicitly, with attributes for the tool call (name, call id, arguments, result) so a tool invocation is a first-class span, not an opaque function call.source
  • OpenLLMetry, maintained by Traceloop, is an open-source extension that auto-instruments common LLM providers and frameworks and emits standard OpenTelemetry signals plus GenAI attributes.source
  • Datadog, New Relic, and Dynatrace support the GenAI semantic conventions natively, so OpenTelemetry-instrumented agent code exports to them without SDK changes (OpenInference, the Arize convention, isn't universally supported).source

Auto-instrumentation or manual: which do you actually need?

Start with auto-instrumentation. OpenLLMetry wraps the LLM client calls, vector database queries, and many agent frameworks for you, emitting spans with token usage and model metadata after a single init call. That covers the model side of the agent with almost no code. OpenInference, the Arize convention, offers AI-tailored span types (LLM, tool, chain, agent, embedding) and suits evaluation-heavy workflows, though its attributes don't always map cleanly onto the OpenTelemetry GenAI conventions, which is why backend support for it's narrower. The honest read: vendor "zero-code" claims come from the library maintainers, but the underlying OpenTelemetry conventions and SDK behavior are standardized and checkable against the official spec, so prototype on your own stack rather than taking a setup demo at face value.

Manual instrumentation covers what auto-instrumentation can't see: your own reasoning steps, your custom tools, and agent state. that's where you open spans by hand and set the convention attributes yourself.

from opentelemetry import trace
tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("invoke_agent", attributes={ "gen_ai.operation.name": "invoke_agent", "gen_ai.agent.name": "travel_planner", }) as agent_span:

with tracer.start_as_current_span("chat", attributes={ "gen_ai.operation.name": "chat", "gen_ai.request.model": "the-model-you-call", }) as llm_span: resp = llm_client.chat(...) # the real call llm_span.set_attribute("gen_ai.usage.input_tokens", resp.usage.input_tokens)

with tracer.start_as_current_span("execute_tool get_weather", attributes={ "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": "get_weather", "gen_ai.tool.call.id": tool_call_id, }) as tool_span: result = weather_api.call(...) # run the tool tool_span.set_attribute("gen_ai.tool.call.result", redact(result))

Swimlane diagram with lanes for agent, model, and tools showing spans nested under one trace

What do you set on each span, and how do you handle the sensitive parts?

Use the convention attributes so any compliant backend understands your traces. On the agent span, set the operation name and agent name. On each model call, set the operation, the model, and token usage. On each tool, set the tool name, the call id, the arguments, and the result, with redaction for anything sensitive. Use CLIENT span kind for remote calls and INTERNAL for in-process steps. For agent state across steps, carry it as baggage or attach it to a wrapping workflow span. Mask prompts, arguments, and results that contain personal data at capture time, so you aren't paying to store, or exposing, data you never needed in the trace.

SpanOperationAttributes that matter
Agent runinvoke_agentgen_ai.agent.name, gen_ai.agent.description
Model callchatgen_ai.request.model, gen_ai.usage.input_tokens / output_tokens
Tool callexecute_toolgen_ai.tool.name, gen_ai.tool.call.id, arguments, result (redacted)

Instrumenting to a standard is what makes the Pattern Intelligence Layer possible. Reliability at the pattern level needs a consistent, queryable record of how the agent ran (which tools, in what order, with what results), and the GenAI conventions give you exactly that record in a shape that survives a model swap or a backend change. Instrument once against the standard, and the trace you need to spot a recurring failure pattern is already there in a form every tool can read.

Frequently asked questions

Do I have to choose between OpenLLMetry and OpenInference?
For most teams, start with OpenLLMetry because it leans into the OpenTelemetry GenAI conventions and has broad auto-instrumentation. OpenInference suits evaluation-heavy setups but has narrower backend support.

what's the minimum to get value?
Instrument your model calls and one tool, export over OTLP to any backend that supports the GenAI conventions, and look at one full trace. Expand from there.

How do I keep prompts and tool arguments out of my backend?
Redact or mask at capture. Put large or sensitive content in span events you can drop or scrub at the Collector, not in always-indexed attributes.


Share this post

Join the discussion

Have a take, a war story, or a question? Sign in with GitHub to comment and react. Comments are powered by GitHub Discussions, ad-free and yours to moderate.

Continue Reading

Find where your agent breaks, before you build it

Faultmap maps where your agent will fail from the goal and your data, then hands you the first test suite it has to pass.