Metadata-Version: 2.4
Name: acceldata-aio-tracer
Version: 1.0.0.dev2
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
Classifier: Programming Language :: Python :: 3.14
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.

## Installation

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

Requires Python 3.10 or newer. FastAPI and httpx instrumentation ship as an
optional extra:

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

## Quickstart

```python
import acceldata_aio_tracer as aio

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

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

That is the whole integration. `init()` runs once at startup, before you build
any client; `root_span()` wraps each unit of work.

`tenant_id` must be nonblank. It is a claim rather than proof: the Collector
checks it against the identity its gateway authenticated, and overwrites it
from there.

## Endpoints

`endpoint` is the AIO server. Telemetry goes to that same host unless you say
otherwise, so a second endpoint is only needed when the Collector lives
somewhere else:

```python
aio.init(
    ...,
    endpoint="https://aio.example.com",
    aio_collector_endpoint="https://collector.example.com",
)
```

Both fall back to the environment when the argument is omitted —
`AIO_ENDPOINT` and `AIO_COLLECTOR_ENDPOINT` — and the standard
`OTEL_EXPORTER_OTLP_ENDPOINT` is honoured for the Collector as well.

## Selecting a project

Pass exactly one selector. When the UUID is known, `project_id` is used
unchanged and the SDK makes no request to the server.

Otherwise pass the name and let the SDK resolve it:

```python
aio.init(
    service="my-app",
    tenant_id="...",
    project_name="customer-facing-agent",
    endpoint="https://aio.example.com",
    access_key="...",
    secret_key="...",
)
```

The name is matched exactly, and a project that does not exist is created with
only `{"name": project_name}`. Two processes racing to create the same name is
safe: the loser adopts the winner's project. Whether a project is one you may
write to — its status, its type — is the server's decision, so the SDK does not
second-guess it.

When a name cannot be resolved, whether from a failed lookup, a refused create,
or a malformed or ambiguous response, `init()` returns `False` and the
application runs without telemetry.

## 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 for itself does not survive
that: the open conversation is the one that reaches ingest.

**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.

## What to wrap

`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"):
    ...
```

Call it once per unit of work, not once per span — it is not a replacement for
`tracer.start_as_current_span()`. `name` becomes a low-cardinality column, so
keep it a fixed label and never interpolate an id into it.

Pass `user_id` when a turn belongs to a particular person. It rides every span
in the unit of work alongside the conversation id, so usage and cost can be
attributed per user.

## 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 (see
[Installation](#installation)), and the SDK works with none of them — you still
get the root span and identity on whatever your application emits itself.

Call `init()` as early as possible in your process. Coverage for a client built
beforehand depends on how that library's instrumentation attaches: most patch a
shared class method and pick up existing instances on their next call, while a
few — the FastAPI middleware in the `[http]` extra among them — attach at
construction and only see clients built afterward. Calling `init()` first is
always safe and never the wrong choice.

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

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

`service`, `tenant_id`, `project_id` and `endpoint` will not be renamed.
`project_name` and `aio_collector_endpoint` are keyword-only, as are the other
optional parameters; always pass them by name.

**Upgrading to 1.0.0.dev2.** `endpoint` used to mean the OTLP Collector and now
means the AIO server. Where the two differ, move the old value to
`aio_collector_endpoint`; otherwise project resolution is pointed at the
Collector.

## Troubleshooting

**`init()` returned `False` and nothing is showing up.** It needs a nonblank
`tenant_id` and exactly one of `project_id` or `project_name` — passing both, or
neither, is refused. The reason is logged at warning level rather than emitting
telemetry under the wrong tenant.

**Spans are missing tokens, tool calls, or model attributes.** The client was
possibly constructed before `init()` ran — see [Instrumentation](#instrumentation)
for which integrations that affects. Calling `init()` before constructing any
client sidesteps the question entirely.

**Calling `init()` from more than one place.** It is idempotent — the second and
later calls are no-ops that return `True` immediately — so it is safe to call
from more than one entry point (a test fixture and the application, for example)
without re-registering instrumentation.

## License

Apache-2.0. The instrumentation layer is vendored from
[OpenLIT](https://github.com/openlit/openlit); `UPSTREAM.md`, included in the
package, records exactly what was changed and why, per the license's
attribution requirements.

