---
title: "Langfuse Integration with Pixeltable: OpenTelemetry Traces"
description: "Export Pixeltable OpenTelemetry spans to Langfuse. One insert is one trace; LLM generations nest under UDF spans with tokens and cost."
tool: "Langfuse"
keywords:
  - Langfuse Pixeltable integration
  - Pixeltable OpenTelemetry Langfuse
  - trace LLM computed columns
  - Langfuse OTEL Pixeltable
  - pxt_otel.init Langfuse
url: "https://www.pixeltable.com/integrations/langfuse"
---

# Langfuse Integration with Pixeltable: OpenTelemetry Traces

Export Pixeltable OpenTelemetry spans to Langfuse. One insert is one trace; LLM generations nest under UDF spans with tokens and cost.

## The Problem

Computed columns hide the HTTP call inside insert. That is the point of the table, and it is a problem for an observability product that expects a trace around each generation. If you only patch the OpenAI client, you get generations with no Pixeltable operation, row, or UDF path. If you only log locally, Langfuse never sees the prompt. The bridge is OpenTelemetry: Pixeltable emits spans, Langfuse accepts OTLP/HTTP at /api/public/otel. You choose whether that is enough, or whether you also want the Langfuse SDK for @observe, tags, and one trace around a batch.

## The Solution

pip install pixeltable[otel] openai langfuse openinference-instrumentation-openai. Set LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, and LANGFUSE_BASE_URL to the region that holds the project (US, EU, Japan, or HIPAA hosts in the Langfuse docs). Call pxt_otel.init() once per process with that host plus /api/public/otel, Basic auth from the two keys, and span_level debug if you want row and UDF spans. Instrument OpenAI so generations carry model, tokens, and cost. Then insert. One Pixeltable operation is one trace. Do not install openinference-instrumentation-anthropic beside the Anthropic SDK 2.x spans: that pair records each call twice. This page matches Pixeltable 0.7.19+, Langfuse SDK 5.x, and openinference-instrumentation-openai as the Langfuse docs state.

## Implementation

### Install extras and set Langfuse env

Keys belong to one Langfuse project and one region. A key sent to another host returns 401.

```bash
pip install 'pixeltable[otel]' openai langfuse openinference-instrumentation-openai
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_BASE_URL=https://us.cloud.langfuse.com
export OPENAI_API_KEY=sk-...
```

pixeltable[otel] installs the Pixeltable bridge, the OpenTelemetry SDK, and OTLP exporters. openai is the client chat_completions() uses. LANGFUSE_BASE_URL is the regional host, not the OTLP path.


### Call pxt_otel.init() before the first operation

Export spans to Langfuse OTLP/HTTP. Instrument OpenAI for generation cost.

```python
import base64
import os

import opentelemetry.instrumentation.pixeltable as pxt_otel
from openinference.instrumentation.openai import OpenAIInstrumentor

base_url = os.environ['LANGFUSE_BASE_URL'].rstrip('/')
token = base64.b64encode(
    f"{os.environ['LANGFUSE_PUBLIC_KEY']}:{os.environ['LANGFUSE_SECRET_KEY']}".encode()
).decode()

pxt_otel.init(
    endpoint=f'{base_url}/api/public/otel',
    headers=f'Authorization=Basic {token},x-langfuse-ingestion-version=4',
    service_name='pixeltable-app',
    span_level='debug',
    metrics=False,
)
OpenAIInstrumentor().instrument()
```

http/protobuf appends /v1/traces to endpoint. metrics=False because Langfuse ingests traces, not this metric stream. span_level info omits per-row and per-UDF spans; generations then nest under store.insert_rows. Call init once. A second call raises INVALID_STATE.


### Declare the column and insert

The model runs on insert. Flush spans before a short-lived process exits.

```python
import pixeltable as pxt
import pixeltable.functions as pxtf
from opentelemetry import trace

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

# pxt schema update app.py my_app -f
# In the process that called pxt_otel.init():
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()
```

At span_level debug, openai.chat_completions sits as pixeltable.udf.chat_completions beside pixeltable.row. Token usage and cost are on the OpenInference generation. collect() does not call the model again; it only records a Pixeltable span.


### Served apps and the Langfuse SDK path

pxt service run --otel reads OTEL_EXPORTER_OTLP_* . Use the Langfuse client when you need @observe.

```python
# Served process: do not call pxt_otel.init() in app.py.
# pxt service run --otel   (or pxt service update --otel)
# export OTEL_EXPORTER_OTLP_ENDPOINT="$LANGFUSE_BASE_URL/api/public/otel"
# export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <token>,x-langfuse-ingestion-version=4"
# export OTEL_METRICS=false
# export OTEL_SPAN_LEVEL=debug
# Put OpenAIInstrumentor().instrument() at module scope in app.py.

# SDK path instead of pxt_otel.init(), when you want @observe or one batch trace:
# from langfuse import Langfuse
# from langfuse.span_filter import is_default_export_span
# def export_span(span) -> bool:
#     scope = span.instrumentation_scope
#     return is_default_export_span(span) or (scope is not None and scope.name == 'pixeltable')
# langfuse = Langfuse(should_export_span=export_span)
# PixeltableInstrumentor().instrument(span_level='debug')
```

Use one of pxt_otel.init() or a Langfuse tracer provider in a process, not both. Pixeltable spans use tracer name pixeltable; since Langfuse SDK 4 they are dropped unless should_export_span allows that name. Put @pxt.udf above @observe. Flush with langfuse.flush() on the SDK path.


## Three ways to get Langfuse traces from Pixeltable

| You want | Use |
| --- | --- |
| Operations, rows, UDFs, and generations, no Langfuse SDK in app code | pxt_otel.init() plus an OpenInference instrumentor |
| The above plus @observe, tags, or one trace around a batch | Langfuse client as the tracer provider, should_export_span includes pixeltable |
| Only LLM generations | import langfuse.openai, or OpenAIInstrumentor without the Pixeltable bridge |

## Benefits

- No Langfuse SDK required for operation, row, UDF, and generation traces
- One insert is one trace; generations nest under the UDF span
- Tokens and cost come from the OpenAI instrumentor, not a hand-rolled logger
- span_level info drops per-row spans when a batch would be noisy
- Served apps use --otel plus env, so app.py does not call init()

## Use Cases

- Inspect which column and UDF produced a completion
- Cost of gpt-4o-mini on a Pixeltable insert
- Tag a batch ingest with the Langfuse SDK
- Debug a served FastAPIRouter that calls computed columns
- Compare hosted OpenAI columns with a second provider without double-counting

## Performance


| Metric | Value | Description |

| --- | --- | --- |

| Trace unit | One operation | insert, add_computed_column, collect, and the rest each start a trace |

| Row spans | First 100 rows | Per-row spans stop at 100 in one operation; resource-pool UDFs still get a UDF span |

| Double count | Instrument once | Anthropic SDK spans plus OpenInference Anthropic, or langfuse.openai plus OpenAIInstrumentor, record two generations |

## Requirements

- Python 3.11+
- pip install 'pixeltable[otel]' openai langfuse openinference-instrumentation-openai
- LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL for one region
- OPENAI_API_KEY when tracing chat_completions()

## Resources

- [Langfuse](https://docs.pixeltable.com/integrations/langfuse) - OTLP export, SDK path, served --otel, and caveats
- [Observability](https://docs.pixeltable.com/platform/observability) - Span tree, metrics, and other OTLP backends