Skip to main content

Documentation Index

Fetch the complete documentation index at: https://arize-ax.mintlify.dev/docs/llms.txt

Use this file to discover all available pages before exploring further.

https://storage.googleapis.com/arize-phoenix-assets/assets/images/arize-docs-images/cookbooks/gc.png

Google Colab

This guide is a hands-on tour of OpenInference best practices for tracing AI applications with Arize AX. You will learn:
  • How OpenInference layers on top of OpenTelemetry to add AI-aware semantics
  • The hierarchy of sessions, traces, and spans that organizes your telemetry
  • Three ways to capture spans — auto-instrumentation, manual instrumentation, and the hybrid approach that combines them
  • The common attributes every span carries, and the kind-specific attributes for the four core span kinds (LLM, chain, agent, and tool)
  • How to add or override attributes on spans, including auto-instrumented spans you cannot access directly
Each section walks through a small piece of code and what to look for in the Arize AX trace view. You can run the companion notebook in Colab, or follow along locally by running each code block independently in your venv.
Every runnable code block below is a complete, self-contained Python script. Save each to a .py file and run it in your venv. The same setup code (register(...) and OpenAIInstrumentor().instrument(...)) appears at the top of every block — that’s intentional, so you can run any block in isolation without having to assemble pieces from earlier sections.

Initial setup

You will need an Arize AX account to run this guide. Sign up now for free if you don’t have an account.

Create a project directory and virtual environment

Create a new directory for your script and a Python virtual environment inside it.

Install libraries

Install all the dependencies you will use across the rest of the guide:
pip install openai openai-agents arize-otel \
    openinference-instrumentation-openai \
    openinference-instrumentation-openai-agents \
    opentelemetry-sdk opentelemetry-exporter-otlp

Set environment variables

The script reads three secrets from environment variables. Find your Arize Space ID and API Key on your Space Settings page: Arize AX Space Settings page showing the Space ID and API Key fields Export them in the same shell session you will run the code from:
export ARIZE_SPACE_ID="your-arize-space-id"
export ARIZE_API_KEY="your-arize-api-key"
export OPENAI_API_KEY="your-openai-api-key"
The OpenAI SDK reads OPENAI_API_KEY automatically; the Arize values are read by the script.

Setup tracing

Every runnable code block in this guide includes the same tracing setup at the top. It uses the arize-otel convenience function to register a tracer provider that sends spans to Arize AX, then enables the OpenAI auto-instrumentor so calls to the OpenAI SDK are traced automatically. See The arize.otel helpers for the full set of arize.otel functions, including routing traces to multiple projects from a single app. Save this code as a .py file and run it to verify your setup before proceeding — you should see the OpenTelemetry tracing details printed to your terminal:
import os

from arize.otel import register
from openinference.instrumentation.openai import OpenAIInstrumentor

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)

OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

Introduction to OpenInference

OpenInference is an open-source set of conventions and instrumentation libraries for tracing AI applications. It is maintained by Arize and is the standard that Arize AX uses to render LLM, tool, agent, chain, retriever, and other AI-specific spans in the trace view. OpenInference is an extension to OpenTelemetry, not a replacement for it. It uses the standard OpenTelemetry SDK and libraries under the hood — the same TracerProvider, Tracer, Span, SpanProcessor, and Exporter you would use for any OTel-instrumented service. What OpenInference adds is:
  • A set of semantic conventions that describe how to represent AI concepts (LLM calls, prompts, messages, tool invocations, retrieval, agent steps, sessions) as span attributes
  • A library of auto-instrumentors for popular LLM SDKs and orchestration frameworks (OpenAI, Anthropic, Bedrock, LangChain, LlamaIndex, CrewAI, AutoGen, and many more)
Because OpenInference is built on OpenTelemetry, any tool that speaks OTel can consume the spans — but a backend that understands the OpenInference conventions (like Arize AX) can render them as a rich, AI-aware trace view rather than a generic span list. Read more: The code below simulates having a conversation with a chatbot and asking it two related questions. It will create traces in AX for a simple set of OpenAI calls. Don’t worry for now about what the code is doing, we will dive into the details later. Save this code as a .py file and run it:
import os
import uuid

from arize.otel import register
from openai import OpenAI
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from openinference.semconv.trace import (
    OpenInferenceSpanKindValues,
    SpanAttributes,
)
from opentelemetry import trace

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

client = OpenAI()
session_id = str(uuid.uuid4())
tracer = trace.get_tracer(__name__)
system_prompt = "You are a helpful assistant. Answer in a concise manner."
messages = [{"role": "system", "content": system_prompt}]


def ask_llm(question: str) -> None:
    messages.append({"role": "user", "content": question})

    with tracer.start_as_current_span(
        "openinference-intro-chain"
    ) as chain_span:
        chain_span.set_attribute(
            SpanAttributes.OPENINFERENCE_SPAN_KIND,
            OpenInferenceSpanKindValues.CHAIN.value,
        )
        chain_span.set_attribute(SpanAttributes.INPUT_VALUE, question)

        response = client.responses.create(
            model="gpt-5.4-mini",
            input=messages,
        )
        answer = response.output_text

        messages.append({"role": "assistant", "content": answer})
        chain_span.set_attribute(SpanAttributes.OUTPUT_VALUE, answer)
        print(answer)


with using_session(session_id=session_id):
    ask_llm("What is OpenInference?")
    ask_llm("How does it relate to OpenTelemetry?")
Open Arize AX and navigate to the otel-best-practices project to view your traces.

Sessions, traces, and spans

Three concepts shape how OpenInference (and OpenTelemetry) organize tracing data, and they nest inside each other:
  • Span — a single step in your application, such as an LLM call, a tool invocation, or a chain stage. Each span has a name, start and end timestamps, attributes, and a potentially a parent — spans nest to form a tree. The root of the tree is known as the root span, and has no parent.
  • Trace — a collection of spans tied together by a shared trace_id. A trace represents one end-to-end request through your agent.
  • Session — a collection of traces tied together by a shared session.id. A session is a logical grouping of traces based on a shared concept, such as multiple agent interactions to solve the same task, or to help with a continuous conversation.
Picture a customer-support chatbot. The user has a multi-turn conversation, and that whole conversation is one session. Each turn — one user message and the app’s response — is one trace. Inside each trace, the app does several things to produce the response (classify intent, call a tool, format a reply), and each of those steps is a span.
Session: support-chat-7a3f

├── Trace 1: "Where is my order?"
│   └── Chain span: handle_message
│       ├── LLM span:  classify_intent
│       ├── Tool span: lookup_order(order_id="A123")
│       └── LLM span:  format_response

├── Trace 2: "When will it arrive?"
│   └── Chain span: handle_message
│       ├── LLM span:  classify_intent
│       ├── Tool span: get_shipping_status(order_id="A123")
│       └── LLM span:  format_response

└── Trace 3: "Thanks!"
    └── Chain span: handle_message
        └── LLM span: generate_acknowledgement
Three traces, one session. In Arize AX you can open a single trace to debug what happened in one turn, or use the session view to see all the turns together. For the full breakdown of how signals, spans, traces, and sessions fit together, see Signals, spans, traces, and sessions.

Sessions

In Arize AX, start with the Sessions tab. You should see a single session that represents the entire two step conversation. The Sessions tab in Arize AX showing one session for the chatbot conversation If you select the session, a pane will appear with details of the session, including both steps in the conversation, showing the input to and the output from the agent. It will also show the latency, so the time the agent took to run, as well as the total number of tokens used and the estimated cost based off published token pricing from the LLM provider if available. Details pane in Arize AX for the chatbot session, showing both conversation turns with input, output, latency, tokens, and cost

Traces

Select the Traces tab. You should see both of the steps in the conversation as distinct traces, showing the input and output to the agent. The code you ran just makes a single LLM call, so the trace has the input set to what was sent to the LLM, and the output set to the response from the LLM. In a more complicated trace, the input is what was sent to the agent, and the output is the final response sent by the agent after it has completed its entire processing, including calling LLMs or tools. The Traces tab in Arize AX showing two traces from the chatbot conversation If you select a trace, a pane will appear with details of the trace. It will show a tree of spans, along with latency, token counts, and estimated cost. Details pane in Arize AX for a single trace, showing the span tree with latency, token counts, and cost
You can also navigate to the individual traces directly from the session view.

Spans

In the trace view you will see a tree of the spans that make up the trace. Spans are grouped into traces by having the same trace_id set on them. A trace is a tree of spans, so one span will be the root span at the top of the tree, and the rest of the spans will be under that tree. The hierarchy is defined using the parent_id on the span — each child span has its parent_id set to the id of the parent span. In the trace we have 2 spans: Details pane in Arize AX for a single trace, showing the span tree with latency, token counts, and cost The root span is a Chain span. Chain spans are starting points for a set of related spans, you can think of them as a folder that groups spans together. In this example, the chain span isn’t really necessary, it’s just here to help show a tree. Under the root span is an LLM span called ChatCompletion. This span represents a call to an LLM, in our case OpenAI. An LLM span named ChatCompletion selected in the Arize AX trace tree Against each span is an Attributes tab that has JSON containing all the attributes associated with the span, such as the input and output, number of tokens used for an LLM span, and so on. We will cover these attributes in the rest of this guide. The Attributes tab in Arize AX showing the JSON attributes for a selected span OpenInference defines a fixed set of span kinds:
Span kindDescription
LLMA call to a large language model. Captures the model, input messages, output messages, token counts, and cost.
CHAINA starting point or link between application steps. Commonly used as a parent span to group related work into a logical block.
AGENTA span representing an agent’s work — typically wraps LLM and tool spans together
TOOLA call to an external tool or function, often invoked in response to a tool-use request from an LLM
RETRIEVERA retrieval operation, such as fetching documents from a vector store or search index
EMBEDDINGA call to an embedding model
RERANKERA reranking step that reorders a set of retrieved documents
GUARDRAILA safety or policy check, such as content moderation, PII detection, or input validation
EVALUATORAn evaluation step that scores or judges an LLM output
PROMPTA prompt definition or templating step
UNKNOWNUsed when no other kind applies
In this guide, we will be looking at LLM, chain, agent, and tool spans. For the complete reference covering every kind and the attributes each is expected to carry, see OpenInference span kinds.

Configuring sessions

Sessions are a logical grouping of traces based on a continuous set of interactions with an agent. For example, in a chatbot, the entire multi-turn conversation that a single user has with the agent would be a session. When the same user starts a brand new conversation with no previous context, or a new user starts a conversation, this would be a new session. Sessions are explicitly managed by the engineer building the agent; they are not created automatically when sending traces. Sessions are set with the using_session function. This sets the session id for any spans created in any code run in this block. using_session is one of a small family of OpenInference context managers — see OpenInference context managers for the full list (using_user, using_metadata, using_tags, using_prompt_template, using_attributes). The following code contains a call to OpenAI inside a session. The session id is hardcoded here, so if you run this code multiple times, each run will be a new trace inside the same session. Save and run this code:
import os

from arize.otel import register
from openai import OpenAI
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

client = OpenAI()

with using_session(session_id="My Session"):
    response = client.responses.create(
        model="gpt-5.4-mini",
        input="What are sessions in OpenInference? Be concise.",
    )
    print(response.output_text)
Look up this session in Arize AX. You will see a single session with multiple traces depending on how many times you ran the code. The Sessions tab in Arize AX showing a single session with multiple traces Now run this code, which uses a different session id and so will create a new session:
import os

from arize.otel import register
from openai import OpenAI
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

client = OpenAI()

with using_session(session_id="My Session 2"):
    response = client.responses.create(
        model="gpt-5.4-mini",
        input="What are sessions in OpenInference? Be concise.",
    )
    print(response.output_text)
You will now see a new session in the sessions list. The Sessions tab in Arize AX showing two distinct sessions in the list

Capturing spans and traces

In the examples so far you have already seen both ways that OpenInference creates spans:
  • Auto-instrumentors wrap a specific library or framework (such as the OpenAI SDK, or LangChain) and emit a span for every call to that library automatically, along with spans for the different actions that the framework performs, such as tool calling. Each call to the library or framework is a separate trace.
  • Manual instrumentation lets you create your own traces and spans by calling the tracer directly in your application code
Most real-world applications use both. The auto-instrumentor handles the standard SDK calls; manual instrumentation captures your application’s own logic that wraps around those calls. See Instrumentation approaches for a deeper comparison of auto-instrumentation, manual instrumentation, and the hybrid pattern.

Auto-instrumentors

Auto-instrumentors are libraries that instrument an SDK or framework, and automatically emit spans for every call, and every action taken by the SDK or framework. You already set up an auto-instrumentor in the Initial setup section:
from openinference.instrumentation.openai import OpenAIInstrumentor

OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
After that single call, every client.responses.create() and client.chat.completions.create() in your code creates an LLM span automatically in a new trace. If you are using a more advanced framework that handles tool calling for example, then each call to the framework would be a new trace, with spans for the LLM and tool calls, grouped under a chain span. OpenInference provides auto-instrumentors for most popular AI SDKs and orchestration frameworks: OpenAI, Anthropic, Bedrock, LangChain, LlamaIndex, CrewAI, AutoGen, and many more. See the OpenInference repository for the full list. If you run the code below, the auto-instrumentor will create a trace with a single LLM span. Save it as a .py file and run it:
import os

from arize.otel import register
from openai import OpenAI
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

client = OpenAI()

with using_session(session_id="Capturing Spans Example"):
    response = client.responses.create(
        model="gpt-5.4-mini",
        input="What are sessions in OpenInference? Be concise.",
    )
    print(response.output_text)
Open the new trace in Arize AX under the Capturing Spans Example session. You will see a single LLM span — the auto-instrumentor created it automatically for the client.responses.create() call, without you writing any tracing code.

Manual instrumentation

Manual instrumentation gives you full control. You call the OpenTelemetry tracer directly to start a span, set its attributes (including its OpenInference span kind), and end it when the work is done. The recommended pattern is the context-manager form, which sets the span as the active span on the OpenTelemetry context (so any spans created inside the block automatically become its children) and ends the span when the block exits:
with tracer.start_as_current_span("my-span") as span:
    span.set_attribute(
        SpanAttributes.OPENINFERENCE_SPAN_KIND,
        OpenInferenceSpanKindValues.CHAIN.value,
    )
    span.set_attribute(SpanAttributes.INPUT_VALUE, "input data")
    # do work here
    span.set_attribute(SpanAttributes.OUTPUT_VALUE, "result")
You can manually create spans of any OpenInference kind, such as chain, LLM, or tool — by setting the openinference.span.kind attribute on the span. The kind controls how Arize AX renders the span (the icon and the detail view) and which set of OpenInference attributes the span is expected to carry. The following code creates a trace with three manually-created spans: a parent data-pipeline chain span with two child spans (step-1-validate and step-2-format) nested inside. There are no LLM or tool calls — every span is created by your code. Save it as a .py file and run it:
import os

from arize.otel import register
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from openinference.semconv.trace import (
    OpenInferenceSpanKindValues,
    SpanAttributes,
)
from opentelemetry import trace

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

tracer = trace.get_tracer(__name__)

with using_session(session_id="Capturing Spans Example"):
    with tracer.start_as_current_span("data-pipeline") as pipeline:
        pipeline.set_attribute(
            SpanAttributes.OPENINFERENCE_SPAN_KIND,
            OpenInferenceSpanKindValues.CHAIN.value,
        )
        pipeline.set_attribute(SpanAttributes.INPUT_VALUE, "raw request")

        with tracer.start_as_current_span("step-1-validate") as step:
            step.set_attribute(
                SpanAttributes.OPENINFERENCE_SPAN_KIND,
                OpenInferenceSpanKindValues.CHAIN.value,
            )
            step.set_attribute(SpanAttributes.INPUT_VALUE, "raw request")
            step.set_attribute(SpanAttributes.OUTPUT_VALUE, "validated request")

        with tracer.start_as_current_span("step-2-format") as step:
            step.set_attribute(
                SpanAttributes.OPENINFERENCE_SPAN_KIND,
                OpenInferenceSpanKindValues.CHAIN.value,
            )
            step.set_attribute(SpanAttributes.INPUT_VALUE, "validated request")
            step.set_attribute(SpanAttributes.OUTPUT_VALUE, "formatted output")

        pipeline.set_attribute(SpanAttributes.OUTPUT_VALUE, "formatted output")
Open the new trace in Arize AX under the Capturing Spans Example session. You will see a single chain span called data-pipeline with two child chain spans (step-1-validate and step-2-format) nested underneath. Trace tree in Arize AX showing the data-pipeline chain span with step-1-validate and step-2-format child chain spans nested inside

Hybrid instrumentation

The most powerful pattern is to use both approaches together. Hybrid instrumentation lets you wrap auto-instrumented calls in your own manually-created spans, so you can group SDK calls into logical units, add custom attributes, and build the trace tree that best represents your application — without losing any of the rich attributes that the auto-instrumentor captures. Auto-instrumented spans and manually-created spans nest together naturally because they share the same OpenTelemetry context. When you open a manual span with tracer.start_as_current_span(...), it becomes the active span on the context. Any call to an auto-instrumented SDK inside that block will create its span as a child of your manual span. You have already seen hybrid instrumentation in the Introduction to OpenInference section. The ask_llm function wraps each OpenAI call in a manually-created chain span. The manually-created chain span is the parent; the LLM span that the OpenAI auto-instrumentor produces around client.responses.create() automatically becomes its child. That is what gives you the tree structure you saw in Arize AX when you ran the introduction example — a chain span at the top, with an LLM span nested inside. This pattern is the typical shape of a real-world traced application: manual chain or agent spans give you the high-level structure of your business logic; auto-instrumented spans fill in the low-level detail of every SDK call you make inside them. Save and run this hybrid instrumentation example:
import os

from arize.otel import register
from openai import OpenAI
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from openinference.semconv.trace import (
    OpenInferenceSpanKindValues,
    SpanAttributes,
)
from opentelemetry import trace

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

client = OpenAI()
tracer = trace.get_tracer(__name__)

with using_session(session_id="Capturing Spans Example"):
    with tracer.start_as_current_span("manual-chain") as chain_span:
        chain_span.set_attribute(
            SpanAttributes.OPENINFERENCE_SPAN_KIND,
            OpenInferenceSpanKindValues.CHAIN.value,
        )
        question = "What are sessions in OpenInference? Be concise."
        chain_span.set_attribute(SpanAttributes.INPUT_VALUE, question)

        response = client.responses.create(
            model="gpt-5.4-mini",
            input=question,
        )

        chain_span.set_attribute(
            SpanAttributes.OUTPUT_VALUE, response.output_text
        )
        print(response.output_text)
Open the new trace in Arize AX under the Capturing Spans Example session. You will see a chain span called manual-chain with an LLM span nested inside it — the manual span is the parent, and the auto-instrumented LLM span automatically became its child because they share the same OpenTelemetry context.

Span attributes

Every OpenInference span carries a small set of common attributes that apply regardless of the span kind, plus a kind-specific set added on top. The common attributes available on any span kind are:
AttributeDescription
openinference.span.kindThe span kind: LLM, CHAIN, AGENT, TOOL, RETRIEVER, EMBEDDING, RERANKER, GUARDRAIL, EVALUATOR, PROMPT, or UNKNOWN. Controls how Arize AX renders the span.
input.valueThe input to the span as a string. If the value is structured, serialize it to JSON and set input.mime_type accordingly.
input.mime_typeThe mime type of input.value. Defaults to text/plain; set to application/json if the value is a JSON string.
output.valueThe output from the span as a string
output.mime_typeThe mime type of output.value. Same convention as input.mime_type.
metadataA JSON dictionary of your own fields. Use it to attach domain-specific context such as user tier, feature flag, or request id.
session.idGroups multiple traces into a session. Set with using_session(...) or directly via span.set_attribute(SpanAttributes.SESSION_ID, ...).
user.idIdentifies the user the trace belongs to. Set with using_user(...) or directly.
tag.tagsA list of string tags for filtering. Set with using_tags(...).
Arize AX uses several of these directly in the UI: openinference.span.kind drives the span icon and the kind-specific detail view; input.value and output.value on the root span feed the trace-level input and output preview in the Traces and Sessions tabs; session.id groups traces into sessions; metadata and tag.tags are filterable across spans. The full attribute catalogue is described in OpenInference semantic conventions (the overall standard) and OpenInference span kinds (per-kind reference).

The core span types

OpenInference defines 11 span kinds, but most AI applications use just four: LLM, chain, agent, and tool. These show up in almost every real-world trace — an agent makes LLM calls, LLM calls trigger tool calls, and chain spans group the related work together. For the canonical reference of every span kind and the attributes each carries, see OpenInference span kinds. The following example uses the OpenAI Agents SDK to produce a single trace containing all four kinds. The SDK has its own auto-instrumentor — openinference-instrumentation-openai-agents — which emits the full set of span kinds for every agent run. The Agents SDK has its own instrumentor, OpenAIAgentsInstrumentor. Attach it to the existing tracer provider alongside the OpenAI auto-instrumentor — the Agents SDK creates its own LLM spans, so the two cooperate without duplicating work. The code below sets up both instrumentors, defines a simple travel assistant with two tools, then asks it a question that requires both tools to be called. The run is wrapped in a session so the trace is easy to find in Arize AX. Save and run it:
import os

from agents import Agent, Runner, function_tool
from arize.otel import register
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
OpenAIAgentsInstrumentor().instrument(tracer_provider=tracer_provider)


@function_tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"The weather in {city} is sunny and 22°C."


@function_tool
def get_time_zone(city: str) -> str:
    """Get the time zone for a city."""
    zones = {
        "Tokyo": "JST (UTC+9)",
        "London": "GMT (UTC+0)",
        "New York": "EST (UTC-5)",
    }
    return zones.get(city, "unknown")


travel_agent = Agent(
    name="TravelAssistant",
    instructions=(
        "You are a helpful travel assistant. "
        "Use get_weather to look up the weather for a city, "
        "and get_time_zone to look up its time zone. "
        "Give a concise final answer."
    ),
    tools=[get_weather, get_time_zone],
)

with using_session(session_id="Travel Agent Example"):
    result = Runner.run_sync(
        travel_agent,
        "What's the weather and time zone in Tokyo?",
    )

print(result.final_output)
In the companion notebook, await Runner.run(...) is used because Jupyter supports top-level await. In a regular Python script, use the synchronous Runner.run_sync(...) instead, as shown above.
Open the trace in Arize AX under the otel-best-practices project. A single agent run produces a trace containing all four core span kinds:
  • Two agent spans — an outer Agent workflow wrapper and an inner TravelAssistant
  • Three chain spans — one wrapping the whole workflow plus one per agent turn
  • Two tool spans, one for each call to get_weather and get_time_zone
  • Four LLM spans — each agent turn produces a Responses API call from the SDK with the underlying OpenAI client call nested inside it
Trace in Arize AX from the OpenAI Agents SDK example, showing agent, chain, tool, and LLM spans nested together This is the trace shape you will see from most non-trivial agents: an outer agent/chain workflow, tool spans for each external call, and LLM spans for every model call inside.

LLM spans

LLM spans represent a call to a large language model. They capture everything you need to debug or analyze the call: the model that was used, the input messages, the output, tools, token counts, costs, and more. In the agent trace are several LLM spans. Select one of these, and you will see the input and output from that LLM call. Against the span in the tree you will also see the number of tokens used, the latency, and the cost. In the Attributes tab, you can see the full attributes for the span. The Attributes tab in Arize AX showing the full attribute set for an LLM span The relevant attributes for this example are:
{
    "openinference": {
        "span": {
            "kind": "LLM"
        }
    },
    "llm": {
        "cost": {
            "completion": 0.0002205,
            "completion_details": {
                "output": 0.0002205,
                "reasoning": 0
            },
            "prompt": 0.00009975,
            "prompt_details": {
                "cache_read": 0,
                "input": 0.00009975
            },
            "total": 0.00032025
        },
        "input_messages": [
            {
                "message.content": "You are a helpful travel assistant. Use get_weather to look up the weather for a city, and get_time_zone to look up its time zone. Give a concise final answer.",
                "message.role": "system"
            },
            {
                "message.content": "What's the weather and time zone in Tokyo?",
                "message.role": "user"
            }
        ],
        "invocation_parameters": "{\"include\": [], \"model\": \"gpt-5.4-mini\", \"prompt_cache_key\": \"agents-sdk:run:1fc4521fe6f248beafa82586c8b0fa3e\", \"reasoning\": {\"effort\": \"none\"}, \"text\": {\"verbosity\": \"low\"}}",
        "model_name": "gpt-5.4-mini-2026-03-17",
        "output_messages": [
            {
                "message.role": "assistant",
                "message.tool_calls": [
                    {
                        "tool_call.function.arguments": "{\"city\":\"Tokyo\"}",
                        "tool_call.function.name": "get_weather",
                        "tool_call.id": "call_OdvgB0fVwTcdKy6mqxV3cmuB"
                    }
                ]
            },
            {
                "message.role": "assistant",
                "message.tool_calls": [
                    {
                        "tool_call.function.arguments": "{\"city\":\"Tokyo\"}",
                        "tool_call.function.name": "get_time_zone",
                        "tool_call.id": "call_cnCgrd4Js5bErfROFdIoc68j"
                    }
                ]
            }
        ],
        "provider": "openai",
        "system": "openai",
        "token_count": {
            "completion": "49",
            "completion_details": {
                "output": "49",
                "reasoning": "0"
            },
            "prompt": "133",
            "prompt_details": {
                "cache_read": "0",
                "input": "133"
            },
            "total": "182"
        },
        "tools": [
            {
                "tool.json_schema": "{\"name\":\"get_weather\",\"parameters\":{\"properties\":{\"city\":{\"title\":\"City\",\"type\":\"string\"}},\"required\":[\"city\"],\"title\":\"get_weather_args\",\"type\":\"object\",\"additionalProperties\":false},\"strict\":true,\"type\":\"function\",\"defer_loading\":null,\"description\":\"Get the current weather for a city.\"}"
            },
            {
                "tool.json_schema": "{\"name\":\"get_time_zone\",\"parameters\":{\"properties\":{\"city\":{\"title\":\"City\",\"type\":\"string\"}},\"required\":[\"city\"],\"title\":\"get_time_zone_args\",\"type\":\"object\",\"additionalProperties\":false},\"strict\":true,\"type\":\"function\",\"defer_loading\":null,\"description\":\"Get the time zone for a city.\"}"
            }
        ]
    }
}

LLM-specific attributes

Beyond the common attributes that any span carries (covered in the Span attributes section above), the OpenAI auto-instrumentor adds an LLM-specific set on every LLM span — all under the llm.* namespace and following the OpenInference semantic conventions:
AttributeDescription
llm.model_nameThe exact model identifier returned by OpenAI, for example gpt-5.4-mini-2026-03-17. This is the resolved snapshot version, not the alias you passed in.
llm.providerThe LLM provider, here openai
llm.systemThe AI system identifier, also openai
llm.invocation_parametersA JSON string of the parameters passed to the API: {"model": "gpt-5.4-mini"}
llm.input_messagesThe messages sent to the API as a structured array. Each entry has message.role and message.content.
llm.output_messagesThe messages returned by the API as a structured array. Each entry has message.role and a message.contents list of structured content items (with message_content.text and message_content.type). This shape is multimodal-aware — text, image, audio, and reasoning content all fit the same structure.
llm.token_count.prompt / .completion / .totalToken counts for the call, with detail breakdowns under llm.token_count.prompt_details.* (cache_read, input) and llm.token_count.completion_details.* (output, reasoning)
llm.cost.prompt / .completion / .totalEstimated cost in USD with the same _details breakdowns. Arize AX computes these from the token counts and the published pricing for the model.
Arize AX uses the LLM-specific attributes to drive the token-count and cost columns in the trace and session views, and to populate the LLM detail view (input messages, output messages, model name).

Tool spans

Tool spans represent a call to an external tool or function — typically a function the LLM has decided to call. They capture the tool’s name, the arguments the LLM passed in, and the value the tool returned. In the agent trace above you have two tool spans: get_weather and get_time_zone, one for each tool the agent invoked. Select one of them in the trace tree to see the tool-specific attributes — the tool name, the arguments the LLM passed in ({"city": "Tokyo"}), and the value the tool returned. The Attributes tab in Arize AX showing the get_weather tool span attributes, including the input city and the returned weather text The relevant attributes for this example are:
{
    "openinference": {
        "span": {
            "kind": "TOOL"
        }
    },
    "tool": {
        "name": "get_weather"
    }
}

Tool-specific attributes

Beyond the common attributes that any span carries, tool spans carry a small set of tool-specific attributes under the tool.* namespace:
AttributeDescription
tool.idThe identifier for the result of the tool call. Corresponds to the tool_call.id emitted by the LLM, which lets Arize AX link the tool span back to the LLM call that requested it.
tool.nameThe name of the tool
tool.descriptionThe tool’s description. The LLM uses this when deciding which tool to call.
tool.parametersA JSON string of the parameter values the LLM passed to the tool
tool.json_schemaThe full JSON schema of the tool’s input, typically in OpenAI tool-calling format. Tells the LLM what shape of arguments the tool expects.
Arize AX uses these to populate the tool detail view: the tool name and description appear at the top, the arguments and return value are surfaced from input.value and output.value, and the tool span links back to the parent LLM span via tool.id.

Agent spans

Agent spans represent the work of an autonomous agent — the orchestration that decides when to call the LLM, when to call tools, when to call the LLM again, and when to stop. An agent span is typically the parent of the LLM, tool, and chain spans that make up the agent’s loop. In the agent trace above, the TravelAssistant span is an agent span. Select it to see how it wraps both of the agent’s turns, with all of the LLM and tool calls nested inside it. The Attributes tab in Arize AX showing the TravelAssistant agent span attributes The relevant attributes for this example are:
{
    "openinference": {
        "span": {
            "kind": "AGENT"
        }
    },
    "graph": {
        "node": {
            "id": "TravelAssistant"
        }
    }
}
Note that the OpenAI Agents SDK auto-instrumentor uses graph.node.id to carry the agent’s name (TravelAssistant) rather than the convention’s agent.name. This is so Arize AX can render multi-agent systems with handoffs as a graph view. When you create agent spans manually, set agent.name (and optionally the graph.node.* attributes if you want the graph view).

Agent-specific attributes

Agent spans carry a small set of agent-specific attributes:
AttributeDescription
agent.nameThe name of the agent. Agents that perform the same logical role should share a name so you can group their traces together.
graph.node.idThe id of this agent’s node in an execution graph. Optional — set when you want to visualize a multi-agent system as a graph in Arize AX.
graph.node.nameA human-readable name for the graph node
graph.node.parent_idThe id of the parent node. Leave unset for a root agent.
The graph.node.* attributes are how Arize AX renders multi-agent systems (LangGraph, AutoGen, CrewAI, etc.) as a graph view alongside the trace tree.

Chain spans

Chain spans are general-purpose grouping spans. Use a chain span when you want to group a set of related work under a single parent in the trace tree — a multi-step pipeline, one agent turn, an LLM call with pre- and post-processing, or just a logical block in your application code. In the agent trace above the outer Agent workflow is a chain span, and each agent turn is also wrapped in a chain span called turn. Select a turn span to see how it groups the LLM and tool calls for that turn together. The Attributes tab in Arize AX showing a turn chain span grouping the LLM and tool calls for one agent turn The relevant attributes for this example are:
{
    "openinference": {
        "span": {
            "kind": "CHAIN"
        }
    }
}
The chain span carries only the kind and the common attributes — its value is purely structural, giving you a named parent in the trace tree.

Chain-specific attributes

Chain spans have no kind-specific attributes. They rely on the common attributes covered in the Span attributes section above — openinference.span.kind, input.value, output.value, metadata, session.id, and so on. The chain span’s value is purely structural: it gives you a named parent in the trace tree, with whatever input and output you choose to attach to it.

Overriding or adding attributes

Auto-instrumentors capture the standard OpenInference attributes for every span they create, but you often want to add your own. Common reasons:
  • Tag the call for filtering — for example experiment="v2-prompt" or tenant="acme"
  • Attach domain metadata — user tier, request id, feature flag value
  • Record a prompt template — the template string, version, and variables you used, separate from the final flattened prompt
With manually-created spans you can call span.set_attribute(...) directly inside the with block, as you saw in the manual instrumentation example. With auto-instrumented spans you do not have direct access to the span object — but you can still enrich it by putting attributes into the OpenTelemetry context using the OpenInference context managers. The auto-instrumentor reads from that context when it creates the span. This means the attributes are applied to every span created inside the block, no matter who creates it. The available context managers are:
Context managerSets
using_session(session_id)session.id
using_user(user_id)user.id
using_metadata(metadata)metadata (a JSON dictionary of your own fields)
using_tags(tags)tag.tags (a list of strings)
using_prompt_template(template=, variables=, version=)The llm.prompt_template.* attributes. Most useful on LLM spans.
using_attributes(...)A convenience wrapper that combines all of the above into a single call
See OpenInference context managers for the full reference, including detailed usage patterns and gotchas. The following code uses using_metadata to attach a domain-specific metadata dictionary. The example here wraps an OpenAI call so the metadata ends up on an LLM span, but the same pattern works for any span — auto-instrumented or manual — created inside the block. Save and run it:
import os

from arize.otel import register
from openai import OpenAI
from openinference.instrumentation import using_metadata, using_session
from openinference.instrumentation.openai import OpenAIInstrumentor

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

client = OpenAI()

with using_session(session_id="LLM Span Example"):
    with using_metadata({"user_tier": "premium", "request_source": "cookbook"}):
        response = client.responses.create(
            model="gpt-5.4-mini",
            input="What are LLM spans in OpenInference? Be concise.",
        )
        print(response.output_text)
Open the new trace in Arize AX. The LLM span now has an additional attribute:
  • metadata — a JSON string containing {"user_tier": "premium", "request_source": "cookbook"}
It appears in the Attributes tab alongside the standard llm.* attributes. The Attributes tab in Arize AX showing an LLM span with the custom metadata attribute alongside the standard llm.* attributes You can also filter spans by metadata values in the Arize AX trace view, which makes it easy to slice traces by tenant, feature flag, or any other domain dimension. In the Spans tab, set the filter to attributes.metadata.request_source = "cookbook" to only see spans created with the request_source metadata set to cookbook. The Spans tab in Arize AX filtered by attributes.metadata.request_source equal to cookbook

Overriding a tool attribute

You can also override attributes that an auto-instrumentor has set, or add attributes that it left out. The OpenAI Agents SDK auto-instrumentor only sets tool.name on tool spans — it does not populate tool.description. The following code redefines get_weather to set a custom tool.description attribute on the active tool span, then recreates the agent and re-runs it. The pattern works because the auto-instrumentor opens the tool span before calling your function, so the tool span is the active span when your function body runs. Calling set_attribute(...) on it inside the function body either overrides the attribute (if the instrumentor set it) or adds it (if the instrumentor did not). Save and run this code:
import os

from agents import Agent, Runner, function_tool
from arize.otel import register
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
from openinference.semconv.trace import SpanAttributes
from opentelemetry import trace

ARIZE_SPACE_ID = os.environ["ARIZE_SPACE_ID"]
ARIZE_API_KEY = os.environ["ARIZE_API_KEY"]

tracer_provider = register(
    space_id=ARIZE_SPACE_ID,
    api_key=ARIZE_API_KEY,
    project_name="otel-best-practices",
    batch=False,
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
OpenAIAgentsInstrumentor().instrument(tracer_provider=tracer_provider)


@function_tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""

    # Set a custom tool.description on the active tool span.
    trace.get_current_span().set_attribute(
        SpanAttributes.TOOL_DESCRIPTION,
        "Looks up the current weather conditions for a given city.",
    )

    return f"The weather in {city} is sunny and 22°C."


@function_tool
def get_time_zone(city: str) -> str:
    """Get the time zone for a city."""
    zones = {
        "Tokyo": "JST (UTC+9)",
        "London": "GMT (UTC+0)",
        "New York": "EST (UTC-5)",
    }
    return zones.get(city, "unknown")


travel_agent = Agent(
    name="TravelAssistant",
    instructions=(
        "You are a helpful travel assistant. "
        "Use get_weather to look up the weather for a city, "
        "and get_time_zone to look up its time zone. "
        "Give a concise final answer."
    ),
    tools=[get_weather, get_time_zone],
)

with using_session(session_id="Tool Override Example"):
    result = Runner.run_sync(
        travel_agent,
        "What's the weather and time zone in Tokyo?",
    )

print(result.final_output)
Open the new trace in Arize AX under the otel-best-practices project (find it via the Tool Override Example session). Select the get_weather tool span and open its Attributes tab. You will now see tool.description populated with the string you set inside the function body, alongside the standard tool.name the auto-instrumentor produced. The Attributes tab in Arize AX showing the get_weather tool span with the manually-set tool.description alongside tool.name The relevant attributes for this example are:
{
    "openinference": {
        "span": {
            "kind": "TOOL"
        }
    },
    "tool": {
        "description": "Looks up the current weather conditions for a given city.",
        "name": "get_weather"
    }
}
The get_time_zone tool span in the same trace still has only tool.name, which is a good visual confirmation that the override applies only to the span you set attributes on.

Summary

You have now seen the building blocks for tracing AI applications with OpenInference and Arize AX:
  • OpenInference layers on top of OpenTelemetry to add semantic conventions and auto-instrumentors for AI-specific concepts. The standard OpenTelemetry SDK still drives everything underneath — TracerProvider, Tracer, Span, SpanProcessor, and Exporter are all unchanged.
  • Spans, traces, and sessions form a hierarchy. A span is one step. A trace is a tree of spans tied together by trace_id. A session is a group of traces tied together by session.id. Sessions are how you stitch a multi-turn conversation together in Arize AX.
  • There are three ways to capture spans. Auto-instrumentors wrap SDKs and emit spans for every call automatically. Manual instrumentation lets you create spans yourself with tracer.start_as_current_span(...). Hybrid instrumentation combines the two — your manual spans wrap auto-instrumented calls and become their parents in the trace tree.
  • Every span carries a small set of common attributesopeninference.span.kind, input.value, output.value, metadata, session.id, user.id, and tag.tags — regardless of the kind. The openinference.span.kind attribute drives how Arize AX renders the span.
  • Four span kinds cover most AI applications: LLM, chain, agent, and tool. Each adds a kind-specific set of attributes — llm.* for model name, token counts, and costs; tool.* for tool name and description; agent.name (or graph.node.id for multi-agent graphs); chain spans rely on the common attributes alone.
  • You can enrich auto-instrumented spans. Use OpenInference context managers like using_session, using_metadata, using_tags, and using_prompt_template to attach attributes that the auto-instrumentor picks up via the OpenTelemetry context. You can also override or add specific attributes by grabbing the active span inside a tool function and calling set_attribute(...) directly.

Where to go next