Metadata-Version: 2.4
Name: unifyapps-otel
Version: 0.1.0
Summary: Lightweight OpenTelemetry wrapper with UnifyApps-aware defaults for tracing LLM/agent applications.
Author: UnifyApps
License: Apache-2.0
Project-URL: Homepage, https://unifyapps.com
Keywords: opentelemetry,otel,tracing,llm,observability,unifyapps
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: opentelemetry-api
Requires-Dist: opentelemetry-sdk
Requires-Dist: opentelemetry-exporter-otlp-proto-http
Provides-Extra: openinference
Requires-Dist: openinference-instrumentation; extra == "openinference"
Provides-Extra: dev
Requires-Dist: pytest<9,>=7.4; extra == "dev"
Dynamic: license-file

# unifyapps-otel

Send your LLM/agent traces to UnifyApps in **one call**. A thin, opinionated
wrapper around OpenTelemetry that points it at your UnifyApps ingestion
endpoint and stays out of your way.

## Installation

```bash
pip install unifyapps-otel
# plus whatever you want to trace, e.g.:
pip install openinference-instrumentation-langchain
```

## Quickstart

```python
from unifyapps.otel import register

tracer_provider = register(
    endpoint="https://<your-unifyapps-domain>/api-endpoint/agent-otel-exporter-otlp/<agent-id>",
    api_key="your-unifyapps-api-key",   # optional
    auto_instrument=True,               # instrument all installed OpenInference libraries
)
```

That's the whole configuration. Copy the `endpoint` from the UnifyApps
onboarding screen for your agent — it already identifies your agent, so there is
nothing else to wire up.

`register(...)` only sets up OpenTelemetry — it does **not** instrument your app
by itself. Use `auto_instrument=True`, or call a specific instrumentor:

```python
tracer_provider = register(endpoint="...")

from openinference.instrumentation.langchain import LangChainInstrumentor
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
```

## Configuration

`register()` takes only what a user genuinely needs:

| Argument | Required | Purpose |
|---|---|---|
| `endpoint` | yes* | Your UnifyApps OTLP URL (from onboarding). `/v1/traces` is appended if missing. |
| `api_key` | no | Auth Token, sent as the `authorization` header. Omit if your endpoint needs no auth. |
| `auto_instrument` | no | Instrument every installed OpenInference library. Default `False`. |
| `set_global_tracer_provider` | no | Register as the global OTel provider. Default `True`. |
| `headers` | no | Extra request headers (advanced — e.g. a non-default auth header). |
| `span_processors` | no | Processors run before export, to enrich/redact spans (advanced). |

\* `endpoint` and `api_key` fall back to environment variables:

| Variable | Maps to |
|---|---|
| `UNIFYAPPS_COLLECTOR_ENDPOINT` | `endpoint` |
| `UNIFYAPPS_API_KEY` | `api_key` |

```python
from unifyapps.otel import register
tracer_provider = register(auto_instrument=True)  # endpoint + key from env
```

## One trace = one export

This library exports **each trace as exactly one OTLP request.** When a turn's
root span ends, the complete trace — root and every child — is shipped in a
single request, on a background thread, so:

- the backend always receives a **whole trace** (a turn is never split across
  exports), and
- your user **never waits** on ingestion (the export happens off the request
  thread).

This is the only export model, and there is nothing to configure. Different
turns ship as separate exports; that is expected.

> Short-lived scripts: call `tracer_provider.shutdown()` (or `.force_flush()`)
> before the process exits so buffered traces are sent. Long-running servers can
> rely on the global provider's `atexit` flush.

## Sessions & users

Session and user identity are **not** computed by this package or derived from
the span tree. They are flat span attributes — `session.id` and `user.id` —
stamped on **every** span produced inside a context. This is the standard
OpenInference model for identity: the *application* supplies the id, and the
backend groups by it.

Wrap a conversation with `using_session(...)`; add `using_user(...)` to tag who
it belongs to:

```python
from unifyapps.otel import using_session, using_user

with using_user("user-42"), using_session(session_id="conversation-123"):
    agent.invoke(...)   # every span here carries user.id + session.id
```

Or set both at once with `using_attributes(...)`:

```python
from unifyapps.otel import using_attributes

with using_attributes(session_id="conversation-123", user_id="user-42"):
    agent.invoke(...)
```

Rules of thumb:

- Use **one stable `session.id` for the whole multi-turn conversation** — reuse
  it across turns. One `session.id` spans many traces (many turns).
- `session.id` only groups traces for observability. It does **not** give the
  model memory — feeding prior turns back to the LLM is your app's job.
- These helpers stamp spans created by OpenInference **instrumentors**. If you
  create spans **by hand** (e.g. wrapping a raw SDK call), set the attributes
  yourself: `span.set_attribute("session.id", session_id)`.

## Scope

Intentionally thin. It builds the `TracerProvider`, the OTLP HTTP exporter, and
the auth headers; guarantees one-export-per-trace; and (optionally) discovers
installed OpenInference instrumentors. Everything else — how spans are parsed and
how traces roll up into sessions — is an ingestion-side concern.
