Metadata-Version: 2.5
Name: immersive-oa-sdk
Version: 0.1.0
Summary: Immersive AI Security SDK: OpenTelemetry setup and assessment-marker handling for Python apps
Requires-Python: >=3.11
Requires-Dist: opentelemetry-api>=1.45.0
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.45.0
Requires-Dist: opentelemetry-instrumentation-aio-pika>=0.66b0
Requires-Dist: opentelemetry-instrumentation-aiohttp-client>=0.66b0
Requires-Dist: opentelemetry-instrumentation-aiohttp-server>=0.66b0
Requires-Dist: opentelemetry-instrumentation-asgi>=0.66b0
Requires-Dist: opentelemetry-instrumentation-boto3sqs>=0.66b0
Requires-Dist: opentelemetry-instrumentation-botocore>=0.66b0
Requires-Dist: opentelemetry-instrumentation-celery>=0.66b0
Requires-Dist: opentelemetry-instrumentation-confluent-kafka>=0.66b0
Requires-Dist: opentelemetry-instrumentation-django>=0.66b0
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.66b0
Requires-Dist: opentelemetry-instrumentation-flask>=0.66b0
Requires-Dist: opentelemetry-instrumentation-grpc>=0.66b0
Requires-Dist: opentelemetry-instrumentation-httpx>=0.66b0
Requires-Dist: opentelemetry-instrumentation-kafka-python>=0.66b0
Requires-Dist: opentelemetry-instrumentation-pika>=0.66b0
Requires-Dist: opentelemetry-instrumentation-requests>=0.66b0
Requires-Dist: opentelemetry-instrumentation-starlette>=0.66b0
Requires-Dist: opentelemetry-instrumentation-urllib3>=0.66b0
Requires-Dist: opentelemetry-instrumentation-wsgi>=0.66b0
Requires-Dist: opentelemetry-sdk>=1.45.0
Provides-Extra: openinference
Requires-Dist: openinference-instrumentation-anthropic>=3.3.0; extra == 'openinference'
Requires-Dist: openinference-instrumentation-langchain>=0.1.81; extra == 'openinference'
Requires-Dist: openinference-instrumentation-mcp>=2.1.0; extra == 'openinference'
Requires-Dist: openinference-instrumentation-openai>=0.1.69; extra == 'openinference'
Provides-Extra: openllmetry
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.62.4; extra == 'openllmetry'
Requires-Dist: opentelemetry-instrumentation-langchain>=0.62.4; extra == 'openllmetry'
Requires-Dist: opentelemetry-instrumentation-mcp>=0.62.4; extra == 'openllmetry'
Requires-Dist: opentelemetry-instrumentation-openai>=0.62.4; extra == 'openllmetry'
Description-Content-Type: text/markdown

# immersive-oa-sdk

OpenTelemetry instrumentation for AI applications, from Immersive AI Security.

`immersive-oa-sdk` gives you visibility into what your AI application is
actually doing. One call sets up the instrumentation an AI app needs — LLM,
agent and tool calls, plus the HTTP and messaging layers around them — and sends
a copy of that trace data to Observe & Assess, where it is assessed for AI
security risk.

If your application doesn't use OpenTelemetry yet, `immersive-oa-sdk` sets it up
for you. If it already does, `immersive-oa-sdk` adds to that setup and never
replaces it. Your own observability vendor keeps receiving everything it
receives today.

## Requirements

Python 3.11 or newer.

OpenTelemetry 1.45 or newer (contrib 0.66b0 or newer). `immersive-oa-sdk`
relies on internals of `opentelemetry.sdk`, `opentelemetry.attributes` and
several `opentelemetry.instrumentation.*` packages, so it is tested against
every OpenTelemetry release in that range.

## Install

Install `immersive-oa-sdk` from PyPI, with the `openllmetry` or
`openinference` extra for LLM, agent and MCP spans (see
[LLM, agent and MCP calls](#llm-agent-and-mcp-calls)). The package imports as
`immersive_oa_sdk`.

```sh
pip install "immersive-oa-sdk[openllmetry]"
# or
uv add "immersive-oa-sdk[openllmetry]"
```

## Quickstart

This sends a small FastAPI app's spans to Observe & Assess.

1. In Observe & Assess, add a project on the Projects page. Copy the ingest
   token it shows: it's shown only once.
2. Install `immersive-oa-sdk`, `fastapi` and `uvicorn`, and in `main.py` call
   `immersive_oa_sdk.configure()` before you create the FastAPI app:

   ```python
   import fastapi

   import immersive_oa_sdk

   immersive_oa_sdk.configure()
   app = fastapi.FastAPI()


   @app.get("/hello")
   def hello() -> dict[str, str]:
       return {"hello": "world"}
   ```

3. Run it with your token, your Observe & Assess address, and a name and
   environment for the service:

   ```sh
   export IMMERSIVE_API_KEY=<token>
   export IMMERSIVE_ENDPOINT=https://<your Observe & Assess address>
   export OTEL_SERVICE_NAME=hello
   export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=dev
   uvicorn main:app --port 8000
   ```

4. Call your app with `curl http://localhost:8000/hello`. Within a few seconds
   the project in Observe & Assess shows the `dev` environment.

## Configuration

`configure()` reads these, and its keyword arguments of the same name win over
them:

| Variable                    | Argument          | Default                | What                                                                     |
| --------------------------- | ----------------- | ---------------------- | ------------------------------------------------------------------------ |
| `IMMERSIVE_API_KEY`         | `api_key`         | none, required         | The project's ingest token.                                              |
| `IMMERSIVE_ENDPOINT`        | `endpoint`        | none, required         | Base URL of your Observe & Assess instance; `/v1/traces` is appended.    |
| `IMMERSIVE_CA_CERT`         | `ca_cert`         | the system trust store | Path to a CA bundle for that endpoint.                                   |
| `IMMERSIVE_CAPTURE_CONTENT` | `capture_content` | `true`                 | `false` keeps prompt and completion text off GenAI spans.                |
| `IMMERSIVE_ENABLED`         |                   | `true`                 | `false` turns the SDK off without removing the call.                     |
|                             | `service_name`    | `OTEL_SERVICE_NAME`    | The service's name, unless the app registers its own tracer provider.    |

`configure()` never raises and never stops your app. A missing endpoint or
token, or a malformed token, logs one warning and leaves export off.

It never reads `OTEL_EXPORTER_*`, so your own exporters keep their endpoint,
headers and certificates and ours never picks them up. It does follow the
standard variables that describe your app rather than an exporter:

- `OTEL_SERVICE_NAME` names the service, and `OTEL_RESOURCE_ATTRIBUTES` sets
  its environment, as `deployment.environment.name=<environment>`. Spans with
  no environment are filed under `default`. Both apply to your own
  OpenTelemetry pipeline too.
- `OTEL_TRACES_SAMPLER` picks the sampler when `immersive-oa-sdk` registers the
  tracer provider. Assessment requests are always sampled whatever it says.
- `OTEL_PROPAGATORS` keeps your propagators; ours is added alongside. Keep
  `baggage` in it, or assessment requests are correlated only in the first
  service they reach.
- `OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` turns off any of the bundled
  instrumentations, by the names `opentelemetry-instrument` uses.
- `OTEL_SEMCONV_STABILITY_OPT_IN`, if you set it, picks the HTTP attributes.
  Otherwise HTTP spans carry the stable ones.

## What's captured

`configure()` turns on the instrumentation for each of these that is
installed, unless you instrument it yourself:

- **Web frameworks:** FastAPI, Starlette, Flask, Django and aiohttp.
- **HTTP clients:** requests, httpx, urllib3 and aiohttp.
- **gRPC**, client and server.
- **Messaging:** Celery, Kafka (kafka-python and confluent-kafka), RabbitMQ
  (pika and aio-pika), and SQS through boto3 clients.
- **GenAI**, with an extra: OpenAI, Anthropic, LangChain and LangGraph, and MCP,
  prompts and completions included unless you turn content capture off.

Every span the app creates, through these or its own tracing, is exported to
Observe & Assess, and to your own exporters as well if you have them.

## Call `configure()` early

Call `configure()` once, at start-up, before you create your web framework's
app object, and create it through the framework's module, as below, rather
than with a class you imported earlier, such as `from fastapi import FastAPI`.
`configure()` turns on the HTTP instrumentation, which replaces the app class
on the framework's module, so an app created from the original class isn't
traced:

```python
import fastapi

import immersive_oa_sdk

immersive_oa_sdk.configure()
app = fastapi.FastAPI()
```

If you instrument a web framework, HTTP client or messaging library yourself,
for example with `FlaskInstrumentor().instrument()`, do that before
`configure()` too, which leaves it as you set it up. Called afterwards,
OpenTelemetry ignores it, along with its options.

Call `configure()` at start-up in the workers that consume your queues too,
before they start consuming. A message published while handling an
assessment request carries its `request_id` and sampling decision, so the
worker's spans join the request's trace.

## Apps that already use OpenTelemetry

If your application registers its own tracer provider, call
`immersive_oa_sdk.configure()` after it does: `immersive-oa-sdk` then adds its
exporter to your provider, which keeps your resource and sampler. OpenTelemetry
ignores a tracer provider registered after the first, so calling `configure()`
first would leave your own provider unused.

Wrap your sampler in `immersive_oa_sdk.sampler()` too, so that requests sent by
an Observe & Assess assessment are always sampled. Everything else is still
sampled as your own sampler decides:

```python
import immersive_oa_sdk
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased

provider = TracerProvider(
    resource=Resource.create(),
    sampler=immersive_oa_sdk.sampler(ParentBased(TraceIdRatioBased(0.1))),
)
trace.set_tracer_provider(provider)
immersive_oa_sdk.configure()
```

`configure()` logs a warning if your provider's sampler isn't wrapped. If
`immersive-oa-sdk` registers the provider itself, its sampler is already wrapped
and follows `OTEL_TRACES_SAMPLER` for everything else.

If the service you assess calls other services of yours that also use
`immersive-oa-sdk`, keep their samplers `ParentBased`, as in the example above.
Only the service that verifies an assessment request, normally the one you
assess, always samples it; the services it calls keep their spans only if their
sampler follows its decision. `ParentBased` is the OpenTelemetry default. If you
set `OTEL_TRACES_SAMPLER`, use one of its `parentbased_*` values.

## LLM, agent and MCP calls

LLM, agent and MCP spans come from an extra, so you keep the GenAI
instrumentation you may already run. `immersive-oa-sdk[openllmetry]` adds
OpenLLMetry's instrumentation for OpenAI, Anthropic, LangChain and LangGraph,
and MCP, and `immersive-oa-sdk[openinference]` adds OpenInference's for the same
libraries. `configure()` turns each on when its library is installed. A
library your app already instruments, with OpenLLMetry or OpenInference, is
left as it is.

Install one extra. With both, `configure()` warns and turns on only one
flavour, so no call is traced twice: the one your app already instruments a
library with, or else OpenLLMetry.

Those spans record prompt and completion text, which Observe & Assess checks
for AI security risk. To keep it out, pass `capture_content=False` to
`configure()` or set `IMMERSIVE_CAPTURE_CONTENT=false`. That turns off
OpenLLMetry's own content capture for the whole process, including any
OpenLLMetry instrumentation of your own. OpenInference reads its switches
when it's turned on, so OpenInference instrumentation of your own is covered
only if you turn it on after `configure()`.

## Agents built by hand

An agent written straight against an LLM SDK, rather than a framework such as
LangGraph, gets spans for its LLM calls but none for the agent or its tools, so
it doesn't appear in Observe & Assess's agent inventory. Trace it with
`immersive_oa_sdk.agent()` and its tools with `immersive_oa_sdk.tool()`, as
decorators on sync or async functions, or as context managers:

```python
import immersive_oa_sdk


@immersive_oa_sdk.tool("search")
async def search(query: str) -> str: ...


@immersive_oa_sdk.agent("planner")
async def plan(goal: str) -> str:
    return await search(goal)


with immersive_oa_sdk.agent("planner"):
    ...
```

The agent's name is how it appears in Observe & Assess, so keep it stable.
Both are harmless without `configure()`.

## Limitations

- **Samplers downstream must be parent-based.** A service the assessed one
  calls, with a sampler that isn't `ParentBased`, may drop an assessment
  request's spans.
- **Every hop must be instrumented.** A service, or a library in one, that
  doesn't pass the trace context and baggage on breaks the chain: everything
  past it is traced without the request's `request_id`, so it isn't
  correlated with the assessment.
- **Forwarding through your own collector gives observability, not
  assessment correlation.** Spans your collector forwards to Observe & Assess
  from apps without `immersive-oa-sdk` are stored and scanned, but only `immersive-oa-sdk`
  in the app verifies an assessment request's marker and tags its spans with
  the `request_id`.
- **With OpenInference, agents are LangChain runs.** OpenInference has no
  agent span, so the first span of each LangChain run, a LangGraph graph's,
  is reported as an agent named after it. A sub-agent inside another isn't
  told apart from a plain graph node. An OpenAI or Anthropic call LangChain
  makes is traced twice, once by each library's instrumentation; the second
  span sits under your request rather than the graph, so it names no agent.
  OpenInference instruments only `openai` 2.8, `anthropic` 1.8 and `mcp` 1.24
  or newer.
- **MCP 2** isn't instrumented with OpenLLMetry yet, and SQS through raw
  botocore clients, rather than boto3, is traced without carrying context.
