Metadata-Version: 2.1
Name: acceldata-aio-tracer
Version: 1.0.0.dev1
Summary: Acceldata AIO instrumentation SDK — OpenTelemetry-native LLM application telemetry
License: Apache-2.0
Author: Acceldata
Requires-Python: >=3.10,<4.0
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Provides-Extra: http
Requires-Dist: opentelemetry-api (>=1.44.0,<2.0.0)
Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.44.0,<2.0.0)
Requires-Dist: opentelemetry-instrumentation (>=0.65b0,<1.0.0)
Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.65b0,<1.0.0) ; extra == "http"
Requires-Dist: opentelemetry-instrumentation-httpx (>=0.65b0,<1.0.0) ; extra == "http"
Requires-Dist: opentelemetry-sdk (>=1.44.0,<2.0.0)
Requires-Dist: opentelemetry-semantic-conventions (>=0.65b0,<1.0.0)
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
Requires-Dist: requests (>=2.31.0,<3.0.0)
Requires-Dist: wrapt (>=1.16.0,<2.0.0)
Description-Content-Type: text/markdown

# acceldata-aio-tracer

The Acceldata AIO instrumentation SDK. OpenTelemetry-native: it does not replace
your instrumentation, it completes it.

```sh
pip install acceldata-aio-tracer
```

```python
import acceldata_aio_tracer as aio

aio.init(
    service="my-app",
    tenant_id="...",
    project_id="...",
    endpoint="https://collector.example.com",
    access_key="...",
    secret_key="...",
)

with aio.root_span(conversation_id=conversation_id, input_preview=user_message):
    ...   # model calls, tools, MCP, outbound HTTP
```

## What it does

**Gives a turn a root.** Chat transports like socket.io have no server span, so
without a root the model call, each tool call, each MCP handshake and every
outbound HTTP request start their own disconnected trace. `root_span()` opens
one span they all nest under, so a user's question is one trace.

**Puts the conversation id on every span.** Instrumentation libraries tag the
spans they own and leave the rest bare. Measured on one application, 2 of 8
instrumentation scopes carried the id, and the untagged spans held over a
million tokens — so per-conversation totals were wrong by 15%. This SDK holds
the id for the duration of the turn and stamps it on every span started inside,
whichever library produced it. An id a library set itself is never overwritten.

**Carries the user's message even when the turn fails.** The preview is set when
the turn opens, not when it completes, so a request that dies before the first
model call still shows what was asked.

## Scope

`root_span()` marks a unit of work the application knows the boundaries of
and instrumentation cannot infer. A chat turn is one. So is a workflow run:

```python
with aio.root_span(conversation_id=f"{instance_id}:{session_id}", name="workflow_run"):
    ...
```

`name` becomes a low-cardinality column — keep it a fixed label, never an
interpolated id.

## Instrumentation

Instrumentation for models, agents and MCP ships inside the package: `init()`
activates whichever of those libraries is installed in your environment and
skips the rest. The FastAPI and httpx layers are the one optional extra, and
the SDK works with none of them — you still get the root span and identity on
whatever your application emits itself.

```sh
pip install 'acceldata-aio-tracer[http]'
```

Call `init()` before constructing HTTP or model clients: instrumentation patches
at import and wraps at construction, so clients built earlier are not covered.

If your application already configures OpenTelemetry, the SDK adds itself to the
existing provider rather than replacing it, and warns if that provider's
resource carries no tenant.

## Stability

`service`, `tenant_id`, `project_id` and `endpoint` will not be renamed. The
keyword-only parameters may evolve across releases — always pass them by name.

Telemetry never breaks the application: every failure path is swallowed, and
`init()` returns `False` rather than raising when it cannot start.

