Skip to main content

observix

Provider-agnostic observability for Python and AI applications. Instrument once. Send the same telemetry to Phoenix, Langfuse, MLflow, Datadog, Arize and any OTLP backend — simultaneously — and change destinations without touching application code.

from observix import observe, configure

configure(service_name="my-app", exporters=["phoenix", "langfuse"])


@observe
def my_function(): ...

That is the whole integration. Adding a backend, removing one, or stripping prompts before they reach a third party are all configuration changes.


Why this exists

Every LLM observability backend now ingests OTLP, so transport is solved. What is not solved is that each backend reads a different attribute vocabulary and renders poorly when handed another's:

Backend Reads Wants
Phoenix / Arize OpenInference openinference.span.kind, input.value, llm.token_count.prompt
Langfuse langfuse.* (highest precedence) langfuse.observation.input, usage_details
MLflow mlflow.* mlflow.spanInputs, mlflow.llm.model
Datadog / Grafana / Honeycomb OTel GenAI gen_ai.*

Emit one vocabulary and you get a first-class experience in exactly one backend. Langfuse #12657 is this problem in miniature: spans using GenAI semconv v1.37+ arrive with null input/output.

Meanwhile the OTel GenAI conventions are still Development-status — they moved to a separate repository in June 2026 — so hard-coding today's names into your application inherits every future rename.

observix records a canonical model once and translates it per-destination at export time. One @observe produces OpenInference for Phoenix, langfuse.* for Langfuse, and gen_ai.* for Datadog, all natively shaped, all from the same recording.

What it is not

It does not reimplement OpenTelemetry. Transport, batching, retries, context propagation and sampling are OTel's. It is not another auto-instrumentation library — OpenLLMetry and OpenInference do that well, and observix adopts their spans rather than competing. It is not a backend; there is no server and no UI.


Install

pip install observix

With the backends you need:

pip install 'observix[all]'

The developer experience

Decorate anything

from observix import observe


@observe
def handle(request): ...


@observe(kind="agent", name="planner")
async def plan(goal: str): ...  # coroutines, generators, async generators


@observe(kind="retriever", capture_output=False)
def search(query: str): ...

Record AI-specific detail

from observix import get_current_span

span = get_current_span()
span.record_llm_call(
    provider="anthropic",
    request_model="claude-opus-4",
    input_messages=[{"role": "user", "content": "Hello"}],
    output_messages=[{"role": "assistant", "content": "Hi"}],
    input_tokens=12,
    output_tokens=4,
)

Cost in USD is computed automatically from a built-in, overridable price book.

Stream without losing the metrics that matter

When a model streams, the decorated function returns before anything is generated — so a plain decorator records an empty output and misses time-to-first-token entirely.

from observix import observe, observe_stream


@observe(kind="chat")
def chat(prompt: str):
    stream = client.messages.create(..., stream=True)
    return observe_stream(stream, provider="anthropic", request_model="claude-opus-4")

Chunks pass through untouched. TTFT, the accumulated response and the chunk count land on the span when the stream ends — including when it's abandoned part-way. observe_astream for async for.

Instrument a block

from observix import observe_block

with observe_block("retrieval", kind="retriever") as span:
    docs = vector_db.search(query, k=5)
    span.set_retrieval(query=query, documents=docs, top_k=5)

Switching backends is configuration

# observix.toml
[observix]
service_name = "my-app"
exporters = ["phoenix", "langfuse"]

[observix.exporters.langfuse]
sample_ratio = 0.25
redact = "hashed"

Or environment variables, for the same result with no file:

export OBSERVIX_EXPORTERS=phoenix,langfuse
export OBSERVIX_LANGFUSE_SAMPLE_RATIO=0.25

Or in code:

configure(
    service_name="my-app",
    exporters=[
        {"provider": "phoenix"},
        {"provider": "langfuse", "sample_ratio": 0.25},
        {"provider": "datadog", "redact": "none"},  # no prompts leave the building
    ],
)

Precedence, lowest to highest: defaults → config file → environment → code.


Per-destination privacy

Privacy is a property of the destination, not of the span. The same recorded prompt can go in full to your self-hosted Langfuse, hashed to staging, and not at all to a third-party SaaS:

configure(
    exporters=[
        {"provider": "langfuse"},  # full content
        {"provider": "phoenix", "redact": "hashed"},  # joinable, unreadable
        {"provider": "datadog", "redact": "none"},  # metrics only
    ]
)

When no destination retains content, observix skips prompt serialisation entirely — you pay nothing for data you were never going to send.


Built-in providers and dialects

Providersconsole, memory, otlp, phoenix, langfuse, mlflow, arize, datadog Dialectspassthrough, otel_genai, openinference, langfuse, mlflow

Add your own without forking. Declare an entry point and it becomes available by name:

[project.entry-points."observix.providers"]
mybackend = "my_pkg:MyProvider"

See docs/extending.md.


Testing

Spans reach the collector after redaction and translation, so tests assert on exactly what a backend would have received:

from observix.testing import collect_spans


def test_renders_in_phoenix():
    with collect_spans(dialect="openinference") as spans:
        my_function()
    assert spans.one().attributes["openinference.span.kind"] == "LLM"

And verified against a real backend. In-memory tests prove observix emits what it intended to; they cannot prove the backend understands it. tests/live/ sends real spans to a running Phoenix and asserts on its typed columns, which only populate for attributes Phoenix actually recognises. That suite caught a real bug — project routing needs a resource attribute, not the header we had been sending.

phoenix serve                       # separate terminal
pytest tests/live -m live

Overhead

Measured, not asserted — full results and caveats in benchmarks/.

ns/call
Undecorated baseline ~200
@observe, unconfigured ~585
Raw OpenTelemetry span ~56,000
@observe, enabled ~85,400

Unconfigured observix costs well under a microsecond. When enabled, roughly two-thirds of a span's cost is the OpenTelemetry SDK underneath it. Fan-out adds ~22 µs per extra destination on the calling thread — translation and network I/O happen on each destination's own worker. When no destination retains content, prompt serialisation is skipped entirely, cutting ~32% off a full LLM span.


Documentation

Licence

Apache-2.0. Contributions welcome — see CONTRIBUTING.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

observix-0.1.0.tar.gz (133.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

observix-0.1.0-py3-none-any.whl (94.8 kB view details)

Uploaded Python 3

File details

Details for the file observix-0.1.0.tar.gz.

File metadata

  • Download URL: observix-0.1.0.tar.gz
  • Upload date:
  • Size: 133.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for observix-0.1.0.tar.gz
Algorithm Hash digest
SHA256 207292027b8c4008b4789eb1d29bfa6367fd25b927188f56f5651a7e6d3f004d
MD5 df7a5ad598f573e9a1d4f6bd53fe737c
BLAKE2b-256 c0f277387a1e88789facb8da90fa6fb9c02540fb6ed6b2d52f54561508d76ed4

See more details on using hashes here.

Provenance

The following attestation bundles were made for observix-0.1.0.tar.gz:

Publisher: release.yml on Pranav-PA/observix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file observix-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: observix-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 94.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for observix-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d3f4180730610b28981457c91f790b95605e2394c4aede48ce476aac3728f948
MD5 3d82f0f2632e0340a73ec62d224109cc
BLAKE2b-256 c97bbf0310a3eb12b620a6bc6917b279a956daa4842849f562ba2ca4772fefe9

See more details on using hashes here.

Provenance

The following attestation bundles were made for observix-0.1.0-py3-none-any.whl:

Publisher: release.yml on Pranav-PA/observix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page