# Phoenix OTEL Reference

Welcome to the Phoenix OTEL reference documentation. This package provides a lightweight wrapper around OpenTelemetry primitives with Phoenix-aware defaults and tracing decorators for common GenAI patterns. `arize-phoenix-otel` simplifies OpenTelemetry configuration for Phoenix users by providing:

- Phoenix-aware defaults for common OpenTelemetry primitives
- Automatic configuration from environment variables 
- Drop-in replacements for OTel classes with enhanced functionality
- Simplified tracing setup with the `register()` function
- Tracing decorators for GenAI patterns

The key components include:

- **[Register](api/register)** - Simple setup and configuration with `register()`
- **[TracerProvider](api/provider)** - Phoenix-aware tracer provider
- **[Span Processors](api/processors)** - Batch and simple span processors
- **[Exporters](api/exporters)** - HTTP and gRPC span exporters

## Installation

```bash
pip install arize-phoenix-otel
```

## Quick Start

The simplest way to get started is with the `register()` function:

```python
from phoenix.otel import register

# Simple setup with automatic instrumentation (recommended)
tracer_provider = register(auto_instrument=True)
```

This is all you need to get started using OpenTelemetry with Phoenix! The `register()` function defaults to sending spans to an endpoint at `http://localhost` using gRPC. With `auto_instrument=True`, your AI/ML libraries are automatically traced

### Basic Configuration

```python
from phoenix.otel import register

# Configure project name, headers, and batch processing
tracer_provider = register(
    project_name="my-llm-app", headers={"Authorization": "Bearer TOKEN"}, batch=True
)
```

### Automatic Instrumentation (Recommended)

For the best experience with minimal setup, enable automatic instrumentation by setting `auto_instrument=True`. This automatically instruments popular AI/ML libraries like OpenAI, Anthropic, LangChain, LlamaIndex, and many others:

```python
from phoenix.otel import register

# Recommended: Enable automatic instrumentation
tracer_provider = register(
    project_name="my-llm-app",
    auto_instrument=True,  # Automatically instruments AI/ML libraries
)

# Now your OpenAI, LangChain, etc. calls are automatically traced!
import openai

client = openai.OpenAI()
response = client.chat.completions.create(  # This will be automatically traced
    model="gpt-4", messages=[{"role": "user", "content": "Hello!"}]
)
```

**Note**: `auto_instrument=True` only works if the corresponding OpenInference instrumentation libraries are installed. For example, to automatically trace OpenAI calls, you need `openinference-instrumentation-openai` installed:

```bash
pip install openinference-instrumentation-openai
pip install openinference-instrumentation-langchain  # For LangChain
pip install openinference-instrumentation-llama-index  # For LlamaIndex
```

See the [OpenInference repository](https://github.com/Arize-ai/openinference) for the complete list of available instrumentation packages.

**Production Tip**: For production deployments, set `batch=True` to improve performance by batching spans before sending them to the collector. Spans are exported in the background, so your application won't be blocked:

```python
tracer_provider = register(
    auto_instrument=True,
    batch=True,  # Recommended for production - exports spans in background
)
```

## Authentication

If the `PHOENIX_API_KEY` environment variable is set, `register()` will automatically add an `authorization` header to each span payload.

```python
# Set environment variable
# export PHOENIX_API_KEY=your-api-key

from phoenix.otel import register

tracer_provider = register()  # Automatically uses API key for auth
```

## Endpoint Configuration

There are two ways to configure the collector endpoint:

### Using Environment Variables

If you're setting the `PHOENIX_COLLECTOR_ENDPOINT` environment variable, `register()` will automatically try to send spans to your Phoenix server using gRPC.

```bash
export PHOENIX_COLLECTOR_ENDPOINT=https://your-phoenix.com:6006
```

```python
from phoenix.otel import register

tracer_provider = register()
```

### Specifying the Endpoint Directly

When passing in the `endpoint` argument, **you must specify the fully qualified endpoint**. For example, to export spans via HTTP to localhost, use Phoenix's HTTP collector endpoint: `http://localhost:6006/v1/traces`. The default gRPC endpoint is different: `http://localhost:4317`.

```python
from phoenix.otel import register

# HTTP endpoint
tracer_provider = register(endpoint="http://localhost:6006/v1/traces")

# Custom gRPC endpoint
tracer_provider = register(endpoint="http://localhost:9999", protocol="grpc")
```

The `protocol` argument can be used to enforce the OTLP transport protocol regardless of the endpoint specified. Valid protocols are: "http/protobuf" and "grpc".

## Drop-in Replacement for OTel Primitives

For more granular tracing configuration, Phoenix OTEL wrappers can be used as drop-in replacements for OpenTelemetry primitives:

```python
from opentelemetry import trace as trace_api
from phoenix.otel import HTTPSpanExporter, TracerProvider, SimpleSpanProcessor

tracer_provider = TracerProvider()
span_exporter = HTTPSpanExporter(endpoint="http://localhost:6006/v1/traces")
span_processor = SimpleSpanProcessor(span_exporter=span_exporter)
tracer_provider.add_span_processor(span_processor)
trace_api.set_tracer_provider(tracer_provider)
```

### Simplified Configuration with Endpoint Inference

Wrappers have Phoenix-aware defaults to greatly simplify the OpenTelemetry configuration process. A special `endpoint` keyword argument can be passed to either a `TracerProvider`, `SimpleSpanProcessor` or `BatchSpanProcessor` to automatically infer which `SpanExporter` to use:

```python
from phoenix.otel import TracerProvider

# Automatically configures the appropriate span exporter
tracer_provider = TracerProvider(endpoint="http://localhost:4317")
```

## Span Processors

### Multiple Span Processors

The `phoenix.otel` TracerProvider automatically creates a default span processor that sends spans to the Phoenix collector endpoint. By default, adding a new span processor will replace this auto-created processor.

To keep the default processor alongside new ones, pass `replace_default_processor=False`:

```python
from phoenix.otel import TracerProvider, BatchSpanProcessor

# TracerProvider automatically creates a default processor
tracer_provider = TracerProvider()

# This replaces the default processor (default behavior)
tracer_provider.add_span_processor(BatchSpanProcessor())

# This keeps the default processor and adds another one
tracer_provider.add_span_processor(BatchSpanProcessor(), replace_default_processor=False)
```

### Batch Processing

```python
from phoenix.otel import TracerProvider, BatchSpanProcessor

tracer_provider = TracerProvider()
batch_processor = BatchSpanProcessor()
tracer_provider.add_span_processor(batch_processor)
```

### Custom GRPC Endpoints

```python
from phoenix.otel import TracerProvider, BatchSpanProcessor, GRPCSpanExporter

tracer_provider = TracerProvider()
batch_processor = BatchSpanProcessor(
    span_exporter=GRPCSpanExporter(endpoint="http://custom-endpoint.com:6789")
)
tracer_provider.add_span_processor(batch_processor)
```

## Resource Configuration

```python
from opentelemetry import trace as trace_api
from phoenix.otel import Resource, PROJECT_NAME, TracerProvider

tracer_provider = TracerProvider(resource=Resource({PROJECT_NAME: "my-project"}))
trace_api.set_tracer_provider(tracer_provider)
```

## Advanced Configuration

Both `register()` and `TracerProvider` accept all the same keyword arguments as the standard OpenTelemetry `TracerProvider`, allowing you to configure advanced features like custom ID generators, sampling, and span limits.

```python
from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
from phoenix.otel import register, TracerProvider

# Configure directly with register()
tracer_provider = register(
    project_name="my-app",
    id_generator=AwsXRayIdGenerator(),  # AWS X-Ray compatible IDs
    sampler=TraceIdRatioBased(0.1),  # Sample 10% of traces
)

# Or configure TracerProvider directly
tracer_provider = TracerProvider(
    project_name="my-app", id_generator=AwsXRayIdGenerator(), sampler=TraceIdRatioBased(0.5)
)
```

## Environment Variables

The package recognizes these Phoenix-specific environment variables for automatic configuration:

- `PHOENIX_COLLECTOR_ENDPOINT`: Collector endpoint URL (e.g., `https://your-phoenix.com:6006`)
- `PHOENIX_PROJECT_NAME`: Project name for spans (e.g., `my-llm-app`)
- `PHOENIX_API_KEY`: User or system key
- `PHOENIX_GRPC_PORT`: gRPC port override (defaults to 4317)
- `PHOENIX_CLIENT_HEADERS`: Additional headers for requests (e.g., `Authorization=Bearer token,custom-header=value`)

### Environment Variable Examples

```bash
# Local Phoenix server (default)
export PHOENIX_COLLECTOR_ENDPOINT="http://localhost:6006"

# Remote Phoenix instance with API key
export PHOENIX_API_KEY="your-api-key"
export PHOENIX_COLLECTOR_ENDPOINT="https://your-phoenix-instance.com"
export PHOENIX_PROJECT_NAME="production-app"

# Custom Phoenix instance with authentication
export PHOENIX_COLLECTOR_ENDPOINT="https://your-phoenix-instance.com"
export PHOENIX_API_KEY="your-api-key"
export PHOENIX_CLIENT_HEADERS="Authorization=Bearer your-api-key,custom-header=value"

# Override default gRPC port
export PHOENIX_GRPC_PORT="9999"
```

When these environment variables are set, `register()` and Phoenix OTEL components will automatically use them for configuration.

## Using Decorators

Phoenix provides helpful decorators to make manual instrumentation easier. After setting up your tracer, you can instrument functions with simple decorators:

```python
from phoenix.otel import register

tracer_provider = register(project_name="my-project")
tracer = tracer_provider.get_tracer(__name__)


@tracer.chain
def process_data(input_text: str) -> str:
    return f"Processed: {input_text}"


result = process_data("hello world")  # This call will be traced
```

### OpenInference Span Kinds

Phoenix decorators support different span kinds that correspond to common AI application patterns:

- **Chains**: For sequential operations and workflows
- **Agents**: For autonomous AI agents
- **Tools**: For function calls and tool usage
- **LLMs**: For language model interactions
- **Retrievers**: For retrieval operations
- **Embeddings**: For embedding generation

### Chains

Use `@tracer.chain` for sequential operations and workflows:

#### Using Decorators

```python
@tracer.chain
def process_document(document: str) -> str:
    # Process the document
    processed = document.upper()
    return processed


# Call the decorated function
result = process_document("hello world")
```

#### Using Context Managers

```python
def process_document_manual(document: str) -> str:
    with tracer.start_as_current_span("process_document", openinference_span_kind="chain") as span:
        span.set_input(document)
        processed = document.upper()
        span.set_output(processed)
        return processed
```

### Agents

Use `@tracer.agent` for autonomous AI agents:

#### Using Decorators

```python
@tracer.agent
def ai_agent(query: str) -> str:
    # Agent logic here
    response = f"Agent response to: {query}"
    return response
```

#### Using Context Managers

```python
def ai_agent_manual(query: str) -> str:
    with tracer.start_as_current_span("ai_agent", openinference_span_kind="agent") as span:
        span.set_input(query)
        response = f"Agent response to: {query}"
        span.set_output(response)
        return response
```

### Tools

Use `@tracer.tool` for function calls and tool usage:

#### Using Decorators

```python
@tracer.tool
def get_weather(city: str) -> str:
    # Simulate weather API call
    return f"Weather in {city}: Sunny, 75°F"


@tracer.tool
def search_database(query: str) -> list:
    # Simulate database search
    return [f"Result 1 for {query}", f"Result 2 for {query}"]
```

#### Using Context Managers

```python
def get_weather_manual(city: str) -> str:
    with tracer.start_as_current_span("get_weather", openinference_span_kind="tool") as span:
        span.set_input(city)
        weather = f"Weather in {city}: Sunny, 75°F"
        span.set_output(weather)
        return weather
```

### LLMs

Use `@tracer.llm` for language model interactions with custom input/output processing:

#### Using Decorators with Processing Functions

```python
from typing import Dict, List, Optional
from openai import OpenAI
from openai.types.chat import ChatCompletion, ChatCompletionMessageParam
from opentelemetry.util.types import AttributeValue

openai_client = OpenAI()


def process_input(
    messages: List[ChatCompletionMessageParam],
    model: str,
    temperature: Optional[float] = None,
    **kwargs,
) -> Dict[str, AttributeValue]:
    """Process input parameters for LLM span attributes."""
    return {
        "llm.input_messages": [
            {"role": msg["role"], "content": msg["content"]} for msg in messages
        ],
        "llm.model_name": model,
        "llm.temperature": temperature,
    }


def process_output(response: ChatCompletion) -> Dict[str, AttributeValue]:
    """Process LLM response for span attributes."""
    message = response.choices[0].message
    return {
        "llm.output_messages": [{"role": message.role, "content": message.content}],
        "llm.token_count.prompt": response.usage.prompt_tokens if response.usage else 0,
        "llm.token_count.completion": response.usage.completion_tokens if response.usage else 0,
    }


@tracer.llm(
    process_input=process_input,
    process_output=process_output,
)
def invoke_llm(
    messages: List[ChatCompletionMessageParam],
    model: str,
    temperature: Optional[float] = None,
) -> ChatCompletion:
    response = openai_client.chat.completions.create(
        messages=messages,
        model=model,
        temperature=temperature,
    )
    return response


# Use the decorated function
response = invoke_llm(
    messages=[{"role": "user", "content": "Hello, world!"}], model="gpt-4", temperature=0.7
)
```

#### Using Context Managers for LLMs

```python
from opentelemetry.trace import Status, StatusCode


def invoke_llm_manual(messages, model, temperature=None):
    with tracer.start_as_current_span("llm_call", openinference_span_kind="llm") as span:
        # Set input attributes
        span.set_attributes(
            {
                "llm.input_messages": [
                    {"role": msg["role"], "content": msg["content"]} for msg in messages
                ],
                "llm.model_name": model,
                "llm.temperature": temperature,
            }
        )

        try:
            response = openai_client.chat.completions.create(
                messages=messages,
                model=model,
                temperature=temperature,
            )

            # Set output attributes
            message = response.choices[0].message
            span.set_attributes(
                {
                    "llm.output_messages": [{"role": message.role, "content": message.content}],
                    "llm.token_count.prompt": response.usage.prompt_tokens if response.usage else 0,
                    "llm.token_count.completion": response.usage.completion_tokens
                    if response.usage
                    else 0,
                }
            )
            span.set_status(Status(StatusCode.OK))
            return response

        except Exception as error:
            span.record_exception(error)
            span.set_status(Status(StatusCode.ERROR))
            raise
```

### Retrievers and Embeddings

Use `@tracer.retriever` and `@tracer.embedding` for retrieval and embedding operations:

```python
@tracer.retriever
def search_documents(query: str, limit: int = 10) -> List[str]:
    # Simulate document retrieval
    return [f"Document {i} matching '{query}'" for i in range(limit)]


@tracer.embedding
def generate_embedding(text: str) -> List[float]:
    # Simulate embedding generation
    return [0.1, 0.2, 0.3, 0.4, 0.5]
```

### Additional Features

#### Suppress Tracing

Use the `suppress_tracing()` context manager to temporarily disable tracing:

```python
from phoenix.otel import suppress_tracing

with suppress_tracing():
    # This function call will not be traced
    result = my_function("input")
```

#### Adding Context Attributes

Use `using_attributes()` to add context attributes to spans:

```python
from phoenix.otel import using_attributes

with using_attributes(session_id="123", user_id="user456"):
    # All spans created within this context will have these attributes
    result = my_function("input")
```

For more detailed examples and advanced usage patterns, see the [Phoenix tracing documentation](https://arize.com/docs/phoenix/tracing/how-to-tracing/setup-tracing/instrument-python).

## API Reference

```{toctree}
:maxdepth: 2

api/register
api/provider
api/processors
api/exporters
api/settings
```

## External Links

- [Main Phoenix Documentation](https://arize.com/docs/phoenix)
- [Python Reference](https://arize-phoenix.readthedocs.io/)
- [GitHub Repository](https://github.com/Arize-ai/phoenix)
- [PyPI Package](https://pypi.org/project/arize-phoenix-otel/) 