This documentation provides a guide on using OpenInference OTEL tracing decorators and methods for instrumenting functions, chains, agents, and tools using OpenTelemetry.These tools can be combined with, or used in place of, OpenTelemetry instrumentation code. They are designed to simplify the instrumentation process.Let your coding agent set this up. Works with Claude Code, Codex, Cursor, and OpenCode.
1
Start Phoenix
In one terminal, start Phoenix and leave it running. It serves at http://localhost:6006:
uvx arize-phoenix serve
Already have a Phoenix deployment? Skip this step. px setup can point at any running Phoenix, including a self-hosted one.
2
Add tracing with a coding agent
In a new terminal, from your app’s root directory, run one of these:
npx -y @arizeai/phoenix-cli setup
npm i -g @arizeai/phoenix-cli && px setup
px setup hands the instrumentation to your coding agent and waits until a real trace arrives. It can also install the Phoenix skills so your agent can query what you capture. See Connect a project with px setup for the full flow, CI usage, and unsupported agents.
Version arize-phoenix-otel>=0.16.0 is required for the examples on this page. Starting in 0.16.0, phoenix.otel re-exports the OpenInference context managers (using_session, using_user, using_metadata, using_tags, using_attributes, using_prompt_template, suppress_tracing) and semantic conventions (SpanAttributes, OpenInferenceSpanKindValues, OpenInferenceMimeTypeValues) directly — no need to also install openinference-instrumentation or openinference-semantic-conventions.On older versions, import the same symbols from openinference.instrumentation and openinference.semconv.trace instead.
npm install @arizeai/phoenix-otel
@arizeai/phoenix-otel re-exports the OpenInference tracing helpers and context utilities, so Phoenix users can register tracing and add decorators or span helpers from the same package.For detailed API documentation, consult the respective documentation sites.
Phoenix TypeScript API
OpenInference JavaScript API
Phoenix does not ship a phoenix-otel-go wrapper. Install the OpenInference Go semconv + instrumentation packages plus the stock OpenTelemetry Go SDK:
go get github.com/Arize-ai/openinference/go/openinference-semantic-conventionsgo get github.com/Arize-ai/openinference/go/openinference-instrumentationgo get go.opentelemetry.io/otelgo get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttpgo get go.opentelemetry.io/otel/sdk
The semconv package provides typed constants for the wire-format-stable attribute keys; instrumentation provides context-attribute helpers (session, user, metadata, tags, suppression).
Configure the OpenTelemetry Go SDK with an OTLP/HTTP exporter pointed at Phoenix, then obtain a tracer. For a remote deployment, set PHOENIX_COLLECTOR_ENDPOINT and, if it has authentication enabled, PHOENIX_API_KEY.
Go has no decorator syntax. Wrap the function body in a tracer.Start span and set input / output / span-kind attributes manually using the OpenInference Go semconv constants:
This entire function will appear as a Span in Phoenix. Input and output attributes in Phoenix will be set automatically based on my_func’s parameters and return. The status attribute will also be set automatically.
with tracer.start_as_current_span( "my-span-name", openinference_span_kind="chain",) as span: span.set_input("input") span.set_output("output") span.set_status(Status(StatusCode.OK))
The code within this clause will be captured as a Span in Phoenix. Here the input, output, and status must be set manually.This approach is useful when you need only a portion of a method to be captured as a Span.
OpenInference Span Kinds denote the possible types of spans you might capture, and will be rendered different in the Phoenix UI.The openinference.span.kind attribute is required for all OpenInference spans and identifies the type of operation being traced. The span kind provides a hint to the tracing backend as to how the trace should be assembled. Valid values include:
Span Kind
Description
LLM
A span that represents a call to a Large Language Model (LLM). For example, an LLM span could be used to represent a call to OpenAI or Llama for chat completions or text generation.
EMBEDDING
A span that represents a call to an LLM or embedding service for generating embeddings. For example, an Embedding span could be used to represent a call to OpenAI to get an ada embedding for retrieval.
CHAIN
A span that represents a starting point or a link between different LLM application steps. For example, a Chain span could be used to represent the beginning of a request to an LLM application or the glue code that passes context from a retriever to an LLM call.
RETRIEVER
A span that represents a data retrieval step. For example, a Retriever span could be used to represent a call to a vector store or a database to fetch documents or information.
RERANKER
A span that represents the reranking of a set of input documents. For example, a cross-encoder may be used to compute the input documents’ relevance scores with respect to a user query, and the top K documents with the highest scores are then returned by the Reranker.
TOOL
A span that represents a call to an external tool such as a calculator, weather API, or any function execution that is invoked by an LLM or agent.
AGENT
A span that encompasses calls to LLMs and Tools. An agent describes a reasoning block that acts on tools using the guidance of an LLM.
GUARDRAIL
A span that represents calls to a component to protect against jailbreak user input prompts by taking action to modify or reject an LLM’s response if it contains undesirable content. For example, a Guardrail span could involve checking if an LLM’s output response contains inappropriate language, via a custom or external guardrail library, and then amending the LLM response to remove references to the inappropriate language.
EVALUATOR
A span that represents a call to a function or process performing an evaluation of the language model’s outputs. Examples include assessing the relevance, correctness, or helpfulness of the language model’s answers.
DECISION
Experimental. A span that represents a call to a decision model, which scores or selects among candidate options provided in the request rather than generating free-form text. Support is still being built out and no decision-model provider is fully supported yet.
with tracer.start_as_current_span( "chain-span-with-plain-text-io", openinference_span_kind="chain",) as span: span.set_input("input") span.set_output("output") span.set_status(Status(StatusCode.OK))
Set the OpenInference span kind to CHAIN on any span that represents a step or glue point in your application. Inputs and outputs go on the same span via the semconv constants:
with tracer.start_as_current_span( "agent-span-with-plain-text-io", openinference_span_kind="agent",) as span: span.set_input("input") span.set_output("output") span.set_status(Status(StatusCode.OK))
import { traceTool } from "@arizeai/phoenix-otel";/** * this tool description should be overridden */const thisToolNameShouldBeOverridden = traceTool( (input1: string, input2: number): void => { // Tool implementation }, { name: "decorated-tool-with-overridden-name", description: "overridden-tool-description", });thisToolNameShouldBeOverridden("input1", 1);
Use the TOOL span kind for spans that wrap calls to external functions, APIs, or any tool the agent invokes. The tool name and inputs are conventionally attached to the same span:
Like other span kinds, LLM spans can be instrumented either via a context manager or via a decorator pattern. It’s also possible to directly patch client methods.While this guide uses the OpenAI Python client for illustration, in practice, you should use the OpenInference auto-instrumentors for OpenAI whenever possible and resort to manual instrumentation for LLM spans only as a last resort.To run the snippets in this section, set your OPENAI_API_KEY environment variable.
This decorator pattern above works for sync functions, async coroutine functions, sync generator functions, and async generator functions. Here’s an example with an async generator.
from typing import AsyncGenerator, Listfrom openai import AsyncOpenAIfrom openai.types.chat import ChatCompletionMessageParamopenai_async_client = AsyncOpenAI()@tracer.llmasync def stream_llm_responses( messages: List[ChatCompletionMessageParam],) -> AsyncGenerator[str, None]: stream = await openai_async_client.chat.completions.create( model="gpt-4o", messages=messages, stream=True, ) async for chunk in stream: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content# invoke inside of an async contextasync for token in stream_llm_responses([{"role": "user", "content": "Hello, world!"}]): print(token, end="")
It’s also possible to directly patch methods on a client. This is useful if you want to transparently use the client in your application with instrumentation logic localized in one place.
from openai import OpenAIopenai_client = OpenAI()# patch the create methodwrapper = tracer.llmopenai_client.chat.completions.create = wrapper(openai_client.chat.completions.create)# invoke the patched method normallyopenai_client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Hello, world!"}],)
The snippets above produce LLM spans with input and output values, but don’t offer rich UI for messages, tools, invocation parameters, etc. In order to manually instrument LLM spans with these features, users can define their own functions to wrangle the input and output of their LLM calls into OpenInference format. These richer LLM attribute helpers are not re-exported from phoenix.otel — install openinference-instrumentation to access them:
When using the tracer.llm decorator, these functions are passed via the process_input and process_output parameters and should satisfy the following:
The input signature of process_input should exactly match the input signature of the decorated function.
The input signature of process_output has a single argument, the output of the decorated function. This argument accepts the returned value when the decorated function is a sync or async function, or a list of yielded values when the decorated function is a sync or async generator function.
Both process_input and process_output should output a dictionary mapping attribute names to values.
As before, it’s possible to directly patch the method on the client. Just ensure that the input signatures of process_input and the patched method match.
from openai import OpenAIfrom openai.types.chat import ChatCompletionMessageParamopenai_client = OpenAI()# patch the create methodwrapper = tracer.llm( process_input=process_input, process_output=process_output,)openai_client.chat.completions.create = wrapper(openai_client.chat.completions.create)# invoke the patched method normallyopenai_client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Hello, world!"}],)
The snippets above produce LLM spans with input and output values, but don’t offer rich UI for messages, tools, invocation parameters, etc. In order to manually instrument LLM spans with these features, users can use helper functions from @arizeai/phoenix-otel that produce valid OpenInference attributes for LLM spans:
When using withSpan to wrap functions, you can pass processInput and processOutput functions as options. These should satisfy the following:
The input signature of processInput should exactly match the input signature of the wrapped function.
The input signature of processOutput has a single argument, the output of the wrapped function. This argument accepts the returned value when the wrapped function is a sync or async function.
Both processInput and processOutput should output a dictionary mapping attribute names to values.
For SDKs with OpenInference Go auto-instrumentation, see OpenAI Go SDK and Anthropic SDK Go — no manual span code required. For SDKs without auto-instrumentation, attach model and token attributes directly using semconv:
from phoenix.otel import suppress_tracingwith suppress_tracing(): # this trace will not be recorded with tracer.start_as_current_span( "THIS-SPAN-SHOULD-NOT-BE-TRACED", openinference_span_kind="chain", ) as span: span.set_input("input") span.set_output("output") span.set_status(Status(StatusCode.OK))
import { withSpan } from "@arizeai/phoenix-otel";import { trace, context, suppressTracing } from "@arizeai/phoenix-otel";await context.with(suppressTracing(context.active()), async () => { // this trace will not be recorded await withSpan( async () => { const span = trace.getActiveSpan(); if (span) { span.setAttributes({ "input.value": "input", "output.value": "output", }); } }, { name: "THIS-SPAN-SHOULD-NOT-BE-TRACED", kind: "CHAIN", } );});
Use instrumentation.WithSuppression to mark a context such that any OpenInference-aware Go instrumentor (e.g. the OpenAI and Anthropic Go transports / middlewares) skips span emission for calls descended from it. Typical use: evaluator or grader code that itself calls an LLM but should not appear in the customer’s product trace.
from phoenix.otel import using_attributeswith using_attributes(session_id="123"): # this trace has session id "123" with tracer.start_as_current_span( "chain-span-with-context-attributes", openinference_span_kind="chain", ) as span: span.set_input("input") span.set_output("output") span.set_status(Status(StatusCode.OK))
import { context } from "@opentelemetry/api";import { setSession } from "@arizeai/phoenix-otel";import { withSpan } from "@arizeai/phoenix-otel";import { trace } from "@arizeai/phoenix-otel";await context.with( setSession(context.active(), { sessionId: "123" }), async () => { // this trace has session id "123" await withSpan( async () => { const span = trace.getActiveSpan(); if (span) { span.setAttributes({ "input.value": "input", "output.value": "output", }); } }, { name: "chain-span-with-context-attributes", kind: "CHAIN", } ); });
Use the helpers in github.com/Arize-ai/openinference/go/openinference-instrumentation to store session, user, metadata, and tags on context.Context. Auto-instrumentors (OpenAI Go, Anthropic Go) automatically apply these to every LLM span descended from the context:
import ( "encoding/json" "github.com/Arize-ai/openinference/go/openinference-instrumentation")metadata := map[string]any{"key-1": "value_1", "key-2": "value_2"}metadataJSON, _ := json.Marshal(metadata)ctx = instrumentation.WithSession(ctx, "my-session-id")ctx = instrumentation.WithUser(ctx, "my-user-id")ctx = instrumentation.WithMetadata(ctx, string(metadataJSON))ctx = instrumentation.WithTags(ctx, "tag_1", "tag_2")// All LLM spans descended from ctx now carry session.id, user.id,// metadata, and tag.tags — no per-span code needed.resp, _ := client.CreateChatCompletion(ctx, req)
For manual spans you author yourself, set the same attributes directly via the semconv keys (SessionID, UserID, Metadata, TagTags) after tracer.Start.
OpenInference includes message types that can be useful in composing text and image or other file inputs and outputs:
Python
TypeScript
Go
import openinference.instrumentation as oiimage_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"text = "describe the weather in this image"content = [ {"type": "text", "text": text}, { "type": "image_url", "image_url": {"url": image_url, "detail": "low"}, }, ]image = oi.Image(url=image_url)contents = [ oi.TextMessageContent( type="text", text=text, ), oi.ImageMessageContent( type="image", image=image, ),]messages = [ oi.Message( role="user", contents=contents, )]with tracer.start_as_current_span( "my-span-name", openinference_span_kind="llm", attributes=oi.get_llm_attributes(input_messages=messages)) as span: span.set_input(text) # Call your LLM here response = "This is a test response" span.set_output(response) print(response.content)
import { withSpan, getLLMAttributes } from "@arizeai/phoenix-otel";import { trace } from "@arizeai/phoenix-otel";const imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg";const text = "describe the weather in this image";const messages = [ { role: "user", contents: [ { type: "text", text: text, }, { type: "image", image: { url: imageUrl, }, }, ], },];await withSpan( async () => { const span = trace.getActiveSpan(); if (span) { span.setAttributes( getLLMAttributes({ inputMessages: messages, }) ); // Call your LLM here const response = "This is a test response"; span.setAttributes({ "input.value": text, "output.value": response, }); console.log(response); } }, { name: "my-span-name", kind: "LLM", });
For SDKs with OpenInference Go auto-instrumentation (OpenAI Go, Anthropic Go), multimodal content is captured automatically when you call the SDK with image-containing messages — no per-span code required.For manual spans, attach image content using the indexed llm.input_messages.{i}.message.contents.{j}.* attributes from github.com/Arize-ai/openinference/go/openinference-semantic-conventions. The Python openinference.instrumentation message-builder helpers do not yet have a Go equivalent — refer to the OpenInference spec for the full multimodal attribute taxonomy.