---
title: "OpenInference Best Practices"
description: "Learn OpenInference best practices for Phoenix and how to enrich auto-instrumented traces with LLM, tool, agent, chain, and session attributes."
keywords: ['openinference', 'span kinds', 'tracing', 'opentelemetry', 'llm', 'tool', 'agent', 'chain', 'sessions']
---

<Frame caption="Work through this cookbook in an interactive notebook">
<Card title="Google Colab" href="https://colab.research.google.com/github/Arize-ai/phoenix/blob/main/tutorials/tracing/openinference_best_practices_tutorial.ipynb" icon="https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/gc.ico" horizontal/>
</Frame>

This guide is a hands-on tour of OpenInference best practices for tracing AI applications with Phoenix. 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 Phoenix trace view. You can [run the companion notebook in Colab](https://colab.research.google.com/github/Arize-ai/phoenix/blob/main/tutorials/tracing/openinference_best_practices_tutorial.ipynb), or follow along locally by running each code block independently in your venv.

<Info>
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.
</Info>

## Initial setup

This guide talks to a locally-running Phoenix instance. Start Phoenix in a separate terminal:

```bash
pip install arize-phoenix
phoenix serve
```

This starts the Phoenix UI at [http://localhost:6006](http://localhost:6006). Leave it running — every code block in this guide sends traces to that endpoint.

### 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:

```bash
pip install openai openai-agents arize-phoenix-otel \
    openinference-instrumentation-openai \
    openinference-instrumentation-openai-agents \
    opentelemetry-sdk opentelemetry-exporter-otlp
```

### Set environment variables

You need one secret: your OpenAI API key. Export it in the same shell session you will run the code from:

```bash
export OPENAI_API_KEY="your-openai-api-key"
```

The OpenAI SDK reads `OPENAI_API_KEY` automatically. Phoenix is running locally, so no Phoenix API key is needed (the `register()` call below uses the default `http://localhost:6006` endpoint).

### Setup tracing

Every runnable code block in this guide includes the same tracing setup at the top. It uses the `phoenix.otel.register()` convenience function to register a tracer provider that sends spans to your local Phoenix instance at `http://localhost:6006`, then enables the OpenAI auto-instrumentor so calls to the OpenAI SDK are traced automatically.

See [The phoenix.otel helpers](/docs/phoenix/tracing/concepts-tracing/otel-openinference/phoenix-otel-helpers) for the full set of `phoenix.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:

```python
from openinference.instrumentation.openai import OpenAIInstrumentor
from phoenix.otel import register

tracer_provider = register(
    project_name="otel-best-practices",
    batch=False,
)

OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
```

## Introduction to OpenInference

[OpenInference](https://github.com/Arize-ai/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 Phoenix uses to render LLM, tool, agent, chain, retriever, and other AI-specific spans in the trace view.

OpenInference is an **extension to [OpenTelemetry](https://opentelemetry.io/docs/)**, 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 Phoenix) can render them as a rich, AI-aware trace view rather than a generic span list.

**Read more:**

- [OpenTelemetry and OpenInference concepts](/docs/phoenix/tracing/concepts-tracing/otel-openinference/overview) — the full reference companion to this guide.
- [OpenInference semantic conventions spec](https://github.com/Arize-ai/openinference/tree/main/spec) — the formal definitions for span kinds, attributes, and message structure.
- [OpenInference repository](https://github.com/Arize-ai/openinference) — auto-instrumentors, examples, and source for every supported language and framework.
- [OpenTelemetry documentation](https://opentelemetry.io/docs/) — the underlying observability framework that OpenInference builds on.

The code below simulates having a conversation with a chatbot and asking it two related questions. It will create traces in Phoenix 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:

```python
import uuid

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
from phoenix.otel import register

tracer_provider = register(
    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 [Phoenix](http://localhost:6006) 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 Phoenix 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](/docs/phoenix/tracing/concepts-tracing/otel-openinference/signals).

#### Sessions

In Phoenix, start with the **Sessions** tab. You should see a single session that represents the entire two step conversation.

![The Sessions tab in Phoenix showing one session for the chatbot conversation](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/sessions.png)

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 Phoenix for the chatbot session, showing both conversation turns with input, output, latency, tokens, and cost](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/session.png)

#### 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 Phoenix showing two traces from the chatbot conversation](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/traces.png)

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 Phoenix for a single trace, showing the span tree with latency, token counts, and cost](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/trace.png)

<Info>
You can also navigate to the individual traces directly from the session view.
</Info>

#### 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 Phoenix for a single trace, showing the span tree with latency, token counts, and cost](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/trace.png)

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 Phoenix trace tree](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/llm-span.png)

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 Phoenix showing the JSON attributes for a selected span](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/span-attributes.png)

OpenInference defines a fixed set of span kinds:

| Span kind | Description |
| --- | --- |
| `LLM` | A call to a large language model. Captures the model, input messages, output messages, token counts, and cost. |
| `CHAIN` | A starting point or link between application steps. Commonly used as a parent span to group related work into a logical block. |
| `AGENT` | A span representing an agent's work — typically wraps LLM and tool spans together |
| `TOOL` | A call to an external tool or function, often invoked in response to a tool-use request from an LLM |
| `RETRIEVER` | A retrieval operation, such as fetching documents from a vector store or search index |
| `EMBEDDING` | A call to an embedding model |
| `RERANKER` | A reranking step that reorders a set of retrieved documents |
| `GUARDRAIL` | A safety or policy check, such as content moderation, PII detection, or input validation |
| `EVALUATOR` | An evaluation step that scores or judges an LLM output |
| `PROMPT` | A prompt definition or templating step |
| `UNKNOWN` | Used 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](/docs/phoenix/tracing/concepts-tracing/otel-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](/docs/phoenix/tracing/concepts-tracing/otel-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:

```python
from openai import OpenAI
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from phoenix.otel import register

tracer_provider = register(
    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 Phoenix. You will see a single session with multiple traces depending on how many times you ran the code.

![The Sessions tab in Phoenix showing a single session with multiple traces](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/sessions-1-session.png)

Now run this code, which uses a different session id and so will create a new session:

```python
from openai import OpenAI
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from phoenix.otel import register

tracer_provider = register(
    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 Phoenix showing two distinct sessions in the list](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/sessions-2-sessions.png)

## 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](/docs/phoenix/tracing/concepts-tracing/otel-openinference/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:

```python
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](https://github.com/Arize-ai/openinference) 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:

```python
from openai import OpenAI
from openinference.instrumentation import using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from phoenix.otel import register

tracer_provider = register(
    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 Phoenix 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:

```python
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 Phoenix 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:

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

tracer_provider = register(
    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 Phoenix 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 Phoenix showing the data-pipeline chain span with step-1-validate and step-2-format child chain spans nested inside](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/data-pipeline.png)

### 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 Phoenix 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:

```python
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
from phoenix.otel import register

tracer_provider = register(
    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 Phoenix 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:

| Attribute | Description |
| --- | --- |
| `openinference.span.kind` | The span kind: `LLM`, `CHAIN`, `AGENT`, `TOOL`, `RETRIEVER`, `EMBEDDING`, `RERANKER`, `GUARDRAIL`, `EVALUATOR`, `PROMPT`, or `UNKNOWN`. Controls how Phoenix renders the span. |
| `input.value` | The input to the span as a string. If the value is structured, serialize it to JSON and set `input.mime_type` accordingly. |
| `input.mime_type` | The mime type of `input.value`. Defaults to `text/plain`; set to `application/json` if the value is a JSON string. |
| `output.value` | The output from the span as a string |
| `output.mime_type` | The mime type of `output.value`. Same convention as `input.mime_type`. |
| `metadata` | A JSON dictionary of your own fields. Use it to attach domain-specific context such as user tier, feature flag, or request id. |
| `session.id` | Groups multiple traces into a session. Set with `using_session(...)` or directly via `span.set_attribute(SpanAttributes.SESSION_ID, ...)`. |
| `user.id` | Identifies the user the trace belongs to. Set with `using_user(...)` or directly. |
| `tag.tags` | A list of string tags for filtering. Set with `using_tags(...)`. |

Phoenix 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](/docs/phoenix/tracing/concepts-tracing/otel-openinference/semantic-conventions) (the overall standard) and [OpenInference span kinds](/docs/phoenix/tracing/concepts-tracing/otel-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](/docs/phoenix/tracing/concepts-tracing/otel-openinference/span-kinds).

The following example uses the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) 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 Phoenix. Save and run it:

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

tracer_provider = register(
    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)
```

<Info>
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.
</Info>

Open the trace in Phoenix 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 Phoenix from the OpenAI Agents SDK example, showing agent, chain, tool, and LLM spans nested together](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/agent-trace.png)

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 Phoenix showing the full attribute set for an LLM span](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/llm-span-attributes.png)

The relevant attributes for this example are:

```json
{
    "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:

| Attribute | Description |
| --- | --- |
| `llm.model_name` | The 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.provider` | The LLM provider, here `openai` |
| `llm.system` | The AI system identifier, also `openai` |
| `llm.invocation_parameters` | A JSON string of the parameters passed to the API: `{"model": "gpt-5.4-mini"}` |
| `llm.input_messages` | The messages sent to the API as a structured array. Each entry has `message.role` and `message.content`. |
| `llm.output_messages` | The 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` / `.total` | Token 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` / `.total` | Estimated cost in USD with the same `_details` breakdowns. Phoenix computes these from the token counts and the published pricing for the model. |

Phoenix 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 Phoenix showing the get_weather tool span attributes, including the input city and the returned weather text](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/tool-span-attributes.png)

The relevant attributes for this example are:

```json
{
    "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:

| Attribute | Description |
| --- | --- |
| `tool.id` | The identifier for the result of the tool call. Corresponds to the `tool_call.id` emitted by the LLM, which lets Phoenix link the tool span back to the LLM call that requested it. |
| `tool.name` | The name of the tool |
| `tool.description` | The tool's description. The LLM uses this when deciding which tool to call. |
| `tool.parameters` | A JSON string of the parameter values the LLM passed to the tool |
| `tool.json_schema` | The full JSON schema of the tool's input, typically in OpenAI tool-calling format. Tells the LLM what shape of arguments the tool expects. |

Phoenix 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 Phoenix showing the TravelAssistant agent span attributes](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/agent-span-attributes.png)

The relevant attributes for this example are:

```json
{
    "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 Phoenix 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:

| Attribute | Description |
| --- | --- |
| `agent.name` | The name of the agent. Agents that perform the same logical role should share a name so you can group their traces together. |
| `graph.node.id` | The 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 Phoenix. |
| `graph.node.name` | A human-readable name for the graph node |
| `graph.node.parent_id` | The id of the parent node. Leave unset for a root agent. |

The `graph.node.*` attributes are how Phoenix 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 Phoenix showing a turn chain span grouping the LLM and tool calls for one agent turn](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/chain-span-attributes.png)

The relevant attributes for this example are:

```json
{
    "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 manager | Sets |
| --- | --- |
| `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](/docs/phoenix/tracing/concepts-tracing/otel-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:

```python
from openai import OpenAI
from openinference.instrumentation import using_metadata, using_session
from openinference.instrumentation.openai import OpenAIInstrumentor
from phoenix.otel import register

tracer_provider = register(
    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 Phoenix. 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 Phoenix showing an LLM span with the custom metadata attribute alongside the standard llm.* attributes](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/llm-span-metadata.png)

You can also filter spans by `metadata` values in the Phoenix 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 Phoenix filtered by attributes.metadata.request_source equal to cookbook](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/metadata-filter.png)

### 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:

```python
from agents import Agent, Runner, function_tool
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
from phoenix.otel import register

tracer_provider = register(
    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 Phoenix 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 Phoenix showing the get_weather tool span with the manually-set tool.description alongside tool.name](https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/cookbooks/otel-cookbook/tool-span-attributes-override.png)

The relevant attributes for this example are:

```json
{
    "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 Phoenix:

- **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 Phoenix.
- **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 attributes** — `openinference.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 Phoenix 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

- Read the [OpenTelemetry and OpenInference concepts](/docs/phoenix/tracing/concepts-tracing/otel-openinference/overview) section of the Arize docs for the full reference companion to this guide
- Dive into the [OpenInference span kinds](/docs/phoenix/tracing/concepts-tracing/otel-openinference/span-kinds) reference for every span kind and the attributes each carries
- Read [Instrumentation approaches](/docs/phoenix/tracing/concepts-tracing/otel-openinference/instrumentation-approaches) for a deeper comparison of auto, manual, and hybrid instrumentation
- Learn how to [propagate context across services or async boundaries](/docs/phoenix/tracing/concepts-tracing/otel-openinference/context-propagation) when your app spans multiple processes
- Reduce trace volume in production with [sampling](/docs/phoenix/tracing/concepts-tracing/otel-openinference/sampling)
- Browse the [OpenInference repository](https://github.com/Arize-ai/openinference) for auto-instrumentors covering Anthropic, Bedrock, LangChain, LlamaIndex, CrewAI, AutoGen, and many others
- Read the [OpenInference semantic conventions spec](https://github.com/Arize-ai/openinference/tree/main/spec) for the source-of-truth attribute definitions
