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.
This page matches Pixeltable 0.7.19, Langfuse SDK 5.0.4, and
openinference-instrumentation-openai 0.1.65. pixeltable[otel] installs opentelemetry-instrumentation-pixeltable.Choose an approach
Install
pixeltable[otel] installs the Pixeltable bridge, the OpenTelemetry SDK, and the OTLP exporters. openai is the client that chat_completions() calls; neither Pixeltable nor the instrumentor installs it. Exporting spans uses the bridge and the OpenAI instrumentor. The Langfuse SDK path adds the langfuse package. Tracing LLM calls only needs langfuse and, optionally, the OpenAI instrumentor.
Set the environment
LANGFUSE_BASE_URL. LANGFUSE_HOST still works and is deprecated.
Export spans to Langfuse
pxt_otel.init() builds the tracer provider and starts Pixeltable’s spans. Langfuse accepts OTLP/HTTP at LANGFUSE_BASE_URL + /api/public/otel, with HTTP Basic auth of public_key:secret_key.
Call pxt_otel.init() once in the process, before the first Pixeltable operation. The default protocol is http/protobuf, which is what /api/public/otel expects. With that protocol the exporter appends /v1/traces to endpoint. If the config sets grpc, pass protocol='http/protobuf'.
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 Authorization value from environment variables, as the example does, and never write the keys into code. On a hosted database, run pxt secret set pxt://<org>:<db> OTEL_EXPORTER_OTLP_HEADERS='Authorization=Basic <token>', then pxt service restart. Configuration.pxt.table, pxt.path, pxt.column, pxt.udf_path, and, for a resource-pool call, pxt.resource_pool and pxt.retries. Token usage and cost are on the generation, which the OpenAI instrumentor adds. 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 generation 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 generation nests under pixeltable.store.insert_rows.
Other OpenInference or OpenLLMetry instrumentors that attach to the global provider follow the same pattern.
Declare the table and insert
An application file puts the columns on aTableModel. 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
- Script
pxt_otel.init():--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 Langfuse endpoint on that entry instead.
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 the Langfuse SDK
Use the Langfuse client as the tracer provider when you also want@observe, propagate_attributes (trace name, tags, metadata, user or session ids), or one trace around a batch.
Create the client first and pass should_export_span. Since SDK 4, Langfuse exports Langfuse spans, gen_ai.* spans, and known LLM-instrumentor spans. Pixeltable’s tracer name is pixeltable, so those spans are dropped until the filter allows them.
Call PixeltableInstrumentor().instrument(), which attaches to the global provider the client installed. Export spans to Langfuse instead calls pxt_otel.init() to build a provider. Use one of the two in a process.
Trace your own UDFs
Put@pxt.udf above @observe. A .py file run as a script cannot define a UDF in __main__ (RequestError: Defining the UDF ... directly in the global namespace of a Python script is not allowed). Put the functions in a module and import them.
pxt schema update app.py my_app creates my_app.docs. In a notebook, the same assignments are t.add_computed_column(words=word_count(t.body)) and t.add_computed_column(title=headline(t.body, t.id)).
Sync and async UDFs both record. The @observe span nests under the pixeltable.udf. span, and an LLM call made inside it nests under the @observe span.
One trace for a batch
Each Pixeltable operation starts its own trace. Wrap the call to choose the name and attach metadata.propagate_attributes metadata values are strings of 200 characters or fewer.
pixeltable.insert, its row and UDF spans, the @observe spans, and every generation land in that trace. Each carries table in its metadata. The same with wraps add_computed_column when that call backfills rows already in the table.
Pixeltable’s own spans do not include row ids or cell values. To attach an id, pass the column into a UDF and call propagate_attributes() inside it, as headline() does.
Trace LLM calls only
Skip the Pixeltable bridge when the generations are enough.import langfuse.openaibefore the first Pixeltable operation. The import patchesopenai, including theAsyncOpenAIclient thatpixeltable.functions.openaiuses. Each computed-column call becomes anOpenAI-generationwith tokens and cost.- Or create a Langfuse client and call
OpenAIInstrumentor().instrument().
View the traces
Open the project in Langfuse and go to Tracing. Filter by trace name (ingest-docs), tags, or metadata (table). A trace shows the span tree, the prompt and completion of each generation, and token and cost totals.
Caveats
- With the bridge on, every Pixeltable operation is a trace, including
create_dir,create_table,drop_dir, andcollect.span_level='info'drops the per-row, per-UDF, and per-file spans. On an insert, the generation then nests underpixeltable.store.insert_rows. - 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 oninsert, onupdate, and whenadd_computed_columnbackfills existing rows. - Flush before the process exits:
trace.get_tracer_provider().force_flush()afterpxt_otel.init(),langfuse.flush()with the Langfuse client. - Call
pxt_otel.init(), or create the Langfuse client, and enable the instrumentors before the first Pixeltable operation.pxt_otel.init()runs once per process. - Instrument each provider once. The Anthropic SDK’s spans plus OpenInference Anthropic, or
langfuse.openaiplusOpenAIInstrumentor, record two generations and count the cost twice. - Prompts and completions are sent to Langfuse.
logsdefaults to off. Leave it off unless those log lines are redacted.
Observability
Span tree, metrics, and a Grafana Cloud export.
Configuration
OTEL_EXPORTER_OTLP_* and the [otel] section.