immersive-oa-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- immersive_oa_sdk-0.1.0/.gitignore +5 -0
- immersive_oa_sdk-0.1.0/PKG-INFO +304 -0
- immersive_oa_sdk-0.1.0/README.md +265 -0
- immersive_oa_sdk-0.1.0/pyproject.toml +125 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/__init__.py +7 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_attributes.py +783 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_configure.py +370 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_consumers.py +261 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_content.py +303 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_helpers.py +124 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_instrumentation.py +310 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_marker.py +128 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_processor.py +437 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_propagator.py +179 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_sampler.py +112 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/_token.py +147 -0
- immersive_oa_sdk-0.1.0/src/immersive_oa_sdk/py.typed +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: immersive-oa-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Immersive AI Security SDK: OpenTelemetry setup and assessment-marker handling for Python apps
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: opentelemetry-api>=1.45.0
|
|
7
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.45.0
|
|
8
|
+
Requires-Dist: opentelemetry-instrumentation-aio-pika>=0.66b0
|
|
9
|
+
Requires-Dist: opentelemetry-instrumentation-aiohttp-client>=0.66b0
|
|
10
|
+
Requires-Dist: opentelemetry-instrumentation-aiohttp-server>=0.66b0
|
|
11
|
+
Requires-Dist: opentelemetry-instrumentation-asgi>=0.66b0
|
|
12
|
+
Requires-Dist: opentelemetry-instrumentation-boto3sqs>=0.66b0
|
|
13
|
+
Requires-Dist: opentelemetry-instrumentation-botocore>=0.66b0
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-celery>=0.66b0
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-confluent-kafka>=0.66b0
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.66b0
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.66b0
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-flask>=0.66b0
|
|
19
|
+
Requires-Dist: opentelemetry-instrumentation-grpc>=0.66b0
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.66b0
|
|
21
|
+
Requires-Dist: opentelemetry-instrumentation-kafka-python>=0.66b0
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation-pika>=0.66b0
|
|
23
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.66b0
|
|
24
|
+
Requires-Dist: opentelemetry-instrumentation-starlette>=0.66b0
|
|
25
|
+
Requires-Dist: opentelemetry-instrumentation-urllib3>=0.66b0
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-wsgi>=0.66b0
|
|
27
|
+
Requires-Dist: opentelemetry-sdk>=1.45.0
|
|
28
|
+
Provides-Extra: openinference
|
|
29
|
+
Requires-Dist: openinference-instrumentation-anthropic>=3.3.0; extra == 'openinference'
|
|
30
|
+
Requires-Dist: openinference-instrumentation-langchain>=0.1.81; extra == 'openinference'
|
|
31
|
+
Requires-Dist: openinference-instrumentation-mcp>=2.1.0; extra == 'openinference'
|
|
32
|
+
Requires-Dist: openinference-instrumentation-openai>=0.1.69; extra == 'openinference'
|
|
33
|
+
Provides-Extra: openllmetry
|
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.62.4; extra == 'openllmetry'
|
|
35
|
+
Requires-Dist: opentelemetry-instrumentation-langchain>=0.62.4; extra == 'openllmetry'
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-mcp>=0.62.4; extra == 'openllmetry'
|
|
37
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.62.4; extra == 'openllmetry'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# immersive-oa-sdk
|
|
41
|
+
|
|
42
|
+
OpenTelemetry instrumentation for AI applications, from Immersive AI Security.
|
|
43
|
+
|
|
44
|
+
`immersive-oa-sdk` gives you visibility into what your AI application is
|
|
45
|
+
actually doing. One call sets up the instrumentation an AI app needs — LLM,
|
|
46
|
+
agent and tool calls, plus the HTTP and messaging layers around them — and sends
|
|
47
|
+
a copy of that trace data to Observe & Assess, where it is assessed for AI
|
|
48
|
+
security risk.
|
|
49
|
+
|
|
50
|
+
If your application doesn't use OpenTelemetry yet, `immersive-oa-sdk` sets it up
|
|
51
|
+
for you. If it already does, `immersive-oa-sdk` adds to that setup and never
|
|
52
|
+
replaces it. Your own observability vendor keeps receiving everything it
|
|
53
|
+
receives today.
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
Python 3.11 or newer.
|
|
58
|
+
|
|
59
|
+
OpenTelemetry 1.45 or newer (contrib 0.66b0 or newer). `immersive-oa-sdk`
|
|
60
|
+
relies on internals of `opentelemetry.sdk`, `opentelemetry.attributes` and
|
|
61
|
+
several `opentelemetry.instrumentation.*` packages, so it is tested against
|
|
62
|
+
every OpenTelemetry release in that range.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
Install `immersive-oa-sdk` from PyPI, with the `openllmetry` or
|
|
67
|
+
`openinference` extra for LLM, agent and MCP spans (see
|
|
68
|
+
[LLM, agent and MCP calls](#llm-agent-and-mcp-calls)). The package imports as
|
|
69
|
+
`immersive_oa_sdk`.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pip install "immersive-oa-sdk[openllmetry]"
|
|
73
|
+
# or
|
|
74
|
+
uv add "immersive-oa-sdk[openllmetry]"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quickstart
|
|
78
|
+
|
|
79
|
+
This sends a small FastAPI app's spans to Observe & Assess.
|
|
80
|
+
|
|
81
|
+
1. In Observe & Assess, add a project on the Projects page. Copy the ingest
|
|
82
|
+
token it shows: it's shown only once.
|
|
83
|
+
2. Install `immersive-oa-sdk`, `fastapi` and `uvicorn`, and in `main.py` call
|
|
84
|
+
`immersive_oa_sdk.configure()` before you create the FastAPI app:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import fastapi
|
|
88
|
+
|
|
89
|
+
import immersive_oa_sdk
|
|
90
|
+
|
|
91
|
+
immersive_oa_sdk.configure()
|
|
92
|
+
app = fastapi.FastAPI()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.get("/hello")
|
|
96
|
+
def hello() -> dict[str, str]:
|
|
97
|
+
return {"hello": "world"}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
3. Run it with your token, your Observe & Assess address, and a name and
|
|
101
|
+
environment for the service:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
export IMMERSIVE_API_KEY=<token>
|
|
105
|
+
export IMMERSIVE_ENDPOINT=https://<your Observe & Assess address>
|
|
106
|
+
export OTEL_SERVICE_NAME=hello
|
|
107
|
+
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=dev
|
|
108
|
+
uvicorn main:app --port 8000
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
4. Call your app with `curl http://localhost:8000/hello`. Within a few seconds
|
|
112
|
+
the project in Observe & Assess shows the `dev` environment.
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
`configure()` reads these, and its keyword arguments of the same name win over
|
|
117
|
+
them:
|
|
118
|
+
|
|
119
|
+
| Variable | Argument | Default | What |
|
|
120
|
+
| --------------------------- | ----------------- | ---------------------- | ------------------------------------------------------------------------ |
|
|
121
|
+
| `IMMERSIVE_API_KEY` | `api_key` | none, required | The project's ingest token. |
|
|
122
|
+
| `IMMERSIVE_ENDPOINT` | `endpoint` | none, required | Base URL of your Observe & Assess instance; `/v1/traces` is appended. |
|
|
123
|
+
| `IMMERSIVE_CA_CERT` | `ca_cert` | the system trust store | Path to a CA bundle for that endpoint. |
|
|
124
|
+
| `IMMERSIVE_CAPTURE_CONTENT` | `capture_content` | `true` | `false` keeps prompt and completion text off GenAI spans. |
|
|
125
|
+
| `IMMERSIVE_ENABLED` | | `true` | `false` turns the SDK off without removing the call. |
|
|
126
|
+
| | `service_name` | `OTEL_SERVICE_NAME` | The service's name, unless the app registers its own tracer provider. |
|
|
127
|
+
|
|
128
|
+
`configure()` never raises and never stops your app. A missing endpoint or
|
|
129
|
+
token, or a malformed token, logs one warning and leaves export off.
|
|
130
|
+
|
|
131
|
+
It never reads `OTEL_EXPORTER_*`, so your own exporters keep their endpoint,
|
|
132
|
+
headers and certificates and ours never picks them up. It does follow the
|
|
133
|
+
standard variables that describe your app rather than an exporter:
|
|
134
|
+
|
|
135
|
+
- `OTEL_SERVICE_NAME` names the service, and `OTEL_RESOURCE_ATTRIBUTES` sets
|
|
136
|
+
its environment, as `deployment.environment.name=<environment>`. Spans with
|
|
137
|
+
no environment are filed under `default`. Both apply to your own
|
|
138
|
+
OpenTelemetry pipeline too.
|
|
139
|
+
- `OTEL_TRACES_SAMPLER` picks the sampler when `immersive-oa-sdk` registers the
|
|
140
|
+
tracer provider. Assessment requests are always sampled whatever it says.
|
|
141
|
+
- `OTEL_PROPAGATORS` keeps your propagators; ours is added alongside. Keep
|
|
142
|
+
`baggage` in it, or assessment requests are correlated only in the first
|
|
143
|
+
service they reach.
|
|
144
|
+
- `OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` turns off any of the bundled
|
|
145
|
+
instrumentations, by the names `opentelemetry-instrument` uses.
|
|
146
|
+
- `OTEL_SEMCONV_STABILITY_OPT_IN`, if you set it, picks the HTTP attributes.
|
|
147
|
+
Otherwise HTTP spans carry the stable ones.
|
|
148
|
+
|
|
149
|
+
## What's captured
|
|
150
|
+
|
|
151
|
+
`configure()` turns on the instrumentation for each of these that is
|
|
152
|
+
installed, unless you instrument it yourself:
|
|
153
|
+
|
|
154
|
+
- **Web frameworks:** FastAPI, Starlette, Flask, Django and aiohttp.
|
|
155
|
+
- **HTTP clients:** requests, httpx, urllib3 and aiohttp.
|
|
156
|
+
- **gRPC**, client and server.
|
|
157
|
+
- **Messaging:** Celery, Kafka (kafka-python and confluent-kafka), RabbitMQ
|
|
158
|
+
(pika and aio-pika), and SQS through boto3 clients.
|
|
159
|
+
- **GenAI**, with an extra: OpenAI, Anthropic, LangChain and LangGraph, and MCP,
|
|
160
|
+
prompts and completions included unless you turn content capture off.
|
|
161
|
+
|
|
162
|
+
Every span the app creates, through these or its own tracing, is exported to
|
|
163
|
+
Observe & Assess, and to your own exporters as well if you have them.
|
|
164
|
+
|
|
165
|
+
## Call `configure()` early
|
|
166
|
+
|
|
167
|
+
Call `configure()` once, at start-up, before you create your web framework's
|
|
168
|
+
app object, and create it through the framework's module, as below, rather
|
|
169
|
+
than with a class you imported earlier, such as `from fastapi import FastAPI`.
|
|
170
|
+
`configure()` turns on the HTTP instrumentation, which replaces the app class
|
|
171
|
+
on the framework's module, so an app created from the original class isn't
|
|
172
|
+
traced:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import fastapi
|
|
176
|
+
|
|
177
|
+
import immersive_oa_sdk
|
|
178
|
+
|
|
179
|
+
immersive_oa_sdk.configure()
|
|
180
|
+
app = fastapi.FastAPI()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If you instrument a web framework, HTTP client or messaging library yourself,
|
|
184
|
+
for example with `FlaskInstrumentor().instrument()`, do that before
|
|
185
|
+
`configure()` too, which leaves it as you set it up. Called afterwards,
|
|
186
|
+
OpenTelemetry ignores it, along with its options.
|
|
187
|
+
|
|
188
|
+
Call `configure()` at start-up in the workers that consume your queues too,
|
|
189
|
+
before they start consuming. A message published while handling an
|
|
190
|
+
assessment request carries its `request_id` and sampling decision, so the
|
|
191
|
+
worker's spans join the request's trace.
|
|
192
|
+
|
|
193
|
+
## Apps that already use OpenTelemetry
|
|
194
|
+
|
|
195
|
+
If your application registers its own tracer provider, call
|
|
196
|
+
`immersive_oa_sdk.configure()` after it does: `immersive-oa-sdk` then adds its
|
|
197
|
+
exporter to your provider, which keeps your resource and sampler. OpenTelemetry
|
|
198
|
+
ignores a tracer provider registered after the first, so calling `configure()`
|
|
199
|
+
first would leave your own provider unused.
|
|
200
|
+
|
|
201
|
+
Wrap your sampler in `immersive_oa_sdk.sampler()` too, so that requests sent by
|
|
202
|
+
an Observe & Assess assessment are always sampled. Everything else is still
|
|
203
|
+
sampled as your own sampler decides:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import immersive_oa_sdk
|
|
207
|
+
from opentelemetry import trace
|
|
208
|
+
from opentelemetry.sdk.resources import Resource
|
|
209
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
210
|
+
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
|
|
211
|
+
|
|
212
|
+
provider = TracerProvider(
|
|
213
|
+
resource=Resource.create(),
|
|
214
|
+
sampler=immersive_oa_sdk.sampler(ParentBased(TraceIdRatioBased(0.1))),
|
|
215
|
+
)
|
|
216
|
+
trace.set_tracer_provider(provider)
|
|
217
|
+
immersive_oa_sdk.configure()
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`configure()` logs a warning if your provider's sampler isn't wrapped. If
|
|
221
|
+
`immersive-oa-sdk` registers the provider itself, its sampler is already wrapped
|
|
222
|
+
and follows `OTEL_TRACES_SAMPLER` for everything else.
|
|
223
|
+
|
|
224
|
+
If the service you assess calls other services of yours that also use
|
|
225
|
+
`immersive-oa-sdk`, keep their samplers `ParentBased`, as in the example above.
|
|
226
|
+
Only the service that verifies an assessment request, normally the one you
|
|
227
|
+
assess, always samples it; the services it calls keep their spans only if their
|
|
228
|
+
sampler follows its decision. `ParentBased` is the OpenTelemetry default. If you
|
|
229
|
+
set `OTEL_TRACES_SAMPLER`, use one of its `parentbased_*` values.
|
|
230
|
+
|
|
231
|
+
## LLM, agent and MCP calls
|
|
232
|
+
|
|
233
|
+
LLM, agent and MCP spans come from an extra, so you keep the GenAI
|
|
234
|
+
instrumentation you may already run. `immersive-oa-sdk[openllmetry]` adds
|
|
235
|
+
OpenLLMetry's instrumentation for OpenAI, Anthropic, LangChain and LangGraph,
|
|
236
|
+
and MCP, and `immersive-oa-sdk[openinference]` adds OpenInference's for the same
|
|
237
|
+
libraries. `configure()` turns each on when its library is installed. A
|
|
238
|
+
library your app already instruments, with OpenLLMetry or OpenInference, is
|
|
239
|
+
left as it is.
|
|
240
|
+
|
|
241
|
+
Install one extra. With both, `configure()` warns and turns on only one
|
|
242
|
+
flavour, so no call is traced twice: the one your app already instruments a
|
|
243
|
+
library with, or else OpenLLMetry.
|
|
244
|
+
|
|
245
|
+
Those spans record prompt and completion text, which Observe & Assess checks
|
|
246
|
+
for AI security risk. To keep it out, pass `capture_content=False` to
|
|
247
|
+
`configure()` or set `IMMERSIVE_CAPTURE_CONTENT=false`. That turns off
|
|
248
|
+
OpenLLMetry's own content capture for the whole process, including any
|
|
249
|
+
OpenLLMetry instrumentation of your own. OpenInference reads its switches
|
|
250
|
+
when it's turned on, so OpenInference instrumentation of your own is covered
|
|
251
|
+
only if you turn it on after `configure()`.
|
|
252
|
+
|
|
253
|
+
## Agents built by hand
|
|
254
|
+
|
|
255
|
+
An agent written straight against an LLM SDK, rather than a framework such as
|
|
256
|
+
LangGraph, gets spans for its LLM calls but none for the agent or its tools, so
|
|
257
|
+
it doesn't appear in Observe & Assess's agent inventory. Trace it with
|
|
258
|
+
`immersive_oa_sdk.agent()` and its tools with `immersive_oa_sdk.tool()`, as
|
|
259
|
+
decorators on sync or async functions, or as context managers:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
import immersive_oa_sdk
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@immersive_oa_sdk.tool("search")
|
|
266
|
+
async def search(query: str) -> str: ...
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@immersive_oa_sdk.agent("planner")
|
|
270
|
+
async def plan(goal: str) -> str:
|
|
271
|
+
return await search(goal)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
with immersive_oa_sdk.agent("planner"):
|
|
275
|
+
...
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The agent's name is how it appears in Observe & Assess, so keep it stable.
|
|
279
|
+
Both are harmless without `configure()`.
|
|
280
|
+
|
|
281
|
+
## Limitations
|
|
282
|
+
|
|
283
|
+
- **Samplers downstream must be parent-based.** A service the assessed one
|
|
284
|
+
calls, with a sampler that isn't `ParentBased`, may drop an assessment
|
|
285
|
+
request's spans.
|
|
286
|
+
- **Every hop must be instrumented.** A service, or a library in one, that
|
|
287
|
+
doesn't pass the trace context and baggage on breaks the chain: everything
|
|
288
|
+
past it is traced without the request's `request_id`, so it isn't
|
|
289
|
+
correlated with the assessment.
|
|
290
|
+
- **Forwarding through your own collector gives observability, not
|
|
291
|
+
assessment correlation.** Spans your collector forwards to Observe & Assess
|
|
292
|
+
from apps without `immersive-oa-sdk` are stored and scanned, but only `immersive-oa-sdk`
|
|
293
|
+
in the app verifies an assessment request's marker and tags its spans with
|
|
294
|
+
the `request_id`.
|
|
295
|
+
- **With OpenInference, agents are LangChain runs.** OpenInference has no
|
|
296
|
+
agent span, so the first span of each LangChain run, a LangGraph graph's,
|
|
297
|
+
is reported as an agent named after it. A sub-agent inside another isn't
|
|
298
|
+
told apart from a plain graph node. An OpenAI or Anthropic call LangChain
|
|
299
|
+
makes is traced twice, once by each library's instrumentation; the second
|
|
300
|
+
span sits under your request rather than the graph, so it names no agent.
|
|
301
|
+
OpenInference instruments only `openai` 2.8, `anthropic` 1.8 and `mcp` 1.24
|
|
302
|
+
or newer.
|
|
303
|
+
- **MCP 2** isn't instrumented with OpenLLMetry yet, and SQS through raw
|
|
304
|
+
botocore clients, rather than boto3, is traced without carrying context.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# immersive-oa-sdk
|
|
2
|
+
|
|
3
|
+
OpenTelemetry instrumentation for AI applications, from Immersive AI Security.
|
|
4
|
+
|
|
5
|
+
`immersive-oa-sdk` gives you visibility into what your AI application is
|
|
6
|
+
actually doing. One call sets up the instrumentation an AI app needs — LLM,
|
|
7
|
+
agent and tool calls, plus the HTTP and messaging layers around them — and sends
|
|
8
|
+
a copy of that trace data to Observe & Assess, where it is assessed for AI
|
|
9
|
+
security risk.
|
|
10
|
+
|
|
11
|
+
If your application doesn't use OpenTelemetry yet, `immersive-oa-sdk` sets it up
|
|
12
|
+
for you. If it already does, `immersive-oa-sdk` adds to that setup and never
|
|
13
|
+
replaces it. Your own observability vendor keeps receiving everything it
|
|
14
|
+
receives today.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
Python 3.11 or newer.
|
|
19
|
+
|
|
20
|
+
OpenTelemetry 1.45 or newer (contrib 0.66b0 or newer). `immersive-oa-sdk`
|
|
21
|
+
relies on internals of `opentelemetry.sdk`, `opentelemetry.attributes` and
|
|
22
|
+
several `opentelemetry.instrumentation.*` packages, so it is tested against
|
|
23
|
+
every OpenTelemetry release in that range.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Install `immersive-oa-sdk` from PyPI, with the `openllmetry` or
|
|
28
|
+
`openinference` extra for LLM, agent and MCP spans (see
|
|
29
|
+
[LLM, agent and MCP calls](#llm-agent-and-mcp-calls)). The package imports as
|
|
30
|
+
`immersive_oa_sdk`.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install "immersive-oa-sdk[openllmetry]"
|
|
34
|
+
# or
|
|
35
|
+
uv add "immersive-oa-sdk[openllmetry]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
This sends a small FastAPI app's spans to Observe & Assess.
|
|
41
|
+
|
|
42
|
+
1. In Observe & Assess, add a project on the Projects page. Copy the ingest
|
|
43
|
+
token it shows: it's shown only once.
|
|
44
|
+
2. Install `immersive-oa-sdk`, `fastapi` and `uvicorn`, and in `main.py` call
|
|
45
|
+
`immersive_oa_sdk.configure()` before you create the FastAPI app:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import fastapi
|
|
49
|
+
|
|
50
|
+
import immersive_oa_sdk
|
|
51
|
+
|
|
52
|
+
immersive_oa_sdk.configure()
|
|
53
|
+
app = fastapi.FastAPI()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.get("/hello")
|
|
57
|
+
def hello() -> dict[str, str]:
|
|
58
|
+
return {"hello": "world"}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
3. Run it with your token, your Observe & Assess address, and a name and
|
|
62
|
+
environment for the service:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
export IMMERSIVE_API_KEY=<token>
|
|
66
|
+
export IMMERSIVE_ENDPOINT=https://<your Observe & Assess address>
|
|
67
|
+
export OTEL_SERVICE_NAME=hello
|
|
68
|
+
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=dev
|
|
69
|
+
uvicorn main:app --port 8000
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
4. Call your app with `curl http://localhost:8000/hello`. Within a few seconds
|
|
73
|
+
the project in Observe & Assess shows the `dev` environment.
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
`configure()` reads these, and its keyword arguments of the same name win over
|
|
78
|
+
them:
|
|
79
|
+
|
|
80
|
+
| Variable | Argument | Default | What |
|
|
81
|
+
| --------------------------- | ----------------- | ---------------------- | ------------------------------------------------------------------------ |
|
|
82
|
+
| `IMMERSIVE_API_KEY` | `api_key` | none, required | The project's ingest token. |
|
|
83
|
+
| `IMMERSIVE_ENDPOINT` | `endpoint` | none, required | Base URL of your Observe & Assess instance; `/v1/traces` is appended. |
|
|
84
|
+
| `IMMERSIVE_CA_CERT` | `ca_cert` | the system trust store | Path to a CA bundle for that endpoint. |
|
|
85
|
+
| `IMMERSIVE_CAPTURE_CONTENT` | `capture_content` | `true` | `false` keeps prompt and completion text off GenAI spans. |
|
|
86
|
+
| `IMMERSIVE_ENABLED` | | `true` | `false` turns the SDK off without removing the call. |
|
|
87
|
+
| | `service_name` | `OTEL_SERVICE_NAME` | The service's name, unless the app registers its own tracer provider. |
|
|
88
|
+
|
|
89
|
+
`configure()` never raises and never stops your app. A missing endpoint or
|
|
90
|
+
token, or a malformed token, logs one warning and leaves export off.
|
|
91
|
+
|
|
92
|
+
It never reads `OTEL_EXPORTER_*`, so your own exporters keep their endpoint,
|
|
93
|
+
headers and certificates and ours never picks them up. It does follow the
|
|
94
|
+
standard variables that describe your app rather than an exporter:
|
|
95
|
+
|
|
96
|
+
- `OTEL_SERVICE_NAME` names the service, and `OTEL_RESOURCE_ATTRIBUTES` sets
|
|
97
|
+
its environment, as `deployment.environment.name=<environment>`. Spans with
|
|
98
|
+
no environment are filed under `default`. Both apply to your own
|
|
99
|
+
OpenTelemetry pipeline too.
|
|
100
|
+
- `OTEL_TRACES_SAMPLER` picks the sampler when `immersive-oa-sdk` registers the
|
|
101
|
+
tracer provider. Assessment requests are always sampled whatever it says.
|
|
102
|
+
- `OTEL_PROPAGATORS` keeps your propagators; ours is added alongside. Keep
|
|
103
|
+
`baggage` in it, or assessment requests are correlated only in the first
|
|
104
|
+
service they reach.
|
|
105
|
+
- `OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` turns off any of the bundled
|
|
106
|
+
instrumentations, by the names `opentelemetry-instrument` uses.
|
|
107
|
+
- `OTEL_SEMCONV_STABILITY_OPT_IN`, if you set it, picks the HTTP attributes.
|
|
108
|
+
Otherwise HTTP spans carry the stable ones.
|
|
109
|
+
|
|
110
|
+
## What's captured
|
|
111
|
+
|
|
112
|
+
`configure()` turns on the instrumentation for each of these that is
|
|
113
|
+
installed, unless you instrument it yourself:
|
|
114
|
+
|
|
115
|
+
- **Web frameworks:** FastAPI, Starlette, Flask, Django and aiohttp.
|
|
116
|
+
- **HTTP clients:** requests, httpx, urllib3 and aiohttp.
|
|
117
|
+
- **gRPC**, client and server.
|
|
118
|
+
- **Messaging:** Celery, Kafka (kafka-python and confluent-kafka), RabbitMQ
|
|
119
|
+
(pika and aio-pika), and SQS through boto3 clients.
|
|
120
|
+
- **GenAI**, with an extra: OpenAI, Anthropic, LangChain and LangGraph, and MCP,
|
|
121
|
+
prompts and completions included unless you turn content capture off.
|
|
122
|
+
|
|
123
|
+
Every span the app creates, through these or its own tracing, is exported to
|
|
124
|
+
Observe & Assess, and to your own exporters as well if you have them.
|
|
125
|
+
|
|
126
|
+
## Call `configure()` early
|
|
127
|
+
|
|
128
|
+
Call `configure()` once, at start-up, before you create your web framework's
|
|
129
|
+
app object, and create it through the framework's module, as below, rather
|
|
130
|
+
than with a class you imported earlier, such as `from fastapi import FastAPI`.
|
|
131
|
+
`configure()` turns on the HTTP instrumentation, which replaces the app class
|
|
132
|
+
on the framework's module, so an app created from the original class isn't
|
|
133
|
+
traced:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import fastapi
|
|
137
|
+
|
|
138
|
+
import immersive_oa_sdk
|
|
139
|
+
|
|
140
|
+
immersive_oa_sdk.configure()
|
|
141
|
+
app = fastapi.FastAPI()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If you instrument a web framework, HTTP client or messaging library yourself,
|
|
145
|
+
for example with `FlaskInstrumentor().instrument()`, do that before
|
|
146
|
+
`configure()` too, which leaves it as you set it up. Called afterwards,
|
|
147
|
+
OpenTelemetry ignores it, along with its options.
|
|
148
|
+
|
|
149
|
+
Call `configure()` at start-up in the workers that consume your queues too,
|
|
150
|
+
before they start consuming. A message published while handling an
|
|
151
|
+
assessment request carries its `request_id` and sampling decision, so the
|
|
152
|
+
worker's spans join the request's trace.
|
|
153
|
+
|
|
154
|
+
## Apps that already use OpenTelemetry
|
|
155
|
+
|
|
156
|
+
If your application registers its own tracer provider, call
|
|
157
|
+
`immersive_oa_sdk.configure()` after it does: `immersive-oa-sdk` then adds its
|
|
158
|
+
exporter to your provider, which keeps your resource and sampler. OpenTelemetry
|
|
159
|
+
ignores a tracer provider registered after the first, so calling `configure()`
|
|
160
|
+
first would leave your own provider unused.
|
|
161
|
+
|
|
162
|
+
Wrap your sampler in `immersive_oa_sdk.sampler()` too, so that requests sent by
|
|
163
|
+
an Observe & Assess assessment are always sampled. Everything else is still
|
|
164
|
+
sampled as your own sampler decides:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import immersive_oa_sdk
|
|
168
|
+
from opentelemetry import trace
|
|
169
|
+
from opentelemetry.sdk.resources import Resource
|
|
170
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
171
|
+
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
|
|
172
|
+
|
|
173
|
+
provider = TracerProvider(
|
|
174
|
+
resource=Resource.create(),
|
|
175
|
+
sampler=immersive_oa_sdk.sampler(ParentBased(TraceIdRatioBased(0.1))),
|
|
176
|
+
)
|
|
177
|
+
trace.set_tracer_provider(provider)
|
|
178
|
+
immersive_oa_sdk.configure()
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`configure()` logs a warning if your provider's sampler isn't wrapped. If
|
|
182
|
+
`immersive-oa-sdk` registers the provider itself, its sampler is already wrapped
|
|
183
|
+
and follows `OTEL_TRACES_SAMPLER` for everything else.
|
|
184
|
+
|
|
185
|
+
If the service you assess calls other services of yours that also use
|
|
186
|
+
`immersive-oa-sdk`, keep their samplers `ParentBased`, as in the example above.
|
|
187
|
+
Only the service that verifies an assessment request, normally the one you
|
|
188
|
+
assess, always samples it; the services it calls keep their spans only if their
|
|
189
|
+
sampler follows its decision. `ParentBased` is the OpenTelemetry default. If you
|
|
190
|
+
set `OTEL_TRACES_SAMPLER`, use one of its `parentbased_*` values.
|
|
191
|
+
|
|
192
|
+
## LLM, agent and MCP calls
|
|
193
|
+
|
|
194
|
+
LLM, agent and MCP spans come from an extra, so you keep the GenAI
|
|
195
|
+
instrumentation you may already run. `immersive-oa-sdk[openllmetry]` adds
|
|
196
|
+
OpenLLMetry's instrumentation for OpenAI, Anthropic, LangChain and LangGraph,
|
|
197
|
+
and MCP, and `immersive-oa-sdk[openinference]` adds OpenInference's for the same
|
|
198
|
+
libraries. `configure()` turns each on when its library is installed. A
|
|
199
|
+
library your app already instruments, with OpenLLMetry or OpenInference, is
|
|
200
|
+
left as it is.
|
|
201
|
+
|
|
202
|
+
Install one extra. With both, `configure()` warns and turns on only one
|
|
203
|
+
flavour, so no call is traced twice: the one your app already instruments a
|
|
204
|
+
library with, or else OpenLLMetry.
|
|
205
|
+
|
|
206
|
+
Those spans record prompt and completion text, which Observe & Assess checks
|
|
207
|
+
for AI security risk. To keep it out, pass `capture_content=False` to
|
|
208
|
+
`configure()` or set `IMMERSIVE_CAPTURE_CONTENT=false`. That turns off
|
|
209
|
+
OpenLLMetry's own content capture for the whole process, including any
|
|
210
|
+
OpenLLMetry instrumentation of your own. OpenInference reads its switches
|
|
211
|
+
when it's turned on, so OpenInference instrumentation of your own is covered
|
|
212
|
+
only if you turn it on after `configure()`.
|
|
213
|
+
|
|
214
|
+
## Agents built by hand
|
|
215
|
+
|
|
216
|
+
An agent written straight against an LLM SDK, rather than a framework such as
|
|
217
|
+
LangGraph, gets spans for its LLM calls but none for the agent or its tools, so
|
|
218
|
+
it doesn't appear in Observe & Assess's agent inventory. Trace it with
|
|
219
|
+
`immersive_oa_sdk.agent()` and its tools with `immersive_oa_sdk.tool()`, as
|
|
220
|
+
decorators on sync or async functions, or as context managers:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
import immersive_oa_sdk
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@immersive_oa_sdk.tool("search")
|
|
227
|
+
async def search(query: str) -> str: ...
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@immersive_oa_sdk.agent("planner")
|
|
231
|
+
async def plan(goal: str) -> str:
|
|
232
|
+
return await search(goal)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
with immersive_oa_sdk.agent("planner"):
|
|
236
|
+
...
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The agent's name is how it appears in Observe & Assess, so keep it stable.
|
|
240
|
+
Both are harmless without `configure()`.
|
|
241
|
+
|
|
242
|
+
## Limitations
|
|
243
|
+
|
|
244
|
+
- **Samplers downstream must be parent-based.** A service the assessed one
|
|
245
|
+
calls, with a sampler that isn't `ParentBased`, may drop an assessment
|
|
246
|
+
request's spans.
|
|
247
|
+
- **Every hop must be instrumented.** A service, or a library in one, that
|
|
248
|
+
doesn't pass the trace context and baggage on breaks the chain: everything
|
|
249
|
+
past it is traced without the request's `request_id`, so it isn't
|
|
250
|
+
correlated with the assessment.
|
|
251
|
+
- **Forwarding through your own collector gives observability, not
|
|
252
|
+
assessment correlation.** Spans your collector forwards to Observe & Assess
|
|
253
|
+
from apps without `immersive-oa-sdk` are stored and scanned, but only `immersive-oa-sdk`
|
|
254
|
+
in the app verifies an assessment request's marker and tags its spans with
|
|
255
|
+
the `request_id`.
|
|
256
|
+
- **With OpenInference, agents are LangChain runs.** OpenInference has no
|
|
257
|
+
agent span, so the first span of each LangChain run, a LangGraph graph's,
|
|
258
|
+
is reported as an agent named after it. A sub-agent inside another isn't
|
|
259
|
+
told apart from a plain graph node. An OpenAI or Anthropic call LangChain
|
|
260
|
+
makes is traced twice, once by each library's instrumentation; the second
|
|
261
|
+
span sits under your request rather than the graph, so it names no agent.
|
|
262
|
+
OpenInference instruments only `openai` 2.8, `anthropic` 1.8 and `mcp` 1.24
|
|
263
|
+
or newer.
|
|
264
|
+
- **MCP 2** isn't instrumented with OpenLLMetry yet, and SQS through raw
|
|
265
|
+
botocore clients, rather than boto3, is traced without carrying context.
|