struct-sdk 0.2.33__tar.gz → 0.3.2__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.3.2/PKG-INFO +703 -0
- struct_sdk-0.2.33/PKG-INFO → struct_sdk-0.3.2/README.md +213 -73
- struct_sdk-0.3.2/pyproject.toml +198 -0
- struct_sdk-0.3.2/src/struct_sdk/_auto.py +724 -0
- struct_sdk-0.3.2/src/struct_sdk/_distro.py +65 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/src/struct_sdk/_genai_content.py +87 -31
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/src/struct_sdk/anthropic.py +6 -4
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/src/struct_sdk/core.py +567 -58
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/src/struct_sdk/langchain.py +1 -1
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/src/struct_sdk/openai.py +4 -2
- struct_sdk-0.3.2/src/struct_sdk/pydantic_ai.py +914 -0
- struct_sdk-0.3.2/src/struct_sdk/temporal.py +119 -0
- struct_sdk-0.2.33/README.md +0 -412
- struct_sdk-0.2.33/pyproject.toml +0 -113
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/.gitignore +0 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/LICENSE +0 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/src/struct_sdk/__init__.py +0 -0
- {struct_sdk-0.2.33 → struct_sdk-0.3.2}/src/struct_sdk/claude_agent.py +0 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: struct-sdk
|
|
3
|
+
Version: 0.3.2
|
|
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: pydantic-ai
|
|
96
|
+
Requires-Dist: pydantic-ai-slim>=2.53.0; extra == 'pydantic-ai'
|
|
97
|
+
Provides-Extra: structlog
|
|
98
|
+
Requires-Dist: opentelemetry-instrumentation-structlog; extra == 'structlog'
|
|
99
|
+
Provides-Extra: temporal
|
|
100
|
+
Requires-Dist: temporalio>=1.11.0; extra == 'temporal'
|
|
101
|
+
Description-Content-Type: text/markdown
|
|
102
|
+
|
|
103
|
+
# struct-sdk
|
|
104
|
+
|
|
105
|
+
OpenTelemetry instrumentation for AI agents in Python. Captures spans,
|
|
106
|
+
token usage, and message events from the Anthropic SDK, the OpenAI SDK
|
|
107
|
+
(Responses API), pydantic-ai, the Claude Agent SDK, and LangChain /
|
|
108
|
+
LangGraph, and exports them to [struct.ai](https://struct.ai) for observability.
|
|
109
|
+
|
|
110
|
+
A TypeScript version is available as
|
|
111
|
+
[`@struct-ai/sdk`](https://www.npmjs.com/package/@struct-ai/sdk). Both
|
|
112
|
+
SDKs produce structurally identical traces, so a single agent system can
|
|
113
|
+
mix languages without a fragmented view.
|
|
114
|
+
|
|
115
|
+
## Install
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install struct-sdk
|
|
119
|
+
# optional — the SDK auto-instruments these if present
|
|
120
|
+
pip install anthropic
|
|
121
|
+
pip install openai
|
|
122
|
+
pip install pydantic-ai-slim
|
|
123
|
+
pip install claude-agent-sdk
|
|
124
|
+
pip install langchain-core langgraph
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Requires Python 3.10+.
|
|
128
|
+
|
|
129
|
+
## Quickstart
|
|
130
|
+
|
|
131
|
+
Get an ingest key from
|
|
132
|
+
[app.struct.ai/settings?tab=ingest-keys](https://app.struct.ai/settings?tab=ingest-keys),
|
|
133
|
+
then call `struct.init()` once at startup and wrap your agent loop:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import os
|
|
137
|
+
from struct_sdk import struct
|
|
138
|
+
|
|
139
|
+
struct.init(
|
|
140
|
+
ingest_key=os.environ["STRUCT_INGEST_KEY"], # or pass the string directly
|
|
141
|
+
service_name="my-agent",
|
|
142
|
+
environment="production",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
import anthropic
|
|
146
|
+
client = anthropic.AsyncAnthropic()
|
|
147
|
+
|
|
148
|
+
# Decorate each tool — auto-captures arguments + result + tool_call_id.
|
|
149
|
+
@struct.tool()
|
|
150
|
+
async def search(query: str):
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
async with struct.agent(name="checkout"):
|
|
154
|
+
msg = await client.messages.create(
|
|
155
|
+
model="claude-3-5-sonnet-20241022",
|
|
156
|
+
max_tokens=1024,
|
|
157
|
+
messages=[{"role": "user", "content": "plan my checkout flow"}],
|
|
158
|
+
)
|
|
159
|
+
result = await search(query="...")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## What gets traced
|
|
163
|
+
|
|
164
|
+
| Library | Span type | Notes |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `anthropic` | `chat {model}` | Cache-token accounting; streaming chats with tool-use reconstruction. Bedrock and Vertex variants supported if installed. |
|
|
167
|
+
| `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). |
|
|
168
|
+
| `pydantic_ai` | `invoke_agent {agent}`, `chat {model}`, `execute_tool {tool}` | pydantic-ai's own instrumentation, turned on with Struct's tracer provider. See [pydantic-ai](#pydantic-ai). |
|
|
169
|
+
| `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. |
|
|
170
|
+
| `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. |
|
|
171
|
+
| `langchain_core` `BaseTool` | `execute_tool {name}` | `tool_call_id` extracted from the LangChain ToolCall when present. |
|
|
172
|
+
| `langchain_core` `BaseRetriever` | `retrieval {name}` | |
|
|
173
|
+
| `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)`. |
|
|
174
|
+
|
|
175
|
+
## Framework integration
|
|
176
|
+
|
|
177
|
+
`struct.init()` takes the same parameters regardless of which framework
|
|
178
|
+
you're instrumenting. Required: `ingest_key` (get one at
|
|
179
|
+
[app.struct.ai/settings?tab=ingest-keys](https://app.struct.ai/settings?tab=ingest-keys)).
|
|
180
|
+
Recommended: `service_name`, `environment`.
|
|
181
|
+
|
|
182
|
+
What you need to do beyond `init()` depends on whether you're using an
|
|
183
|
+
**agent framework** (which has built-in concepts of agents and tools) or
|
|
184
|
+
an **LLM SDK directly** (which only knows about chat completions). The
|
|
185
|
+
SDK auto-instruments both, but only agent frameworks get full agent +
|
|
186
|
+
tool spans for free — when you call an LLM SDK directly, you have to
|
|
187
|
+
tell the SDK where the agent and tool boundaries are.
|
|
188
|
+
|
|
189
|
+
Call `init()` once, as early as possible, *before* the instrumented
|
|
190
|
+
libraries are imported.
|
|
191
|
+
|
|
192
|
+
### Agent frameworks — fully auto-instrumented
|
|
193
|
+
|
|
194
|
+
For these, calling `struct.init()` is the only setup. Agent, tool, chat,
|
|
195
|
+
and retrieval spans all emit automatically.
|
|
196
|
+
|
|
197
|
+
#### Claude Agent SDK
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from struct_sdk import struct
|
|
201
|
+
struct.init(ingest_key="pk-...", service_name="claude-agent")
|
|
202
|
+
|
|
203
|
+
from claude_agent_sdk import ClaudeAgentOptions, query
|
|
204
|
+
# Telemetry is generated by Claude Code itself in the subprocess and
|
|
205
|
+
# exported directly via OTLP. struct.init() configures the ingest
|
|
206
|
+
# credentials on ClaudeAgentOptions; subagents inherit them automatically.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### pydantic-ai
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from struct_sdk import struct
|
|
213
|
+
struct.init(ingest_key="pk-...", service_name="support-agent")
|
|
214
|
+
|
|
215
|
+
from pydantic_ai import Agent
|
|
216
|
+
agent = Agent("openai:gpt-5.5", name="support")
|
|
217
|
+
result = await agent.run("Refund order 42", conversation_id="thread-42")
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
pydantic-ai already emits OpenTelemetry GenAI spans; `struct.init()` turns
|
|
221
|
+
that on for every agent (`Agent.instrument_all`) with Struct's tracer
|
|
222
|
+
provider, unless you already set your own default (for example with
|
|
223
|
+
logfire). You get pydantic-ai's `invoke_agent`, `chat` and `execute_tool`
|
|
224
|
+
spans, with one `chat` span per model call (Struct's OpenAI and Anthropic
|
|
225
|
+
patches stay out of the way for pydantic-ai's own provider calls; model
|
|
226
|
+
calls your code makes while reading a stream keep their spans). During agent
|
|
227
|
+
runs, Struct guards telemetry faults and caps content. Realtime sessions and
|
|
228
|
+
direct `InstrumentedModel` exception recording are outside this guarantee.
|
|
229
|
+
|
|
230
|
+
- **Content** goes on span attributes only (pydantic-ai has no message
|
|
231
|
+
events): `span_only` / `span_and_event` include prompts, answers and tool
|
|
232
|
+
arguments; `event_only` and `none` do not. Tool definitions are always
|
|
233
|
+
recorded.
|
|
234
|
+
- **Sessions:** on Struct's provider, a `struct.agent(session_id=...)`
|
|
235
|
+
around the run wins; otherwise pydantic-ai's `conversation_id` is used.
|
|
236
|
+
Instrumentation configured on your own provider keeps its conversation IDs.
|
|
237
|
+
- **Turn it off** with
|
|
238
|
+
`struct.init(..., instrumentation_options={"pydantic_ai": {"enabled": False}})`,
|
|
239
|
+
or for one agent with `agent.instrument = False`. An
|
|
240
|
+
`Agent.instrument_all(False)` before `struct.init()` is pydantic-ai's
|
|
241
|
+
initial value, so it does not keep pydantic-ai off.
|
|
242
|
+
|
|
243
|
+
#### LangChain / LangGraph (with an agent or graph)
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from struct_sdk import struct
|
|
247
|
+
struct.init(ingest_key="pk-...", service_name="my-graph")
|
|
248
|
+
|
|
249
|
+
from langgraph.prebuilt import create_react_agent
|
|
250
|
+
# Pregel / CompiledStateGraph / AgentExecutor invocations get invoke_agent
|
|
251
|
+
# spans. BaseChatModel calls get chat spans. BaseTool.invoke gets
|
|
252
|
+
# execute_tool spans. BaseRetriever.invoke gets retrieval spans.
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
##### Recommended pattern: wrap LangChain entry points in `struct.agent`
|
|
256
|
+
|
|
257
|
+
For multi-turn HTTP-style usage (every request continues the same
|
|
258
|
+
conversation), wrap your request handler in
|
|
259
|
+
`struct.agent(session_id=conversation_id)`. This is the struct-native
|
|
260
|
+
replacement for `with ls.tracing_context(parent=run_tree):` and gives
|
|
261
|
+
you two things you can't get from `configurable.thread_id` alone:
|
|
262
|
+
|
|
263
|
+
1. **Threading without per-call config plumbing.** Every nested
|
|
264
|
+
LangChain call inherits the conversation id via the SDK's ambient
|
|
265
|
+
contextvar — you don't have to ensure each `compiled_graph.ainvoke`
|
|
266
|
+
gets `thread_id` on its config.
|
|
267
|
+
2. **One trace per request.** `struct.agent` creates a parent OTel span
|
|
268
|
+
so all the LangChain work for the request nests under one trace
|
|
269
|
+
(clean tree, "Subagents" / "Spawned by" UI links work). Without it,
|
|
270
|
+
each `.invoke()` becomes its own root trace, and the UI's session
|
|
271
|
+
list shows a non-deterministic agent name (`omni_agent`,
|
|
272
|
+
`LangGraph`, the first sub-agent it sees…).
|
|
273
|
+
|
|
274
|
+
Migrating from LangSmith:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
# Before — LangSmith convention, fragments under struct-sdk
|
|
278
|
+
with ls.tracing_context(parent=run_tree):
|
|
279
|
+
await orchestrator.ainvoke(inputs, config=config)
|
|
280
|
+
|
|
281
|
+
# After — struct-native, threads correctly, no langsmith dep
|
|
282
|
+
async with struct.agent(session_id=conversation_id):
|
|
283
|
+
await orchestrator.ainvoke(inputs, config=config)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Also works as a sync context manager (`with struct.agent(...)`) for
|
|
287
|
+
non-async handlers.
|
|
288
|
+
|
|
289
|
+
### LLM SDKs used directly — manual agent + tool scopes required
|
|
290
|
+
|
|
291
|
+
When you call an LLM SDK directly (no agent framework wrapping it), only
|
|
292
|
+
`chat` spans emit automatically. You need to wrap your agent loop in
|
|
293
|
+
`struct.agent()` and each tool execution in `struct.tool()` so the SDK
|
|
294
|
+
knows where to put the agent and tool boundaries — otherwise you'll see
|
|
295
|
+
free-floating chat spans with no agent or tool context around them.
|
|
296
|
+
|
|
297
|
+
#### Anthropic SDK (raw)
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from struct_sdk import struct
|
|
301
|
+
struct.init(ingest_key="pk-...", service_name="checkout-agent")
|
|
302
|
+
|
|
303
|
+
import anthropic
|
|
304
|
+
client = anthropic.AsyncAnthropic()
|
|
305
|
+
|
|
306
|
+
# Recommended: define each tool as a function and DECORATE it. The decorator
|
|
307
|
+
# auto-captures the tool's arguments + result on the execute_tool span and
|
|
308
|
+
# auto-fills tool_call_id from the preceding Anthropic response — no manual
|
|
309
|
+
# bookkeeping.
|
|
310
|
+
@struct.tool()
|
|
311
|
+
async def search(query: str):
|
|
312
|
+
...
|
|
313
|
+
|
|
314
|
+
# Required: wrap the agent loop yourself.
|
|
315
|
+
async with struct.agent(name="checkout"):
|
|
316
|
+
msg = await client.messages.create(
|
|
317
|
+
model="claude-3-5-sonnet-20241022",
|
|
318
|
+
max_tokens=1024,
|
|
319
|
+
messages=[...],
|
|
320
|
+
)
|
|
321
|
+
# Dispatching a decorated tool inside the agent emits a fully-populated
|
|
322
|
+
# execute_tool span (name, id, arguments, result):
|
|
323
|
+
result = await search(query="...")
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
For **dynamic dispatch** (the LLM picks a tool from a registry at runtime),
|
|
327
|
+
apply the decorator at runtime — still automatic, just bind the name when you
|
|
328
|
+
wrap the callable:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
registry = {t.name: struct.tool(name=t.name)(t.execute) for t in tools}
|
|
332
|
+
result = await registry[block.name](**block.input) # arguments + result captured
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
> `struct.tool()` can also be used as a context manager
|
|
336
|
+
> (`async with struct.tool(name=...): ...`) to instrument an arbitrary block of
|
|
337
|
+
> code as a tool span. That form is a **manual escape hatch** — it does NOT
|
|
338
|
+
> auto-capture arguments/result (a `with` block can't see the body's return
|
|
339
|
+
> value), so prefer the decorator for actual tool calls. See
|
|
340
|
+
> [Parallel tool calls](#parallel-tool-calls--pass-tool_call_id-explicitly) for
|
|
341
|
+
> the one runtime value (`tool_call_id`) you must supply under concurrency.
|
|
342
|
+
|
|
343
|
+
`anthropic.Anthropic`, `anthropic.AsyncAnthropic`, and the bedrock/vertex
|
|
344
|
+
clients are all auto-instrumented for chat spans.
|
|
345
|
+
|
|
346
|
+
#### Parallel tool calls — pass `tool_call_id` explicitly
|
|
347
|
+
|
|
348
|
+
When you execute an assistant turn's tool calls **sequentially** — one
|
|
349
|
+
`await` at a time, in the order the `tool_use` blocks appear — `struct.tool()`
|
|
350
|
+
auto-fills `gen_ai.tool.call.id` by matching each span to the next pending
|
|
351
|
+
`tool_use` of the same tool name. Nothing extra to do.
|
|
352
|
+
|
|
353
|
+
When you execute them **concurrently** (e.g. `asyncio.gather`), that
|
|
354
|
+
name-and-order matching is ambiguous: two `struct.tool(name="search")` spans
|
|
355
|
+
can start in any order, so the auto-fill may attach the wrong id (and thus the
|
|
356
|
+
wrong arguments/result) to a call. In that case **pass `tool_call_id`
|
|
357
|
+
explicitly** from the originating `tool_use` block — an explicit id always
|
|
358
|
+
overrides the auto-linkage:
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
async def run_one(block):
|
|
362
|
+
# The id from THIS block overrides the name/order auto-fill.
|
|
363
|
+
async with struct.tool(name=block.name, tool_call_id=block.id):
|
|
364
|
+
return await dispatch(block.name, **block.input)
|
|
365
|
+
|
|
366
|
+
# Concurrent execution — each tool span still carries the correct id.
|
|
367
|
+
results = await asyncio.gather(*[run_one(b) for b in tool_use_blocks])
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Rule of thumb: **serial tool execution → automatic; concurrent tool execution
|
|
371
|
+
→ provide `tool_call_id=` yourself.** (Auto-instrumented frameworks such as
|
|
372
|
+
LangChain read the id from the framework's `ToolCall`, so this only applies
|
|
373
|
+
when you drive the tool loop directly against an LLM SDK.)
|
|
374
|
+
|
|
375
|
+
#### LangChain `BaseChatModel` (no agent/graph)
|
|
376
|
+
|
|
377
|
+
If you call `ChatAnthropic.invoke(...)` (or any other `BaseChatModel`)
|
|
378
|
+
without wrapping it in an `AgentExecutor` or LangGraph, only the chat
|
|
379
|
+
span emits automatically. Same rule as raw Anthropic — wrap your agent
|
|
380
|
+
loop in `struct.agent()` and tool execution in `struct.tool()`.
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
from struct_sdk import struct
|
|
384
|
+
struct.init(ingest_key="pk-...", service_name="my-agent")
|
|
385
|
+
|
|
386
|
+
from langchain_anthropic import ChatAnthropic
|
|
387
|
+
llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
|
|
388
|
+
|
|
389
|
+
async with struct.agent(name="my-agent"):
|
|
390
|
+
response = await llm.ainvoke([("user", "...")])
|
|
391
|
+
async with struct.tool(name="search"):
|
|
392
|
+
...
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
When you do use `ChatAnthropic` *and* have the `anthropic` SDK installed,
|
|
396
|
+
the chat span comes from the LangChain layer (single span); the Anthropic
|
|
397
|
+
instrumentation skips its own to avoid duplicates.
|
|
398
|
+
|
|
399
|
+
## Content capture
|
|
400
|
+
|
|
401
|
+
The SDK supports four capture modes controlling how prompt/response
|
|
402
|
+
content is emitted.
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
from struct_sdk import struct, ContentCaptureMode
|
|
406
|
+
|
|
407
|
+
struct.init(
|
|
408
|
+
ingest_key=...,
|
|
409
|
+
content_capture=ContentCaptureMode.EVENT_ONLY, # opt in; the default is NONE
|
|
410
|
+
# or ContentCaptureMode.SPAN_ONLY, SPAN_AND_EVENT
|
|
411
|
+
)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
- **`EVENT_ONLY`**: per-message content lands on OTel log
|
|
415
|
+
records (`gen_ai.{user,assistant,system,tool}.message`,
|
|
416
|
+
`gen_ai.choice`). Spans carry metadata only.
|
|
417
|
+
- **`SPAN_ONLY`**: content on span attributes (`gen_ai.input.messages`,
|
|
418
|
+
`gen_ai.output.messages`).
|
|
419
|
+
- **`SPAN_AND_EVENT`**: both.
|
|
420
|
+
- **`NONE`** (default since 0.3.0): no content captured, including the
|
|
421
|
+
user prompt on the agent span and tool arguments and results. Token
|
|
422
|
+
counts, tool call IDs, finish reasons, and other metadata still flow.
|
|
423
|
+
|
|
424
|
+
`capture_content=False` is a shorthand for `ContentCaptureMode.NONE`, and
|
|
425
|
+
`capture_content=True` for `EVENT_ONLY`.
|
|
426
|
+
|
|
427
|
+
## Manual scopes
|
|
428
|
+
|
|
429
|
+
`struct.agent()` and `struct.tool()` create `invoke_agent` and
|
|
430
|
+
`execute_tool` spans. Use them when you call an LLM SDK directly; you
|
|
431
|
+
don't need them when an agent framework (LangGraph, AgentExecutor,
|
|
432
|
+
Claude Agent SDK) is already creating those spans for you.
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
async with struct.agent(
|
|
436
|
+
name="onboarding",
|
|
437
|
+
session_id=conversation_id,
|
|
438
|
+
metadata={"tenant": "acme"},
|
|
439
|
+
):
|
|
440
|
+
async with struct.tool(name="fetch-profile"):
|
|
441
|
+
return await fetch_profile()
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Decorator form:
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
@struct.agent(name="checkout")
|
|
448
|
+
async def run_checkout(order_id: str):
|
|
449
|
+
@struct.tool(name="charge-card")
|
|
450
|
+
async def charge():
|
|
451
|
+
return await stripe.charge(order_id)
|
|
452
|
+
|
|
453
|
+
return await charge()
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Sub-agents (e.g. a `create_agent` graph invoked from inside another
|
|
457
|
+
agent's tool body) record their parent via the
|
|
458
|
+
`struct.agent.parent_session_id` attribute on the inner `invoke_agent`
|
|
459
|
+
span. This powers the UI's "Spawned by" backlink, which works for any
|
|
460
|
+
nested invocation.
|
|
461
|
+
|
|
462
|
+
The parent's "Subagents" forward list — the inverse direction —
|
|
463
|
+
additionally requires that the nested invoke shares the outer agent's
|
|
464
|
+
trace. This works automatically when the outer tool is built with the
|
|
465
|
+
`@tool` decorator (or any callback-aware wrapping) since the nested
|
|
466
|
+
invoke inherits the parent's run state. Bare `Tool(...)` constructors
|
|
467
|
+
that bypass the callback chain can break the forward link; the
|
|
468
|
+
backlink still renders.
|
|
469
|
+
|
|
470
|
+
## Semantic conventions
|
|
471
|
+
|
|
472
|
+
Emits attributes per the OTel GenAI semantic conventions:
|
|
473
|
+
|
|
474
|
+
- `gen_ai.operation.name` — `chat`, `execute_tool`, `invoke_agent`, `retrieval`
|
|
475
|
+
- `gen_ai.provider.name` — the GenAI provider on inference spans (`anthropic`, `openai`, …); platform-routed calls report the platform value (`aws.bedrock`, `gcp.vertex_ai`, `azure.ai.openai`) when detectable from the client. `invoke_agent` spans inherit the provider from their first child inference call (omitted when no model is reached); `execute_tool`/`retrieval` spans carry no provider.
|
|
476
|
+
- `gen_ai.request.{model, max_tokens, temperature, top_p, top_k, stop_sequences}`
|
|
477
|
+
- `gen_ai.response.{model, id, finish_reasons}`
|
|
478
|
+
- `gen_ai.usage.{input_tokens, output_tokens, cache_read.input_tokens, cache_creation.input_tokens}`
|
|
479
|
+
- `gen_ai.conversation.id`
|
|
480
|
+
- `gen_ai.tool.{name, call.id, call.arguments, call.result}`
|
|
481
|
+
- `error.type` + `StatusCode.ERROR` on failures
|
|
482
|
+
|
|
483
|
+
### `error.type` values emitted
|
|
484
|
+
|
|
485
|
+
The OTel conventions ask instrumentations to document the error values
|
|
486
|
+
they report ("Instrumentations SHOULD document the list of errors they
|
|
487
|
+
report"). This SDK emits exactly two kinds of value, both alongside
|
|
488
|
+
span `StatusCode.ERROR`:
|
|
489
|
+
|
|
490
|
+
| Value | When | Meaning |
|
|
491
|
+
| --- | --- | --- |
|
|
492
|
+
| exception class name (e.g. `RuntimeError`, `httpx.ConnectError`) | the instrumented call raised | We failed to execute the request. Also records an OTel exception event. |
|
|
493
|
+
| `tool_error` | an `execute_tool` span whose result signalled failure **in band** — Anthropic `tool_result` blocks with `is_error: true`, MCP `CallToolResult.isError`, or a LangChain `ToolMessage` with `status="error"` | The tool ran and reported a failure back to the model (bad arguments, a domain "no"), so the model can self-correct. No exception object exists, so no class name is available. |
|
|
494
|
+
|
|
495
|
+
`tool_error` is a deliberate low-cardinality sentinel, which the
|
|
496
|
+
`error.type` convention explicitly permits ("another low-cardinality
|
|
497
|
+
error identifier"; a custom value MAY be used where no well-known one
|
|
498
|
+
applies). The same split is used by OpenTelemetry's MCP instrumentation
|
|
499
|
+
in OpenLLMetry, which likewise reports `error.type="tool_error"` for the
|
|
500
|
+
`isError` path and the exception class name otherwise.
|
|
501
|
+
|
|
502
|
+
The distinction is what lets a monitor page on genuine execution
|
|
503
|
+
failures while excluding failures the model already saw and can recover
|
|
504
|
+
from.
|
|
505
|
+
|
|
506
|
+
Note: `gen_ai.usage.input_tokens` for Anthropic is the true total — the
|
|
507
|
+
SDK adds back `cache_read_input_tokens` and
|
|
508
|
+
`cache_creation_input_tokens`, which Anthropic's raw response excludes
|
|
509
|
+
from `input_tokens`.
|
|
510
|
+
|
|
511
|
+
## Configuration
|
|
512
|
+
|
|
513
|
+
```python
|
|
514
|
+
struct.init(
|
|
515
|
+
ingest_key="pk-...", # required
|
|
516
|
+
service_name="my-agent", # default: OTEL_SERVICE_NAME, else "default-agent"
|
|
517
|
+
service_version="1.2.3", # default: from OTEL_RESOURCE_ATTRIBUTES, else "0.0.0"
|
|
518
|
+
environment="production", # default: from OTEL_RESOURCE_ATTRIBUTES, else "development"
|
|
519
|
+
endpoint="https://ingest.struct.ai", # default; override for self-hosted
|
|
520
|
+
shutdown_timeout_seconds=5.0, # default: 5.0
|
|
521
|
+
content_capture=ContentCaptureMode.EVENT_ONLY, # default: NONE
|
|
522
|
+
auto_instrument=True, # default; False = AI libraries only
|
|
523
|
+
instrumentation_options=None, # per-library full-app settings
|
|
524
|
+
redact_attributes=["*.password"], # default: STRUCT_REDACT_ATTRIBUTES
|
|
525
|
+
)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
With `auto_instrument=False` the SDK uses an isolated `TracerProvider` and
|
|
529
|
+
`LoggerProvider` and instruments only the AI libraries; your existing OTel
|
|
530
|
+
setup is unaffected. The default is [full-app mode](#full-app-mode).
|
|
531
|
+
|
|
532
|
+
Service identity follows the standard OpenTelemetry variables when you don't
|
|
533
|
+
pass it: `OTEL_SERVICE_NAME`, and `service.version` and
|
|
534
|
+
`deployment.environment` in `OTEL_RESOURCE_ATTRIBUTES`. Arguments to `init()`
|
|
535
|
+
win. The resource also carries host, runtime and (with the `[auto]` extra)
|
|
536
|
+
container metadata, plus the commit when `STRUCT_GIT_COMMIT_SHA` and
|
|
537
|
+
`STRUCT_GIT_REPOSITORY_URL` are set. It includes a `service.instance.id` so
|
|
538
|
+
replicas stay distinguishable; an ID from `OTEL_RESOURCE_ATTRIBUTES` is
|
|
539
|
+
preserved.
|
|
540
|
+
|
|
541
|
+
In pre-fork servers (for example `gunicorn --preload`), call `struct.init()` in
|
|
542
|
+
each worker (gunicorn's `post_fork` hook), not in the parent: forked workers
|
|
543
|
+
would otherwise share the parent's instance id. A worker that exports after
|
|
544
|
+
being forked from an initialized parent logs one INFO line saying so.
|
|
545
|
+
|
|
546
|
+
### Redaction
|
|
547
|
+
|
|
548
|
+
`redact_attributes` takes glob patterns, matched case-insensitively. Span,
|
|
549
|
+
span-event and log attributes whose keys match leave the process as
|
|
550
|
+
`[REDACTED]`, for example `["http.request.header.authorization", "*.password"]`.
|
|
551
|
+
`STRUCT_REDACT_ATTRIBUTES` (comma-separated) sets them without code. Only what
|
|
552
|
+
Struct sends is changed; your own exporters still see the real values.
|
|
553
|
+
Metrics, log bodies and span names are not redacted.
|
|
554
|
+
|
|
555
|
+
## Full-app mode
|
|
556
|
+
|
|
557
|
+
Full-app mode is the default since 0.3.0: `struct.init()` adds APM-style
|
|
558
|
+
telemetry for the whole service, like an OpenTelemetry distro: HTTP servers and
|
|
559
|
+
clients, databases, caches, queues, app logs and process metrics, built on the
|
|
560
|
+
OpenTelemetry contrib instrumentors. Every supported library you have installed
|
|
561
|
+
is instrumented; turn individual ones off below. To keep the 0.2 behavior
|
|
562
|
+
(AI libraries only, isolated providers), pass `auto_instrument=False`.
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
pip install "struct-sdk[auto]" # needs opentelemetry-sdk 1.30+; add [azure] for Azure SDK tracing,
|
|
566
|
+
# [structlog] for structlog (opentelemetry-sdk 1.44+),
|
|
567
|
+
# [exceptions] for uncaught exceptions (1.43+)
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
import fastapi
|
|
572
|
+
from struct_sdk import struct
|
|
573
|
+
|
|
574
|
+
struct.init(ingest_key="pk-...", service_name="checkout-api")
|
|
575
|
+
|
|
576
|
+
app = fastapi.FastAPI() # resolve the class after init()
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Framework instrumentors replace the framework's class when `init()` runs, so
|
|
580
|
+
an app class bound earlier stays uninstrumented: `from fastapi import FastAPI`
|
|
581
|
+
above `struct.init()` keeps the original class. Import framework classes after
|
|
582
|
+
`init()`, or reference them through the module as above. For an app created
|
|
583
|
+
before `init()`, call the contrib instrumentor directly (for example
|
|
584
|
+
`FastAPIInstrumentor.instrument_app(app)`). In pre-fork servers, run
|
|
585
|
+
`init()` in each worker, as described above.
|
|
586
|
+
|
|
587
|
+
| Signal | Coverage |
|
|
588
|
+
|---|---|
|
|
589
|
+
| 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. |
|
|
590
|
+
| 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. |
|
|
591
|
+
| 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. |
|
|
592
|
+
|
|
593
|
+
Each instrumentor activates only when its library is installed.
|
|
594
|
+
`instrumentation_options` disables or configures one by name; other keys are
|
|
595
|
+
passed to that instrumentor:
|
|
596
|
+
|
|
597
|
+
```python
|
|
598
|
+
struct.init(
|
|
599
|
+
ingest_key="pk-...",
|
|
600
|
+
instrumentation_options={
|
|
601
|
+
"fastapi": {"excluded_urls": "health,ready"},
|
|
602
|
+
"logging": {"enabled": False},
|
|
603
|
+
},
|
|
604
|
+
)
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
As with `opentelemetry-instrument` and the Azure Monitor distro, the standard
|
|
608
|
+
`OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` environment variable also turns
|
|
609
|
+
libraries off (`OTEL_PYTHON_DISABLED_INSTRUMENTATIONS=requests,urllib3`, or
|
|
610
|
+
`*` for all of them). An explicit `instrumentation_options` entry takes
|
|
611
|
+
precedence over it.
|
|
612
|
+
|
|
613
|
+
Full-app mode installs Struct's providers as the global OpenTelemetry
|
|
614
|
+
providers only when no tracer, meter or logger provider is installed. If the
|
|
615
|
+
process already configured any of them (Azure Monitor, your own SDK setup),
|
|
616
|
+
Struct adds its exporters to your SDK tracer and logger providers and
|
|
617
|
+
instruments nothing: your setup keeps ownership of instrumentation, sampling
|
|
618
|
+
and shutdown, and your metrics are not sent to Struct. Struct's own GenAI
|
|
619
|
+
spans keep their isolated provider either way, so your sampler does not drop
|
|
620
|
+
them.
|
|
621
|
+
|
|
622
|
+
### Datadog ddtrace
|
|
623
|
+
|
|
624
|
+
Under `ddtrace-run`, Struct leaves the libraries ddtrace patches to it, and
|
|
625
|
+
`urllib3` beneath `requests` or `botocore`, to avoid duplicate spans and
|
|
626
|
+
conflicting trace headers, and logs one INFO line listing them. Logs, process metrics, thread context, AI spans and libraries
|
|
627
|
+
ddtrace doesn't patch still go to Struct. With `DD_TRACE_OTEL_ENABLED=true`,
|
|
628
|
+
ddtrace's OpenTelemetry provider isn't the OpenTelemetry SDK, so full-app
|
|
629
|
+
mode attaches nothing (an INFO line says so) and only AI spans reach Struct.
|
|
630
|
+
|
|
631
|
+
### Zero-code activation
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
OTEL_PYTHON_DISTRO=struct \
|
|
635
|
+
STRUCT_INGEST_KEY=pk-... \
|
|
636
|
+
OTEL_SERVICE_NAME=checkout-api \
|
|
637
|
+
opentelemetry-instrument python app.py
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
`opentelemetry-instrument` comes with `[auto]`. Struct calls `struct.init()`
|
|
641
|
+
before your app starts, with `STRUCT_INGEST_URL` as the endpoint when set.
|
|
642
|
+
Message content stays off; call `struct.init()` in code to turn it on.
|
|
643
|
+
Installing the SDK never changes another distro: Struct's distro is used only
|
|
644
|
+
when `OTEL_PYTHON_DISTRO=struct`. When selected, it also keeps other installed
|
|
645
|
+
configurators (such as `opentelemetry-distro`'s) from installing competing
|
|
646
|
+
providers, unless you set `OTEL_PYTHON_CONFIGURATOR` yourself. If you already run another distro, call
|
|
647
|
+
`struct.init()` in code instead and Struct attaches to its providers.
|
|
648
|
+
|
|
649
|
+
### Temporal
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
pip install "struct-sdk[temporal]"
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
from temporalio.client import Client
|
|
657
|
+
from temporalio.runtime import Runtime
|
|
658
|
+
from struct_sdk import struct, temporal
|
|
659
|
+
|
|
660
|
+
struct.init(ingest_key="pk-...", service_name="billing-worker")
|
|
661
|
+
runtime = Runtime(telemetry=temporal.telemetry_config(global_tags={"replica": replica_id}))
|
|
662
|
+
client = await Client.connect(
|
|
663
|
+
"temporal:7233",
|
|
664
|
+
runtime=runtime,
|
|
665
|
+
interceptors=[temporal.tracing_interceptor()],
|
|
666
|
+
)
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
`telemetry_config()` exports Temporal Core metrics (task slots,
|
|
670
|
+
schedule-to-start latency, poll results, sticky cache) to Struct. It keeps
|
|
671
|
+
Core's console logging at Temporal's defaults, except that failed metric
|
|
672
|
+
exports are not printed. `tracing_interceptor()` (temporalio 1.11+) traces
|
|
673
|
+
workflows and activities with Struct's tracer; a failing span processor never
|
|
674
|
+
blocks a Temporal call or replaces its exception.
|
|
675
|
+
Temporal server metrics are a Prometheus endpoint; scrape them with an
|
|
676
|
+
OpenTelemetry Collector and export OTLP/HTTP to your Struct ingest endpoint
|
|
677
|
+
with the `x-struct-ingest-key` header.
|
|
678
|
+
|
|
679
|
+
## Reliability
|
|
680
|
+
|
|
681
|
+
The SDK is designed never to break your application. Instrumentation
|
|
682
|
+
hooks, span exports, and shutdown all swallow exceptions internally and log
|
|
683
|
+
at DEBUG on the `struct_sdk` logger, with a traceback for the first failure
|
|
684
|
+
at each site.
|
|
685
|
+
Process exit is bounded by `shutdown_timeout_seconds` and runs in a
|
|
686
|
+
background thread, so a slow or unreachable ingest endpoint cannot hang
|
|
687
|
+
shutdown.
|
|
688
|
+
|
|
689
|
+
## Troubleshooting
|
|
690
|
+
|
|
691
|
+
- **Spans missing after instrumenting:** Call `struct.init()` *before*
|
|
692
|
+
importing the instrumented libraries, so the SDK can wire up
|
|
693
|
+
instrumentation before any instance is constructed.
|
|
694
|
+
- **No message log records appearing:** they only emit when the capture
|
|
695
|
+
mode is `EVENT_ONLY` or `SPAN_AND_EVENT`. The default is `NONE`.
|
|
696
|
+
- **Duplicate chat spans:** When an LLM provider SDK and a wrapping
|
|
697
|
+
framework are both instrumented (e.g. `ChatAnthropic` calling through
|
|
698
|
+
to `anthropic`), the provider-level chat span is skipped and the
|
|
699
|
+
framework's is kept.
|
|
700
|
+
|
|
701
|
+
## License
|
|
702
|
+
|
|
703
|
+
Apache-2.0
|