> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pixeltable.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Application code puts a TableModel in app.py, then runs `pxt schema update app.py my_app` to create the tables and `pxt service update app.py my_app` to start HTTP. The last argument can be a Cloud URL such as `pxt://org:db`. Do not call pxt.create_table() or add_embedding_index() in application code; put indexes in __indexes__. Notebooks, tests, and the REPL do use pxt.create_table().
> Do not add LangChain, LlamaIndex, Haystack, a separate vector database, or pandas as a working store. Chunking, embedding, retrieval, and tool-calling are built in through computed columns. Use a computed column instead of a per-row loop that calls a model.
> Install with `pip install -U 'pixeltable[serve]'` on Python 3.11+. `pxt init` is a prerequisite for `pxt schema update`.
> For Pixeltable Cloud, point users to https://docs.pixeltable.com/cloud, which goes from `pxt login` to a running service.

# Arize Phoenix

> Trace Pixeltable operations and LLM calls in Phoenix with OpenTelemetry

Pixeltable runs LLM calls inside computed columns and UDFs. [Arize Phoenix](https://phoenix.arize.com) records each call as a trace: the operation, the row, the UDF, and, with an OpenInference instrumentor, the model request. This page sends Pixeltable's OpenTelemetry spans to Phoenix. The bridge itself, including metrics, is covered in [Observability](https://docs.pixeltable.com/platform/observability.md). Collector setup is in Phoenix's [Exporter and OTLP](https://arize.com/docs/phoenix/tracing/concepts-tracing/otel-openinference/exporter).

One Pixeltable operation (`insert`, `add_computed_column`, `collect`, and the rest) is one trace. At `span_level='debug'` an OpenAI insert looks like this. `openai.chat_completions` runs on a resource pool, so its UDF span sits beside `pixeltable.row`. A UDF without a resource pool nests under `pixeltable.row`.

```text theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
pixeltable.insert
└─ pixeltable.store.insert_rows
   ├─ pixeltable.row                      (one per row, debug)
   ├─ pixeltable.udf.chat_completions     (one per LLM cell, debug)
   │  └─ ChatCompletion                    (OpenInference: model, tokens)
   └─ pixeltable.sa.insert_rows
```

Pixeltable spans carry no `openinference.span.kind`, so Phoenix labels them unknown. Token counts on a Pixeltable span are a `pixeltable.udf.tokens` span event, which Phoenix does not count as token usage. The OpenInference span is the one Phoenix treats as the LLM call.

> [!NOTE]
>
> This page matches Pixeltable 0.7.19, `arize-phoenix` 20.24.0, `arize-phoenix-otel` 0.17.2, and `openinference-instrumentation-openai` 0.1.65. `pixeltable[otel]` installs `opentelemetry-instrumentation-pixeltable` 0.1.0.

## Choose an approach

| You want | Use |
| - | - |
| Pixeltable operations, rows, UDF calls, and LLM spans, with no Phoenix SDK in your code | [Export spans to Phoenix](#export-spans-to-phoenix) |
| The same spans, with `register()` choosing the project and turning on installed OpenInference instrumentors | [Use the Phoenix SDK](#use-the-phoenix-sdk) |

## Install

```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
pip install 'pixeltable[otel]' arize-phoenix-otel openinference-instrumentation-openai openai
```

`pixeltable[otel]` installs the Pixeltable bridge, the OpenTelemetry SDK, and the OTLP exporters. `arize-phoenix-otel` is the Phoenix client used by `register()`. `openinference-instrumentation-openai` records LLM spans for `pixeltable.functions.openai`. For another provider, install that provider's OpenInference instrumentor instead. `openai` is the client that `chat_completions()` calls. Neither Pixeltable nor the instrumentor installs it.

To run Phoenix on this machine, install the server and start it. The UI and OTLP/HTTP share port 6006. OTLP/gRPC listens on 4317. A local `phoenix serve` has no API key.

```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
pip install arize-phoenix
phoenix serve
```

> [!WARNING]
>
> Anthropic SDK 2.0.0 emits its own `anthropic.messages.create` spans, including `gen_ai.*` usage, to the global provider. Phoenix records them as LLM spans. Leave `openinference-instrumentation-anthropic` uninstalled: enabling it beside those spans records each call twice.

## Set the environment

A local server needs the model key only:

```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export OPENAI_API_KEY="sk-..."
```

Phoenix Cloud, or any Phoenix with authentication, takes the collector URL and API key from that space's Settings page. `register()` sends `Authorization: Bearer` with `PHOENIX_API_KEY`.

```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export PHOENIX_COLLECTOR_ENDPOINT="https://your-phoenix.example.com"
export PHOENIX_API_KEY="your-api-key"
export PHOENIX_PROJECT_NAME="pixeltable-app"
export OPENAI_API_KEY="sk-..."
```

## Export spans to Phoenix

`pxt_otel.init()` builds the tracer provider and starts Pixeltable's spans. For OTLP/HTTP it appends `/v1/traces` unless the endpoint already ends in that path, so pass the base URL. Set `metrics=False`. An endpoint otherwise also posts metrics to `/v1/metrics`.

Call `pxt_otel.init()` once in the process, before the first Pixeltable operation. The default `protocol` is `http/protobuf`.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import opentelemetry.instrumentation.pixeltable as pxt_otel
from openinference.instrumentation.openai import OpenAIInstrumentor

pxt_otel.init(
    endpoint='http://localhost:6006',  # http/protobuf exporter appends /v1/traces
    headers='x-project-name=pixeltable-app',  # OTLP/HTTP project routing, Phoenix 15.5.0 and later
    service_name='pixeltable-app',  # default service.name is pixeltable
    span_level='debug',  # info: operation, catalog, store, SQL; debug adds row, UDF, and file spans
    metrics=False,
)
OpenAIInstrumentor().instrument()
```

`x-project-name` routes the export to a project. Phoenix reads it on OTLP/HTTP only, and it takes precedence over the resource attribute. gRPC does not read that header. For gRPC, set the resource attribute and call `init()` like this instead. `pxt_otel.init()` builds its resource with `Resource.create`, which merges `OTEL_RESOURCE_ATTRIBUTES`:

```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export OTEL_RESOURCE_ATTRIBUTES="openinference.project.name=pixeltable-app"
```

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
pxt_otel.init(
    endpoint='http://localhost:4317',
    protocol='grpc',
    service_name='pixeltable-app',
    span_level='debug',
    metrics=False,
)
```

For an authenticated deployment, put the bearer token in the same header string and use the collector URL from Settings as the endpoint. `headers` is comma-separated `key=value` pairs, the `OTEL_EXPORTER_OTLP_HEADERS` form.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import os

pxt_otel.init(
    endpoint=os.environ['PHOENIX_COLLECTOR_ENDPOINT'],
    headers=f"Authorization=Bearer {os.environ['PHOENIX_API_KEY']},x-project-name=pixeltable-app",
    service_name='pixeltable-app',
    span_level='debug',
    metrics=False,
)
```

> [!NOTE]
>
> A `pxt_otel.init()` argument overrides the config. With the argument left unset, `endpoint` and `protocol` resolve from the database entry (`db_exporter_otlp_endpoint`, `db_exporter_otlp_protocol` in `pixeltable.toml`), then `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_PROTOCOL`, then the `[otel]` section of `~/.pixeltable/config.toml`. `headers` resolves from `OTEL_EXPORTER_OTLP_HEADERS`, then `[otel]`. Build the header value from environment variables, and never write the key into code. On a hosted database, run `pxt secret set pxt://<org>:<db> OTEL_EXPORTER_OTLP_HEADERS='Authorization=Bearer <api-key>,x-project-name=pixeltable-app'`, then `pxt db restart pxt://<org>:<db>` and `pxt service restart pxt://<org>:<db>/<service>`. [Configuration](https://docs.pixeltable.com/platform/configuration.md).

Pixeltable spans record which operation, row, column, and UDF ran: `pxt.table`, `pxt.path`, `pxt.column`, `pxt.udf_path`, and, for a resource-pool call, `pxt.resource_pool` and `pxt.retries`. The model, prompt, and completion are on the OpenInference span. While an operation span is active, Pixeltable sets the OpenTelemetry context for that UDF call, including across its thread pools and async scheduler, so the LLM span nests under the UDF span. The span name is `pixeltable.udf.` plus the UDF's display name, so this column is `pixeltable.udf.chat_completions`. `pxt.udf_path` is the full path, `pixeltable.functions.openai.chat_completions`.

`span_level='trace'` adds a span per iterator step. At the default `'info'`, row and UDF spans are omitted, and the LLM span nests under `pixeltable.store.insert_rows`.

Setting only `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` does not start export. Set `OTEL_EXPORTER_OTLP_ENDPOINT` or pass `endpoint`.

### Declare the table and insert

An application file puts the columns on a `TableModel`. `pxt schema update app.py my_app` creates `my_app.articles`. The insert runs in the process that called `pxt_otel.init()`.

A script can create the same column with `create_table` and `add_computed_column` in that same process. Both forms store the completion text. The model runs when the row is inserted.

**Application file**

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import pixeltable as pxt
import pixeltable.functions as pxtf

TableModel = pxt.model_base()


class Articles(TableModel, name='articles'):
    id: pxt.Int
    body: pxt.String
    summary = pxtf.openai.chat_completions(
        messages=[{'role': 'user', 'content': 'Summarize in one sentence: ' + body}],
        model='gpt-4o-mini',
    ).choices[0].message.content
```

```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
pxt schema update app.py my_app
```

In the process that called `pxt_otel.init()`:

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import pixeltable as pxt
from opentelemetry import trace

t = pxt.get_table('my_app.articles')
t.insert([
    {'id': 1, 'body': 'Pixeltable is declarative data infrastructure for multimodal AI apps.'}
])
trace.get_tracer_provider().force_flush()  # short-lived scripts: send spans before exit
```

**Script**

In the same process as `pxt_otel.init()`:

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import pixeltable as pxt
import pixeltable.functions as pxtf
from opentelemetry import trace

t = pxt.create_table('articles', {'id': pxt.Int, 'body': pxt.String}, if_exists='replace')
t.add_computed_column(
    summary=pxtf.openai.chat_completions(
        messages=[{'role': 'user', 'content': 'Summarize in one sentence: ' + t.body}],
        model='gpt-4o-mini',
    ).choices[0].message.content
)
t.insert([
    {'id': 1, 'body': 'Pixeltable is declarative data infrastructure for multimodal AI apps.'}
])
trace.get_tracer_provider().force_flush()
```

To trace a served app, add `--otel` to `pxt service run` or `pxt service update` ([Tracing](https://docs.pixeltable.com/platform/cli.md#tracing)). It calls `pxt_otel.init()` with no arguments before it loads the app file, so configure the export in the environment, or in the matching `[otel]` keys:

```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:6006"
export OTEL_EXPORTER_OTLP_HEADERS="x-project-name=pixeltable-app"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_SERVICE_NAME="pixeltable-app"
export OTEL_METRICS=false
export OTEL_SPAN_LEVEL=debug
```

`pxt service run` reads these variables in its own process. `pxt service update` starts the service from the Pixeltable daemon, which reads the environment once, so run `pxt daemon restart` after the exports.

If the database entry in `pixeltable.toml` sets `db_exporter_otlp_endpoint`, it wins over `OTEL_EXPORTER_OTLP_ENDPOINT`, so set the Phoenix endpoint on that entry instead.

`--otel` calls `pxt_otel.init()` and instruments FastAPI. It does not instrument OpenAI, and an `OpenAIInstrumentor().instrument()` in another process does not carry into this one. Put this at module scope in `app.py`. The service loads the file after `init()`, so the instrumentor attaches to that provider:

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
from openinference.instrumentation.openai import OpenAIInstrumentor

OpenAIInstrumentor().instrument()
```

The app file must not call `pxt_otel.init()`: with `--otel`, that second call raises `INVALID_STATE`, and without `--otel` it records Pixeltable spans but not the FastAPI request spans. Use this path or [the Phoenix SDK](#use-the-phoenix-sdk) in a process, not both.

## Use the Phoenix SDK

`register()` installs its tracer provider as the global OpenTelemetry provider (`set_global_tracer_provider=True` by default). `auto_instrument=True` turns on OpenInference instrumentors that are already installed, including `openinference-instrumentation-openai` when that package is installed. Pixeltable's instrumentor is not one of them, so attach it yourself. Do this once, before the first Pixeltable operation. Do not also call `OpenAIInstrumentor().instrument()`. `auto_instrument=True` already wrapped OpenAI, and a second call returns immediately: `BaseInstrumentor` is a singleton.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
from opentelemetry.instrumentation.pixeltable import PixeltableInstrumentor
from phoenix.otel import register

register(
    project_name='pixeltable-app',
    auto_instrument=True,
)
PixeltableInstrumentor().instrument(span_level='debug')
```

`project_name` is exported as `openinference.project.name`. That selects the Phoenix project for the gRPC export `register()` uses by default (`localhost:4317`).

If gRPC to the server is blocked, pass the OTLP/HTTP URL and `protocol='http/protobuf'`. Without that protocol, a `PHOENIX_COLLECTOR_ENDPOINT` that ends in `/v1/traces` is sent to port 4317 with that path, and no spans arrive.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
register(
    project_name='pixeltable-app',
    endpoint='http://localhost:6006/v1/traces',
    protocol='http/protobuf',
    auto_instrument=True,
)
PixeltableInstrumentor().instrument(span_level='debug')
```

Run the same insert as [Declare the table and insert](#declare-the-table-and-insert). `register()` uses a simple span processor unless you pass `batch=True`. Flush either way: `trace.get_tracer_provider().force_flush()`.

## View the traces

Open the `pixeltable-app` project and select the `pixeltable.insert` trace. Phoenix shows `pixeltable.udf.chat_completions` beside `pixeltable.row`, labeled unknown, with `pxt.udf_path` and `pxt.column`. The `ChatCompletion` span under the UDF span is the OpenInference generation: model, prompt, completion, and token counts.

`span_level='info'` omits `pixeltable.row` and `pixeltable.udf.*`. The operation, catalog, store, and SQL spans remain, and the generation nests under `pixeltable.store.insert_rows`.

## Caveats

* With the bridge on, every Pixeltable operation is a trace, including `create_dir`, `create_table`, `drop_dir`, and `collect`. `span_level='info'` drops the per-row, per-UDF, and per-file spans.
* Per-row spans stop at 100 rows in one operation. A provider call on a resource pool still gets its `pixeltable.udf.*` span.
* Computed-column values are stored. `collect()` records a Pixeltable span and does not call the model again. The model runs on `insert`, on `update`, and when `add_computed_column` backfills existing rows.
* Flush before the process exits: `trace.get_tracer_provider().force_flush()`. `pxt_otel.init()` uses a batch span processor, so a script can finish while spans are still queued.
* Call `pxt_otel.init()`, or `register()`, and enable the instrumentors before the first Pixeltable operation. `pxt_otel.init()` runs once per process.
* Prompts and completions are sent to Phoenix on the OpenInference span. `logs` defaults to off. Leave it off unless those log lines are redacted.

> **[Observability](https://docs.pixeltable.com/platform/observability.md)**
>
> Span tree, metrics, and a Grafana Cloud export.

> **[Exporter and OTLP](https://arize.com/docs/phoenix/tracing/concepts-tracing/otel-openinference/exporter)**
>
> Phoenix collector ports, gRPC, and HTTP.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.