struct-sdk 0.2.33__tar.gz → 0.3.1__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.
- struct_sdk-0.2.33/README.md → struct_sdk-0.3.1/PKG-INFO +275 -22
- struct_sdk-0.2.33/PKG-INFO → struct_sdk-0.3.1/README.md +175 -71
- struct_sdk-0.3.1/pyproject.toml +192 -0
- struct_sdk-0.3.1/src/struct_sdk/_auto.py +724 -0
- struct_sdk-0.3.1/src/struct_sdk/_distro.py +65 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/src/struct_sdk/anthropic.py +6 -4
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/src/struct_sdk/core.py +548 -53
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/src/struct_sdk/langchain.py +1 -1
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/src/struct_sdk/openai.py +4 -2
- struct_sdk-0.3.1/src/struct_sdk/temporal.py +119 -0
- struct_sdk-0.2.33/pyproject.toml +0 -113
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/.gitignore +0 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/LICENSE +0 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/src/struct_sdk/__init__.py +0 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/src/struct_sdk/_genai_content.py +0 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.1}/src/struct_sdk/claude_agent.py +0 -0
|
@@ -1,3 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: struct-sdk
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry
|
|
5
|
+
Project-URL: Homepage, https://struct.ai
|
|
6
|
+
Project-URL: Documentation, https://struct.ai/docs
|
|
7
|
+
Project-URL: Issues, https://struct.ai/support
|
|
8
|
+
Author-email: Struct <support@struct.ai>
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,ai,anthropic,langchain,observability,openai,opentelemetry,struct,tracing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: opentelemetry-api>=1.45.1
|
|
25
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
|
|
26
|
+
Requires-Dist: opentelemetry-sdk>=1.27.0
|
|
27
|
+
Provides-Extra: anthropic
|
|
28
|
+
Requires-Dist: anthropic>=0.30.0; extra == 'anthropic'
|
|
29
|
+
Provides-Extra: auto
|
|
30
|
+
Requires-Dist: opentelemetry-instrumentation-aio-pika; extra == 'auto'
|
|
31
|
+
Requires-Dist: opentelemetry-instrumentation-aiohttp-client; extra == 'auto'
|
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-aiohttp-server; extra == 'auto'
|
|
33
|
+
Requires-Dist: opentelemetry-instrumentation-aiokafka; extra == 'auto'
|
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-aiopg; extra == 'auto'
|
|
35
|
+
Requires-Dist: opentelemetry-instrumentation-asyncpg; extra == 'auto'
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-botocore; extra == 'auto'
|
|
37
|
+
Requires-Dist: opentelemetry-instrumentation-cassandra; extra == 'auto'
|
|
38
|
+
Requires-Dist: opentelemetry-instrumentation-celery; extra == 'auto'
|
|
39
|
+
Requires-Dist: opentelemetry-instrumentation-confluent-kafka; extra == 'auto'
|
|
40
|
+
Requires-Dist: opentelemetry-instrumentation-django; extra == 'auto'
|
|
41
|
+
Requires-Dist: opentelemetry-instrumentation-falcon; extra == 'auto'
|
|
42
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi; extra == 'auto'
|
|
43
|
+
Requires-Dist: opentelemetry-instrumentation-flask; extra == 'auto'
|
|
44
|
+
Requires-Dist: opentelemetry-instrumentation-grpc; extra == 'auto'
|
|
45
|
+
Requires-Dist: opentelemetry-instrumentation-httpx; extra == 'auto'
|
|
46
|
+
Requires-Dist: opentelemetry-instrumentation-jinja2; extra == 'auto'
|
|
47
|
+
Requires-Dist: opentelemetry-instrumentation-kafka-python; extra == 'auto'
|
|
48
|
+
Requires-Dist: opentelemetry-instrumentation-logging; extra == 'auto'
|
|
49
|
+
Requires-Dist: opentelemetry-instrumentation-mysql; extra == 'auto'
|
|
50
|
+
Requires-Dist: opentelemetry-instrumentation-mysqlclient; extra == 'auto'
|
|
51
|
+
Requires-Dist: opentelemetry-instrumentation-pika; extra == 'auto'
|
|
52
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg; extra == 'auto'
|
|
53
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg2; extra == 'auto'
|
|
54
|
+
Requires-Dist: opentelemetry-instrumentation-pymemcache; extra == 'auto'
|
|
55
|
+
Requires-Dist: opentelemetry-instrumentation-pymongo; extra == 'auto'
|
|
56
|
+
Requires-Dist: opentelemetry-instrumentation-pymssql; extra == 'auto'
|
|
57
|
+
Requires-Dist: opentelemetry-instrumentation-pymysql; extra == 'auto'
|
|
58
|
+
Requires-Dist: opentelemetry-instrumentation-pyramid; extra == 'auto'
|
|
59
|
+
Requires-Dist: opentelemetry-instrumentation-redis; extra == 'auto'
|
|
60
|
+
Requires-Dist: opentelemetry-instrumentation-requests; extra == 'auto'
|
|
61
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy; extra == 'auto'
|
|
62
|
+
Requires-Dist: opentelemetry-instrumentation-sqlite3; extra == 'auto'
|
|
63
|
+
Requires-Dist: opentelemetry-instrumentation-starlette; extra == 'auto'
|
|
64
|
+
Requires-Dist: opentelemetry-instrumentation-system-metrics; extra == 'auto'
|
|
65
|
+
Requires-Dist: opentelemetry-instrumentation-threading; extra == 'auto'
|
|
66
|
+
Requires-Dist: opentelemetry-instrumentation-tornado; extra == 'auto'
|
|
67
|
+
Requires-Dist: opentelemetry-instrumentation-tortoiseorm; extra == 'auto'
|
|
68
|
+
Requires-Dist: opentelemetry-instrumentation-urllib; extra == 'auto'
|
|
69
|
+
Requires-Dist: opentelemetry-instrumentation-urllib3; extra == 'auto'
|
|
70
|
+
Requires-Dist: opentelemetry-instrumentation>=0.51b0; extra == 'auto'
|
|
71
|
+
Requires-Dist: opentelemetry-resource-detector-containerid; extra == 'auto'
|
|
72
|
+
Provides-Extra: azure
|
|
73
|
+
Requires-Dist: azure-core-tracing-opentelemetry>=1.0.0b11; extra == 'azure'
|
|
74
|
+
Provides-Extra: claude-agent-sdk
|
|
75
|
+
Requires-Dist: claude-agent-sdk>=0.2.121; extra == 'claude-agent-sdk'
|
|
76
|
+
Requires-Dist: mcp<2,>=1.30.0; extra == 'claude-agent-sdk'
|
|
77
|
+
Provides-Extra: demo
|
|
78
|
+
Requires-Dist: langchain-anthropic>=1.4.6; extra == 'demo'
|
|
79
|
+
Requires-Dist: langchain-core>=1.3.3; extra == 'demo'
|
|
80
|
+
Requires-Dist: langchain-openai>=0.2.0; extra == 'demo'
|
|
81
|
+
Requires-Dist: langchain>=1.3.9; extra == 'demo'
|
|
82
|
+
Requires-Dist: langgraph>=0.2.0; extra == 'demo'
|
|
83
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == 'demo'
|
|
84
|
+
Provides-Extra: dev
|
|
85
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
86
|
+
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
87
|
+
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
88
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
89
|
+
Provides-Extra: exceptions
|
|
90
|
+
Requires-Dist: opentelemetry-instrumentation-exceptions; extra == 'exceptions'
|
|
91
|
+
Provides-Extra: langchain
|
|
92
|
+
Requires-Dist: langchain-core>=1.3.3; extra == 'langchain'
|
|
93
|
+
Provides-Extra: openai
|
|
94
|
+
Requires-Dist: openai>=1.66.0; extra == 'openai'
|
|
95
|
+
Provides-Extra: structlog
|
|
96
|
+
Requires-Dist: opentelemetry-instrumentation-structlog; extra == 'structlog'
|
|
97
|
+
Provides-Extra: temporal
|
|
98
|
+
Requires-Dist: temporalio>=1.11.0; extra == 'temporal'
|
|
99
|
+
Description-Content-Type: text/markdown
|
|
100
|
+
|
|
1
101
|
# struct-sdk
|
|
2
102
|
|
|
3
103
|
OpenTelemetry instrumentation for AI agents in Python. Captures spans,
|
|
@@ -63,7 +163,7 @@ async with struct.agent(name="checkout"):
|
|
|
63
163
|
| `anthropic` | `chat {model}` | Cache-token accounting; streaming chats with tool-use reconstruction. Bedrock and Vertex variants supported if installed. |
|
|
64
164
|
| `openai` (Responses API) | `chat {model}` | Non-streaming `responses.create` on sync and async clients (Azure clients included). Cached-token accounting from `usage.input_tokens_details`. Streaming and `chat.completions` calls pass through untraced. Message-content capture at parity with the Anthropic integration: per-message events (`gen_ai.system/user/assistant/tool.message`) and `gen_ai.choice`, plus `gen_ai.input.messages` / `gen_ai.output.messages` in span-content mode. Reasoning items are skipped (`encrypted_content` is never emitted). |
|
|
65
165
|
| `claude_agent_sdk` | `agent`, `chat`, `execute_tool` | Telemetry comes from Claude Code itself in the subprocess; ingest credentials are propagated to it via `ClaudeAgentOptions`. Subagents inherit the configuration. |
|
|
66
|
-
| `langchain_core` `BaseChatModel` | `chat {model}` |
|
|
166
|
+
| `langchain_core` `BaseChatModel` | `chat {model}` | When the underlying provider SDK is also instrumented (e.g. `ChatAnthropic` + `anthropic`), the provider's span is suppressed, so there is a single span. |
|
|
67
167
|
| `langchain_core` `BaseTool` | `execute_tool {name}` | `tool_call_id` extracted from the LangChain ToolCall when present. |
|
|
68
168
|
| `langchain_core` `BaseRetriever` | `retrieval {name}` | |
|
|
69
169
|
| `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent`, `langchain.agents.create_agent`, and custom graphs. Reads conversation id from any of: `configurable.thread_id` (LangGraph canonical), or `metadata.{thread_id, session_id, conversation_id}` (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in [`struct.agent(session_id=conv_id)`](#recommended-pattern-wrap-langchain-entry-points-in-structagent) — that's the struct-native replacement for `ls.tracing_context(parent=run_tree)`. |
|
|
@@ -255,8 +355,8 @@ async with struct.agent(name="my-agent"):
|
|
|
255
355
|
```
|
|
256
356
|
|
|
257
357
|
When you do use `ChatAnthropic` *and* have the `anthropic` SDK installed,
|
|
258
|
-
the chat span comes from the
|
|
259
|
-
|
|
358
|
+
the chat span comes from the LangChain layer (single span); the Anthropic
|
|
359
|
+
instrumentation skips its own to avoid duplicates.
|
|
260
360
|
|
|
261
361
|
## Content capture
|
|
262
362
|
|
|
@@ -268,21 +368,23 @@ from struct_sdk import struct, ContentCaptureMode
|
|
|
268
368
|
|
|
269
369
|
struct.init(
|
|
270
370
|
ingest_key=...,
|
|
271
|
-
content_capture=ContentCaptureMode.EVENT_ONLY, # default
|
|
272
|
-
# or ContentCaptureMode.
|
|
371
|
+
content_capture=ContentCaptureMode.EVENT_ONLY, # opt in; the default is NONE
|
|
372
|
+
# or ContentCaptureMode.SPAN_ONLY, SPAN_AND_EVENT
|
|
273
373
|
)
|
|
274
374
|
```
|
|
275
375
|
|
|
276
|
-
- **`EVENT_ONLY
|
|
376
|
+
- **`EVENT_ONLY`**: per-message content lands on OTel log
|
|
277
377
|
records (`gen_ai.{user,assistant,system,tool}.message`,
|
|
278
378
|
`gen_ai.choice`). Spans carry metadata only.
|
|
279
379
|
- **`SPAN_ONLY`**: content on span attributes (`gen_ai.input.messages`,
|
|
280
380
|
`gen_ai.output.messages`).
|
|
281
381
|
- **`SPAN_AND_EVENT`**: both.
|
|
282
|
-
- **`NONE
|
|
283
|
-
|
|
382
|
+
- **`NONE`** (default since 0.3.0): no content captured, including the
|
|
383
|
+
user prompt on the agent span and tool arguments and results. Token
|
|
384
|
+
counts, tool call IDs, finish reasons, and other metadata still flow.
|
|
284
385
|
|
|
285
|
-
`capture_content=False` is a shorthand for `ContentCaptureMode.NONE
|
|
386
|
+
`capture_content=False` is a shorthand for `ContentCaptureMode.NONE`, and
|
|
387
|
+
`capture_content=True` for `EVENT_ONLY`.
|
|
286
388
|
|
|
287
389
|
## Manual scopes
|
|
288
390
|
|
|
@@ -373,23 +475,175 @@ from `input_tokens`.
|
|
|
373
475
|
```python
|
|
374
476
|
struct.init(
|
|
375
477
|
ingest_key="pk-...", # required
|
|
376
|
-
service_name="my-agent", # default: "default-agent"
|
|
377
|
-
service_version="1.2.3", # default: "0.0.0"
|
|
378
|
-
environment="production", # default: "development"
|
|
478
|
+
service_name="my-agent", # default: OTEL_SERVICE_NAME, else "default-agent"
|
|
479
|
+
service_version="1.2.3", # default: from OTEL_RESOURCE_ATTRIBUTES, else "0.0.0"
|
|
480
|
+
environment="production", # default: from OTEL_RESOURCE_ATTRIBUTES, else "development"
|
|
379
481
|
endpoint="https://ingest.struct.ai", # default; override for self-hosted
|
|
380
482
|
shutdown_timeout_seconds=5.0, # default: 5.0
|
|
381
|
-
content_capture=ContentCaptureMode.EVENT_ONLY,
|
|
483
|
+
content_capture=ContentCaptureMode.EVENT_ONLY, # default: NONE
|
|
484
|
+
auto_instrument=True, # default; False = AI libraries only
|
|
485
|
+
instrumentation_options=None, # per-library full-app settings
|
|
486
|
+
redact_attributes=["*.password"], # default: STRUCT_REDACT_ATTRIBUTES
|
|
487
|
+
)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
With `auto_instrument=False` the SDK uses an isolated `TracerProvider` and
|
|
491
|
+
`LoggerProvider` and instruments only the AI libraries; your existing OTel
|
|
492
|
+
setup is unaffected. The default is [full-app mode](#full-app-mode).
|
|
493
|
+
|
|
494
|
+
Service identity follows the standard OpenTelemetry variables when you don't
|
|
495
|
+
pass it: `OTEL_SERVICE_NAME`, and `service.version` and
|
|
496
|
+
`deployment.environment` in `OTEL_RESOURCE_ATTRIBUTES`. Arguments to `init()`
|
|
497
|
+
win. The resource also carries host, runtime and (with the `[auto]` extra)
|
|
498
|
+
container metadata, plus the commit when `STRUCT_GIT_COMMIT_SHA` and
|
|
499
|
+
`STRUCT_GIT_REPOSITORY_URL` are set. It includes a `service.instance.id` so
|
|
500
|
+
replicas stay distinguishable; an ID from `OTEL_RESOURCE_ATTRIBUTES` is
|
|
501
|
+
preserved.
|
|
502
|
+
|
|
503
|
+
In pre-fork servers (for example `gunicorn --preload`), call `struct.init()` in
|
|
504
|
+
each worker (gunicorn's `post_fork` hook), not in the parent: forked workers
|
|
505
|
+
would otherwise share the parent's instance id. A worker that exports after
|
|
506
|
+
being forked from an initialized parent logs one INFO line saying so.
|
|
507
|
+
|
|
508
|
+
### Redaction
|
|
509
|
+
|
|
510
|
+
`redact_attributes` takes glob patterns, matched case-insensitively. Span,
|
|
511
|
+
span-event and log attributes whose keys match leave the process as
|
|
512
|
+
`[REDACTED]`, for example `["http.request.header.authorization", "*.password"]`.
|
|
513
|
+
`STRUCT_REDACT_ATTRIBUTES` (comma-separated) sets them without code. Only what
|
|
514
|
+
Struct sends is changed; your own exporters still see the real values.
|
|
515
|
+
Metrics, log bodies and span names are not redacted.
|
|
516
|
+
|
|
517
|
+
## Full-app mode
|
|
518
|
+
|
|
519
|
+
Full-app mode is the default since 0.3.0: `struct.init()` adds APM-style
|
|
520
|
+
telemetry for the whole service, like an OpenTelemetry distro: HTTP servers and
|
|
521
|
+
clients, databases, caches, queues, app logs and process metrics, built on the
|
|
522
|
+
OpenTelemetry contrib instrumentors. Every supported library you have installed
|
|
523
|
+
is instrumented; turn individual ones off below. To keep the 0.2 behavior
|
|
524
|
+
(AI libraries only, isolated providers), pass `auto_instrument=False`.
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
pip install "struct-sdk[auto]" # needs opentelemetry-sdk 1.30+; add [azure] for Azure SDK tracing,
|
|
528
|
+
# [structlog] for structlog (opentelemetry-sdk 1.44+),
|
|
529
|
+
# [exceptions] for uncaught exceptions (1.43+)
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
import fastapi
|
|
534
|
+
from struct_sdk import struct
|
|
535
|
+
|
|
536
|
+
struct.init(ingest_key="pk-...", service_name="checkout-api")
|
|
537
|
+
|
|
538
|
+
app = fastapi.FastAPI() # resolve the class after init()
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Framework instrumentors replace the framework's class when `init()` runs, so
|
|
542
|
+
an app class bound earlier stays uninstrumented: `from fastapi import FastAPI`
|
|
543
|
+
above `struct.init()` keeps the original class. Import framework classes after
|
|
544
|
+
`init()`, or reference them through the module as above. For an app created
|
|
545
|
+
before `init()`, call the contrib instrumentor directly (for example
|
|
546
|
+
`FastAPIInstrumentor.instrument_app(app)`). In pre-fork servers, run
|
|
547
|
+
`init()` in each worker, as described above.
|
|
548
|
+
|
|
549
|
+
| Signal | Coverage |
|
|
550
|
+
|---|---|
|
|
551
|
+
| Traces | Django, FastAPI, Flask, Starlette, Tornado, Falcon, Pyramid, aiohttp (client and server), requests, httpx, urllib, urllib3, gRPC, psycopg2, psycopg, aiopg, asyncpg, PyMySQL, MySQL Connector, mysqlclient, pymssql, sqlite3, PyMongo, Cassandra, Redis, pymemcache, Tortoise ORM, Celery, botocore, aiobotocore, Kafka (confluent-kafka, aiokafka, kafka-python), RabbitMQ (pika, aio-pika), Jinja2, Azure SDK. Work handed to threads and `ThreadPoolExecutor` stays in the caller's trace. SQLAlchemy is off by default because its query spans duplicate the database driver's; enable it with `{"sqlalchemy": {"enabled": True}}` if you prefer ORM-level spans. |
|
|
552
|
+
| Logs | Python `logging` records that reach the root logger, with trace context. The SDK's own and OpenTelemetry's records are excluded. With the `[structlog]` extra, structlog events that structlog writes itself (its default) are exported with their fields as attributes; events structlog hands to `logging` (`structlog.stdlib.LoggerFactory`) are exported once, through the `logging` bridge. Extras and structlog fields OpenTelemetry can't take as-is are exported as strings: dicts and mixed-type lists as JSON, everything else (UUIDs, datetimes, `None`, bytes) as its `str()`. Your other handlers still see the original values. With the `[exceptions]` extra, uncaught exceptions (main thread, other threads, asyncio) are exported as log records. If your app configures no logging, its warnings still print to stderr as they would without Struct. This is separate from `content_capture`, which only governs LLM content; disable `{"logging": {"enabled": False}}` and `{"structlog": {"enabled": False}}` (which also stops copies of your logs when Struct attaches to your OpenTelemetry setup), or redact attributes, if application logs can carry sensitive data. |
|
|
553
|
+
| Metrics | Instrumentor metrics (for example `http.server.duration`) and process metrics (`process.cpu.*`, `process.memory.*`, threads, file descriptors, GC). Counters and histograms use delta temporality. |
|
|
554
|
+
|
|
555
|
+
Each instrumentor activates only when its library is installed.
|
|
556
|
+
`instrumentation_options` disables or configures one by name; other keys are
|
|
557
|
+
passed to that instrumentor:
|
|
558
|
+
|
|
559
|
+
```python
|
|
560
|
+
struct.init(
|
|
561
|
+
ingest_key="pk-...",
|
|
562
|
+
instrumentation_options={
|
|
563
|
+
"fastapi": {"excluded_urls": "health,ready"},
|
|
564
|
+
"logging": {"enabled": False},
|
|
565
|
+
},
|
|
566
|
+
)
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
As with `opentelemetry-instrument` and the Azure Monitor distro, the standard
|
|
570
|
+
`OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` environment variable also turns
|
|
571
|
+
libraries off (`OTEL_PYTHON_DISABLED_INSTRUMENTATIONS=requests,urllib3`, or
|
|
572
|
+
`*` for all of them). An explicit `instrumentation_options` entry takes
|
|
573
|
+
precedence over it.
|
|
574
|
+
|
|
575
|
+
Full-app mode installs Struct's providers as the global OpenTelemetry
|
|
576
|
+
providers only when no tracer, meter or logger provider is installed. If the
|
|
577
|
+
process already configured any of them (Azure Monitor, your own SDK setup),
|
|
578
|
+
Struct adds its exporters to your SDK tracer and logger providers and
|
|
579
|
+
instruments nothing: your setup keeps ownership of instrumentation, sampling
|
|
580
|
+
and shutdown, and your metrics are not sent to Struct. Struct's own GenAI
|
|
581
|
+
spans keep their isolated provider either way, so your sampler does not drop
|
|
582
|
+
them.
|
|
583
|
+
|
|
584
|
+
### Datadog ddtrace
|
|
585
|
+
|
|
586
|
+
Under `ddtrace-run`, Struct leaves the libraries ddtrace patches to it, and
|
|
587
|
+
`urllib3` beneath `requests` or `botocore`, to avoid duplicate spans and
|
|
588
|
+
conflicting trace headers, and logs one INFO line listing them. Logs, process metrics, thread context, AI spans and libraries
|
|
589
|
+
ddtrace doesn't patch still go to Struct. With `DD_TRACE_OTEL_ENABLED=true`,
|
|
590
|
+
ddtrace's OpenTelemetry provider isn't the OpenTelemetry SDK, so full-app
|
|
591
|
+
mode attaches nothing (an INFO line says so) and only AI spans reach Struct.
|
|
592
|
+
|
|
593
|
+
### Zero-code activation
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
OTEL_PYTHON_DISTRO=struct \
|
|
597
|
+
STRUCT_INGEST_KEY=pk-... \
|
|
598
|
+
OTEL_SERVICE_NAME=checkout-api \
|
|
599
|
+
opentelemetry-instrument python app.py
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
`opentelemetry-instrument` comes with `[auto]`. Struct calls `struct.init()`
|
|
603
|
+
before your app starts, with `STRUCT_INGEST_URL` as the endpoint when set.
|
|
604
|
+
Message content stays off; call `struct.init()` in code to turn it on.
|
|
605
|
+
Installing the SDK never changes another distro: Struct's distro is used only
|
|
606
|
+
when `OTEL_PYTHON_DISTRO=struct`. When selected, it also keeps other installed
|
|
607
|
+
configurators (such as `opentelemetry-distro`'s) from installing competing
|
|
608
|
+
providers, unless you set `OTEL_PYTHON_CONFIGURATOR` yourself. If you already run another distro, call
|
|
609
|
+
`struct.init()` in code instead and Struct attaches to its providers.
|
|
610
|
+
|
|
611
|
+
### Temporal
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
pip install "struct-sdk[temporal]"
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
```python
|
|
618
|
+
from temporalio.client import Client
|
|
619
|
+
from temporalio.runtime import Runtime
|
|
620
|
+
from struct_sdk import struct, temporal
|
|
621
|
+
|
|
622
|
+
struct.init(ingest_key="pk-...", service_name="billing-worker")
|
|
623
|
+
runtime = Runtime(telemetry=temporal.telemetry_config(global_tags={"replica": replica_id}))
|
|
624
|
+
client = await Client.connect(
|
|
625
|
+
"temporal:7233",
|
|
626
|
+
runtime=runtime,
|
|
627
|
+
interceptors=[temporal.tracing_interceptor()],
|
|
382
628
|
)
|
|
383
629
|
```
|
|
384
630
|
|
|
385
|
-
|
|
386
|
-
|
|
631
|
+
`telemetry_config()` exports Temporal Core metrics (task slots,
|
|
632
|
+
schedule-to-start latency, poll results, sticky cache) to Struct. It keeps
|
|
633
|
+
Core's console logging at Temporal's defaults, except that failed metric
|
|
634
|
+
exports are not printed. `tracing_interceptor()` (temporalio 1.11+) traces
|
|
635
|
+
workflows and activities with Struct's tracer; a failing span processor never
|
|
636
|
+
blocks a Temporal call or replaces its exception.
|
|
637
|
+
Temporal server metrics are a Prometheus endpoint; scrape them with an
|
|
638
|
+
OpenTelemetry Collector and export OTLP/HTTP to your Struct ingest endpoint
|
|
639
|
+
with the `x-struct-ingest-key` header.
|
|
387
640
|
|
|
388
641
|
## Reliability
|
|
389
642
|
|
|
390
643
|
The SDK is designed never to break your application. Instrumentation
|
|
391
|
-
hooks, span exports, and shutdown all swallow exceptions internally
|
|
392
|
-
|
|
644
|
+
hooks, span exports, and shutdown all swallow exceptions internally and log
|
|
645
|
+
at DEBUG on the `struct_sdk` logger, with a traceback for the first failure
|
|
646
|
+
at each site.
|
|
393
647
|
Process exit is bounded by `shutdown_timeout_seconds` and runs in a
|
|
394
648
|
background thread, so a slow or unreachable ingest endpoint cannot hang
|
|
395
649
|
shutdown.
|
|
@@ -399,13 +653,12 @@ shutdown.
|
|
|
399
653
|
- **Spans missing after instrumenting:** Call `struct.init()` *before*
|
|
400
654
|
importing the instrumented libraries, so the SDK can wire up
|
|
401
655
|
instrumentation before any instance is constructed.
|
|
402
|
-
- **No log records appearing:**
|
|
403
|
-
mode is `EVENT_ONLY` or `SPAN_AND_EVENT
|
|
404
|
-
`capture_content=False`, content events are disabled.
|
|
656
|
+
- **No message log records appearing:** they only emit when the capture
|
|
657
|
+
mode is `EVENT_ONLY` or `SPAN_AND_EVENT`. The default is `NONE`.
|
|
405
658
|
- **Duplicate chat spans:** When an LLM provider SDK and a wrapping
|
|
406
659
|
framework are both instrumented (e.g. `ChatAnthropic` calling through
|
|
407
|
-
to `anthropic`), the
|
|
408
|
-
|
|
660
|
+
to `anthropic`), the provider-level chat span is skipped and the
|
|
661
|
+
framework's is kept.
|
|
409
662
|
|
|
410
663
|
## License
|
|
411
664
|
|
|
@@ -1,52 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: struct-sdk
|
|
3
|
-
Version: 0.2.33
|
|
4
|
-
Summary: Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry
|
|
5
|
-
Project-URL: Homepage, https://struct.ai
|
|
6
|
-
Project-URL: Documentation, https://struct.ai/docs
|
|
7
|
-
Project-URL: Issues, https://struct.ai/support
|
|
8
|
-
Author-email: Struct <support@struct.ai>
|
|
9
|
-
License: Apache-2.0
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: agent,ai,anthropic,langchain,observability,openai,opentelemetry,struct,tracing
|
|
12
|
-
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
-
Classifier: Topic :: System :: Monitoring
|
|
23
|
-
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: opentelemetry-api>=1.45.1
|
|
25
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
|
|
26
|
-
Requires-Dist: opentelemetry-sdk>=1.27.0
|
|
27
|
-
Provides-Extra: anthropic
|
|
28
|
-
Requires-Dist: anthropic>=0.30.0; extra == 'anthropic'
|
|
29
|
-
Provides-Extra: claude-agent-sdk
|
|
30
|
-
Requires-Dist: claude-agent-sdk>=0.2.121; extra == 'claude-agent-sdk'
|
|
31
|
-
Requires-Dist: mcp<2,>=1.30.0; extra == 'claude-agent-sdk'
|
|
32
|
-
Provides-Extra: demo
|
|
33
|
-
Requires-Dist: langchain-anthropic>=1.4.6; extra == 'demo'
|
|
34
|
-
Requires-Dist: langchain-core>=1.3.3; extra == 'demo'
|
|
35
|
-
Requires-Dist: langchain-openai>=0.2.0; extra == 'demo'
|
|
36
|
-
Requires-Dist: langchain>=1.3.9; extra == 'demo'
|
|
37
|
-
Requires-Dist: langgraph>=0.2.0; extra == 'demo'
|
|
38
|
-
Requires-Dist: python-dotenv>=1.0.0; extra == 'demo'
|
|
39
|
-
Provides-Extra: dev
|
|
40
|
-
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
41
|
-
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
42
|
-
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
43
|
-
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
44
|
-
Provides-Extra: langchain
|
|
45
|
-
Requires-Dist: langchain-core>=1.3.3; extra == 'langchain'
|
|
46
|
-
Provides-Extra: openai
|
|
47
|
-
Requires-Dist: openai>=1.66.0; extra == 'openai'
|
|
48
|
-
Description-Content-Type: text/markdown
|
|
49
|
-
|
|
50
1
|
# struct-sdk
|
|
51
2
|
|
|
52
3
|
OpenTelemetry instrumentation for AI agents in Python. Captures spans,
|
|
@@ -112,7 +63,7 @@ async with struct.agent(name="checkout"):
|
|
|
112
63
|
| `anthropic` | `chat {model}` | Cache-token accounting; streaming chats with tool-use reconstruction. Bedrock and Vertex variants supported if installed. |
|
|
113
64
|
| `openai` (Responses API) | `chat {model}` | Non-streaming `responses.create` on sync and async clients (Azure clients included). Cached-token accounting from `usage.input_tokens_details`. Streaming and `chat.completions` calls pass through untraced. Message-content capture at parity with the Anthropic integration: per-message events (`gen_ai.system/user/assistant/tool.message`) and `gen_ai.choice`, plus `gen_ai.input.messages` / `gen_ai.output.messages` in span-content mode. Reasoning items are skipped (`encrypted_content` is never emitted). |
|
|
114
65
|
| `claude_agent_sdk` | `agent`, `chat`, `execute_tool` | Telemetry comes from Claude Code itself in the subprocess; ingest credentials are propagated to it via `ClaudeAgentOptions`. Subagents inherit the configuration. |
|
|
115
|
-
| `langchain_core` `BaseChatModel` | `chat {model}` |
|
|
66
|
+
| `langchain_core` `BaseChatModel` | `chat {model}` | When the underlying provider SDK is also instrumented (e.g. `ChatAnthropic` + `anthropic`), the provider's span is suppressed, so there is a single span. |
|
|
116
67
|
| `langchain_core` `BaseTool` | `execute_tool {name}` | `tool_call_id` extracted from the LangChain ToolCall when present. |
|
|
117
68
|
| `langchain_core` `BaseRetriever` | `retrieval {name}` | |
|
|
118
69
|
| `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent`, `langchain.agents.create_agent`, and custom graphs. Reads conversation id from any of: `configurable.thread_id` (LangGraph canonical), or `metadata.{thread_id, session_id, conversation_id}` (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in [`struct.agent(session_id=conv_id)`](#recommended-pattern-wrap-langchain-entry-points-in-structagent) — that's the struct-native replacement for `ls.tracing_context(parent=run_tree)`. |
|
|
@@ -304,8 +255,8 @@ async with struct.agent(name="my-agent"):
|
|
|
304
255
|
```
|
|
305
256
|
|
|
306
257
|
When you do use `ChatAnthropic` *and* have the `anthropic` SDK installed,
|
|
307
|
-
the chat span comes from the
|
|
308
|
-
|
|
258
|
+
the chat span comes from the LangChain layer (single span); the Anthropic
|
|
259
|
+
instrumentation skips its own to avoid duplicates.
|
|
309
260
|
|
|
310
261
|
## Content capture
|
|
311
262
|
|
|
@@ -317,21 +268,23 @@ from struct_sdk import struct, ContentCaptureMode
|
|
|
317
268
|
|
|
318
269
|
struct.init(
|
|
319
270
|
ingest_key=...,
|
|
320
|
-
content_capture=ContentCaptureMode.EVENT_ONLY, # default
|
|
321
|
-
# or ContentCaptureMode.
|
|
271
|
+
content_capture=ContentCaptureMode.EVENT_ONLY, # opt in; the default is NONE
|
|
272
|
+
# or ContentCaptureMode.SPAN_ONLY, SPAN_AND_EVENT
|
|
322
273
|
)
|
|
323
274
|
```
|
|
324
275
|
|
|
325
|
-
- **`EVENT_ONLY
|
|
276
|
+
- **`EVENT_ONLY`**: per-message content lands on OTel log
|
|
326
277
|
records (`gen_ai.{user,assistant,system,tool}.message`,
|
|
327
278
|
`gen_ai.choice`). Spans carry metadata only.
|
|
328
279
|
- **`SPAN_ONLY`**: content on span attributes (`gen_ai.input.messages`,
|
|
329
280
|
`gen_ai.output.messages`).
|
|
330
281
|
- **`SPAN_AND_EVENT`**: both.
|
|
331
|
-
- **`NONE
|
|
332
|
-
|
|
282
|
+
- **`NONE`** (default since 0.3.0): no content captured, including the
|
|
283
|
+
user prompt on the agent span and tool arguments and results. Token
|
|
284
|
+
counts, tool call IDs, finish reasons, and other metadata still flow.
|
|
333
285
|
|
|
334
|
-
`capture_content=False` is a shorthand for `ContentCaptureMode.NONE
|
|
286
|
+
`capture_content=False` is a shorthand for `ContentCaptureMode.NONE`, and
|
|
287
|
+
`capture_content=True` for `EVENT_ONLY`.
|
|
335
288
|
|
|
336
289
|
## Manual scopes
|
|
337
290
|
|
|
@@ -422,23 +375,175 @@ from `input_tokens`.
|
|
|
422
375
|
```python
|
|
423
376
|
struct.init(
|
|
424
377
|
ingest_key="pk-...", # required
|
|
425
|
-
service_name="my-agent", # default: "default-agent"
|
|
426
|
-
service_version="1.2.3", # default: "0.0.0"
|
|
427
|
-
environment="production", # default: "development"
|
|
378
|
+
service_name="my-agent", # default: OTEL_SERVICE_NAME, else "default-agent"
|
|
379
|
+
service_version="1.2.3", # default: from OTEL_RESOURCE_ATTRIBUTES, else "0.0.0"
|
|
380
|
+
environment="production", # default: from OTEL_RESOURCE_ATTRIBUTES, else "development"
|
|
428
381
|
endpoint="https://ingest.struct.ai", # default; override for self-hosted
|
|
429
382
|
shutdown_timeout_seconds=5.0, # default: 5.0
|
|
430
|
-
content_capture=ContentCaptureMode.EVENT_ONLY,
|
|
383
|
+
content_capture=ContentCaptureMode.EVENT_ONLY, # default: NONE
|
|
384
|
+
auto_instrument=True, # default; False = AI libraries only
|
|
385
|
+
instrumentation_options=None, # per-library full-app settings
|
|
386
|
+
redact_attributes=["*.password"], # default: STRUCT_REDACT_ATTRIBUTES
|
|
387
|
+
)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
With `auto_instrument=False` the SDK uses an isolated `TracerProvider` and
|
|
391
|
+
`LoggerProvider` and instruments only the AI libraries; your existing OTel
|
|
392
|
+
setup is unaffected. The default is [full-app mode](#full-app-mode).
|
|
393
|
+
|
|
394
|
+
Service identity follows the standard OpenTelemetry variables when you don't
|
|
395
|
+
pass it: `OTEL_SERVICE_NAME`, and `service.version` and
|
|
396
|
+
`deployment.environment` in `OTEL_RESOURCE_ATTRIBUTES`. Arguments to `init()`
|
|
397
|
+
win. The resource also carries host, runtime and (with the `[auto]` extra)
|
|
398
|
+
container metadata, plus the commit when `STRUCT_GIT_COMMIT_SHA` and
|
|
399
|
+
`STRUCT_GIT_REPOSITORY_URL` are set. It includes a `service.instance.id` so
|
|
400
|
+
replicas stay distinguishable; an ID from `OTEL_RESOURCE_ATTRIBUTES` is
|
|
401
|
+
preserved.
|
|
402
|
+
|
|
403
|
+
In pre-fork servers (for example `gunicorn --preload`), call `struct.init()` in
|
|
404
|
+
each worker (gunicorn's `post_fork` hook), not in the parent: forked workers
|
|
405
|
+
would otherwise share the parent's instance id. A worker that exports after
|
|
406
|
+
being forked from an initialized parent logs one INFO line saying so.
|
|
407
|
+
|
|
408
|
+
### Redaction
|
|
409
|
+
|
|
410
|
+
`redact_attributes` takes glob patterns, matched case-insensitively. Span,
|
|
411
|
+
span-event and log attributes whose keys match leave the process as
|
|
412
|
+
`[REDACTED]`, for example `["http.request.header.authorization", "*.password"]`.
|
|
413
|
+
`STRUCT_REDACT_ATTRIBUTES` (comma-separated) sets them without code. Only what
|
|
414
|
+
Struct sends is changed; your own exporters still see the real values.
|
|
415
|
+
Metrics, log bodies and span names are not redacted.
|
|
416
|
+
|
|
417
|
+
## Full-app mode
|
|
418
|
+
|
|
419
|
+
Full-app mode is the default since 0.3.0: `struct.init()` adds APM-style
|
|
420
|
+
telemetry for the whole service, like an OpenTelemetry distro: HTTP servers and
|
|
421
|
+
clients, databases, caches, queues, app logs and process metrics, built on the
|
|
422
|
+
OpenTelemetry contrib instrumentors. Every supported library you have installed
|
|
423
|
+
is instrumented; turn individual ones off below. To keep the 0.2 behavior
|
|
424
|
+
(AI libraries only, isolated providers), pass `auto_instrument=False`.
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
pip install "struct-sdk[auto]" # needs opentelemetry-sdk 1.30+; add [azure] for Azure SDK tracing,
|
|
428
|
+
# [structlog] for structlog (opentelemetry-sdk 1.44+),
|
|
429
|
+
# [exceptions] for uncaught exceptions (1.43+)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
import fastapi
|
|
434
|
+
from struct_sdk import struct
|
|
435
|
+
|
|
436
|
+
struct.init(ingest_key="pk-...", service_name="checkout-api")
|
|
437
|
+
|
|
438
|
+
app = fastapi.FastAPI() # resolve the class after init()
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Framework instrumentors replace the framework's class when `init()` runs, so
|
|
442
|
+
an app class bound earlier stays uninstrumented: `from fastapi import FastAPI`
|
|
443
|
+
above `struct.init()` keeps the original class. Import framework classes after
|
|
444
|
+
`init()`, or reference them through the module as above. For an app created
|
|
445
|
+
before `init()`, call the contrib instrumentor directly (for example
|
|
446
|
+
`FastAPIInstrumentor.instrument_app(app)`). In pre-fork servers, run
|
|
447
|
+
`init()` in each worker, as described above.
|
|
448
|
+
|
|
449
|
+
| Signal | Coverage |
|
|
450
|
+
|---|---|
|
|
451
|
+
| Traces | Django, FastAPI, Flask, Starlette, Tornado, Falcon, Pyramid, aiohttp (client and server), requests, httpx, urllib, urllib3, gRPC, psycopg2, psycopg, aiopg, asyncpg, PyMySQL, MySQL Connector, mysqlclient, pymssql, sqlite3, PyMongo, Cassandra, Redis, pymemcache, Tortoise ORM, Celery, botocore, aiobotocore, Kafka (confluent-kafka, aiokafka, kafka-python), RabbitMQ (pika, aio-pika), Jinja2, Azure SDK. Work handed to threads and `ThreadPoolExecutor` stays in the caller's trace. SQLAlchemy is off by default because its query spans duplicate the database driver's; enable it with `{"sqlalchemy": {"enabled": True}}` if you prefer ORM-level spans. |
|
|
452
|
+
| Logs | Python `logging` records that reach the root logger, with trace context. The SDK's own and OpenTelemetry's records are excluded. With the `[structlog]` extra, structlog events that structlog writes itself (its default) are exported with their fields as attributes; events structlog hands to `logging` (`structlog.stdlib.LoggerFactory`) are exported once, through the `logging` bridge. Extras and structlog fields OpenTelemetry can't take as-is are exported as strings: dicts and mixed-type lists as JSON, everything else (UUIDs, datetimes, `None`, bytes) as its `str()`. Your other handlers still see the original values. With the `[exceptions]` extra, uncaught exceptions (main thread, other threads, asyncio) are exported as log records. If your app configures no logging, its warnings still print to stderr as they would without Struct. This is separate from `content_capture`, which only governs LLM content; disable `{"logging": {"enabled": False}}` and `{"structlog": {"enabled": False}}` (which also stops copies of your logs when Struct attaches to your OpenTelemetry setup), or redact attributes, if application logs can carry sensitive data. |
|
|
453
|
+
| Metrics | Instrumentor metrics (for example `http.server.duration`) and process metrics (`process.cpu.*`, `process.memory.*`, threads, file descriptors, GC). Counters and histograms use delta temporality. |
|
|
454
|
+
|
|
455
|
+
Each instrumentor activates only when its library is installed.
|
|
456
|
+
`instrumentation_options` disables or configures one by name; other keys are
|
|
457
|
+
passed to that instrumentor:
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
struct.init(
|
|
461
|
+
ingest_key="pk-...",
|
|
462
|
+
instrumentation_options={
|
|
463
|
+
"fastapi": {"excluded_urls": "health,ready"},
|
|
464
|
+
"logging": {"enabled": False},
|
|
465
|
+
},
|
|
466
|
+
)
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
As with `opentelemetry-instrument` and the Azure Monitor distro, the standard
|
|
470
|
+
`OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` environment variable also turns
|
|
471
|
+
libraries off (`OTEL_PYTHON_DISABLED_INSTRUMENTATIONS=requests,urllib3`, or
|
|
472
|
+
`*` for all of them). An explicit `instrumentation_options` entry takes
|
|
473
|
+
precedence over it.
|
|
474
|
+
|
|
475
|
+
Full-app mode installs Struct's providers as the global OpenTelemetry
|
|
476
|
+
providers only when no tracer, meter or logger provider is installed. If the
|
|
477
|
+
process already configured any of them (Azure Monitor, your own SDK setup),
|
|
478
|
+
Struct adds its exporters to your SDK tracer and logger providers and
|
|
479
|
+
instruments nothing: your setup keeps ownership of instrumentation, sampling
|
|
480
|
+
and shutdown, and your metrics are not sent to Struct. Struct's own GenAI
|
|
481
|
+
spans keep their isolated provider either way, so your sampler does not drop
|
|
482
|
+
them.
|
|
483
|
+
|
|
484
|
+
### Datadog ddtrace
|
|
485
|
+
|
|
486
|
+
Under `ddtrace-run`, Struct leaves the libraries ddtrace patches to it, and
|
|
487
|
+
`urllib3` beneath `requests` or `botocore`, to avoid duplicate spans and
|
|
488
|
+
conflicting trace headers, and logs one INFO line listing them. Logs, process metrics, thread context, AI spans and libraries
|
|
489
|
+
ddtrace doesn't patch still go to Struct. With `DD_TRACE_OTEL_ENABLED=true`,
|
|
490
|
+
ddtrace's OpenTelemetry provider isn't the OpenTelemetry SDK, so full-app
|
|
491
|
+
mode attaches nothing (an INFO line says so) and only AI spans reach Struct.
|
|
492
|
+
|
|
493
|
+
### Zero-code activation
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
OTEL_PYTHON_DISTRO=struct \
|
|
497
|
+
STRUCT_INGEST_KEY=pk-... \
|
|
498
|
+
OTEL_SERVICE_NAME=checkout-api \
|
|
499
|
+
opentelemetry-instrument python app.py
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
`opentelemetry-instrument` comes with `[auto]`. Struct calls `struct.init()`
|
|
503
|
+
before your app starts, with `STRUCT_INGEST_URL` as the endpoint when set.
|
|
504
|
+
Message content stays off; call `struct.init()` in code to turn it on.
|
|
505
|
+
Installing the SDK never changes another distro: Struct's distro is used only
|
|
506
|
+
when `OTEL_PYTHON_DISTRO=struct`. When selected, it also keeps other installed
|
|
507
|
+
configurators (such as `opentelemetry-distro`'s) from installing competing
|
|
508
|
+
providers, unless you set `OTEL_PYTHON_CONFIGURATOR` yourself. If you already run another distro, call
|
|
509
|
+
`struct.init()` in code instead and Struct attaches to its providers.
|
|
510
|
+
|
|
511
|
+
### Temporal
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
pip install "struct-sdk[temporal]"
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
from temporalio.client import Client
|
|
519
|
+
from temporalio.runtime import Runtime
|
|
520
|
+
from struct_sdk import struct, temporal
|
|
521
|
+
|
|
522
|
+
struct.init(ingest_key="pk-...", service_name="billing-worker")
|
|
523
|
+
runtime = Runtime(telemetry=temporal.telemetry_config(global_tags={"replica": replica_id}))
|
|
524
|
+
client = await Client.connect(
|
|
525
|
+
"temporal:7233",
|
|
526
|
+
runtime=runtime,
|
|
527
|
+
interceptors=[temporal.tracing_interceptor()],
|
|
431
528
|
)
|
|
432
529
|
```
|
|
433
530
|
|
|
434
|
-
|
|
435
|
-
|
|
531
|
+
`telemetry_config()` exports Temporal Core metrics (task slots,
|
|
532
|
+
schedule-to-start latency, poll results, sticky cache) to Struct. It keeps
|
|
533
|
+
Core's console logging at Temporal's defaults, except that failed metric
|
|
534
|
+
exports are not printed. `tracing_interceptor()` (temporalio 1.11+) traces
|
|
535
|
+
workflows and activities with Struct's tracer; a failing span processor never
|
|
536
|
+
blocks a Temporal call or replaces its exception.
|
|
537
|
+
Temporal server metrics are a Prometheus endpoint; scrape them with an
|
|
538
|
+
OpenTelemetry Collector and export OTLP/HTTP to your Struct ingest endpoint
|
|
539
|
+
with the `x-struct-ingest-key` header.
|
|
436
540
|
|
|
437
541
|
## Reliability
|
|
438
542
|
|
|
439
543
|
The SDK is designed never to break your application. Instrumentation
|
|
440
|
-
hooks, span exports, and shutdown all swallow exceptions internally
|
|
441
|
-
|
|
544
|
+
hooks, span exports, and shutdown all swallow exceptions internally and log
|
|
545
|
+
at DEBUG on the `struct_sdk` logger, with a traceback for the first failure
|
|
546
|
+
at each site.
|
|
442
547
|
Process exit is bounded by `shutdown_timeout_seconds` and runs in a
|
|
443
548
|
background thread, so a slow or unreachable ingest endpoint cannot hang
|
|
444
549
|
shutdown.
|
|
@@ -448,13 +553,12 @@ shutdown.
|
|
|
448
553
|
- **Spans missing after instrumenting:** Call `struct.init()` *before*
|
|
449
554
|
importing the instrumented libraries, so the SDK can wire up
|
|
450
555
|
instrumentation before any instance is constructed.
|
|
451
|
-
- **No log records appearing:**
|
|
452
|
-
mode is `EVENT_ONLY` or `SPAN_AND_EVENT
|
|
453
|
-
`capture_content=False`, content events are disabled.
|
|
556
|
+
- **No message log records appearing:** they only emit when the capture
|
|
557
|
+
mode is `EVENT_ONLY` or `SPAN_AND_EVENT`. The default is `NONE`.
|
|
454
558
|
- **Duplicate chat spans:** When an LLM provider SDK and a wrapping
|
|
455
559
|
framework are both instrumented (e.g. `ChatAnthropic` calling through
|
|
456
|
-
to `anthropic`), the
|
|
457
|
-
|
|
560
|
+
to `anthropic`), the provider-level chat span is skipped and the
|
|
561
|
+
framework's is kept.
|
|
458
562
|
|
|
459
563
|
## License
|
|
460
564
|
|