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.
@@ -0,0 +1,5 @@
1
+ # hatchling copies the nearest .gitignore into the published sdist, and builds
2
+ # from its patterns. Without this one it ships the repo root's.
3
+ __pycache__/
4
+ .venv/
5
+ dist/
@@ -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.