Skip to main content
Pixeltable runs LLM calls inside computed columns and UDFs. Arize Phoenix 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. Collector setup is in Phoenix’s Exporter and OTLP. 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.
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.
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

Install

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

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.
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:
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.
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.
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.
In the process that called pxt_otel.init():
To trace a served app, add --otel to pxt service run or pxt service update (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:
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:
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 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.
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.
Run the same insert as 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

Span tree, metrics, and a Grafana Cloud export.

Exporter and OTLP

Phoenix collector ports, gRPC, and HTTP.
Last modified on October 26, 2026