agnt5 0.3.0a8__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
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.
Potentially problematic release.
This version of agnt5 might be problematic. Click here for more details.
- agnt5/__init__.py +119 -0
- agnt5/_compat.py +16 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/_retry_utils.py +172 -0
- agnt5/_schema_utils.py +312 -0
- agnt5/_sentry.py +515 -0
- agnt5/_telemetry.py +191 -0
- agnt5/agent/__init__.py +48 -0
- agnt5/agent/context.py +458 -0
- agnt5/agent/core.py +1793 -0
- agnt5/agent/decorator.py +112 -0
- agnt5/agent/handoff.py +105 -0
- agnt5/agent/registry.py +68 -0
- agnt5/agent/result.py +39 -0
- agnt5/checkpoint.py +246 -0
- agnt5/client.py +1478 -0
- agnt5/context.py +210 -0
- agnt5/entity.py +1230 -0
- agnt5/events.py +566 -0
- agnt5/exceptions.py +102 -0
- agnt5/function.py +325 -0
- agnt5/lm.py +1033 -0
- agnt5/memory.py +521 -0
- agnt5/tool.py +657 -0
- agnt5/tracing.py +196 -0
- agnt5/types.py +110 -0
- agnt5/version.py +19 -0
- agnt5/worker.py +1982 -0
- agnt5/workflow.py +1584 -0
- agnt5-0.3.0a8.dist-info/METADATA +26 -0
- agnt5-0.3.0a8.dist-info/RECORD +32 -0
- agnt5-0.3.0a8.dist-info/WHEEL +5 -0
agnt5/_sentry.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""Sentry integration for AGNT5 SDK error tracking and monitoring.
|
|
2
|
+
|
|
3
|
+
This module provides automatic SDK error tracking to help improve AGNT5.
|
|
4
|
+
|
|
5
|
+
**Telemetry Behavior:**
|
|
6
|
+
- Alpha/Beta releases (e.g., 0.2.8a12, 1.0.0b3): Telemetry ENABLED by default
|
|
7
|
+
- Stable releases (e.g., 1.0.0, 2.1.3): Telemetry DISABLED by default
|
|
8
|
+
|
|
9
|
+
**What's Collected:**
|
|
10
|
+
- SDK initialization failures and crashes
|
|
11
|
+
- Rust FFI import errors
|
|
12
|
+
- Component registration failures
|
|
13
|
+
- Anonymized service metadata (no user code/data)
|
|
14
|
+
|
|
15
|
+
**Privacy:**
|
|
16
|
+
- Only SDK errors are captured (not your application errors)
|
|
17
|
+
- All data is anonymized (no secrets, IP addresses, or personal data)
|
|
18
|
+
- Full transparency in what's sent
|
|
19
|
+
|
|
20
|
+
Environment Variables:
|
|
21
|
+
AGNT5_DISABLE_SDK_TELEMETRY: Set to "true" to disable (for alpha/beta)
|
|
22
|
+
AGNT5_ENABLE_SDK_TELEMETRY: Set to "true" to enable (for stable)
|
|
23
|
+
AGNT5_SENTRY_ENVIRONMENT: Environment tag (default: "production")
|
|
24
|
+
AGNT5_SENTRY_TRACES_SAMPLE_RATE: APM trace sampling rate (default: 0.1)
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
# Disable telemetry in alpha/beta
|
|
28
|
+
export AGNT5_DISABLE_SDK_TELEMETRY="true"
|
|
29
|
+
|
|
30
|
+
# Enable telemetry in stable (to help AGNT5 team)
|
|
31
|
+
export AGNT5_ENABLE_SDK_TELEMETRY="true"
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
import os
|
|
36
|
+
import re
|
|
37
|
+
from typing import Any, Dict, Optional
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
# AGNT5-owned Sentry project for SDK error collection
|
|
42
|
+
# This DSN is hardcoded and sends SDK errors to the AGNT5 team
|
|
43
|
+
# Users can override for testing with AGNT5_SDK_SENTRY_DSN env var
|
|
44
|
+
AGNT5_SDK_SENTRY_DSN = os.getenv(
|
|
45
|
+
"AGNT5_SDK_SENTRY_DSN",
|
|
46
|
+
"https://a25fea6eeec2e8b393a77f1e2cc7fe2c@o4509047159521280.ingest.us.sentry.io/4509047294656512"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_sentry_initialized = False
|
|
50
|
+
_sentry_available = False
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
import sentry_sdk
|
|
54
|
+
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
55
|
+
|
|
56
|
+
_sentry_available = True
|
|
57
|
+
except ImportError:
|
|
58
|
+
logger.debug("sentry-sdk not installed, Sentry integration disabled")
|
|
59
|
+
_sentry_available = False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_sentry_enabled() -> bool:
|
|
63
|
+
"""Check if Sentry integration is enabled and initialized.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if Sentry is available and initialized, False otherwise
|
|
67
|
+
"""
|
|
68
|
+
return _sentry_initialized and _sentry_available
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_prerelease_version(version: str) -> bool:
|
|
72
|
+
"""Check if SDK version is alpha or beta (pre-release).
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
version: Version string (e.g., "0.2.8a12", "1.0.0b3", "1.2.3")
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if version contains 'a' (alpha) or 'b' (beta), False otherwise
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
>>> _is_prerelease_version("0.2.8a12")
|
|
82
|
+
True
|
|
83
|
+
>>> _is_prerelease_version("1.0.0b3")
|
|
84
|
+
True
|
|
85
|
+
>>> _is_prerelease_version("1.2.3")
|
|
86
|
+
False
|
|
87
|
+
>>> _is_prerelease_version("1.2.3rc1")
|
|
88
|
+
False
|
|
89
|
+
"""
|
|
90
|
+
# Match alpha (a) or beta (b) followed by digits after version number
|
|
91
|
+
# Pattern: <major>.<minor>.<patch>(a|b)<number>
|
|
92
|
+
# More robust: anchored to end and requires digits after a/b
|
|
93
|
+
return bool(re.search(r'\d+\.\d+\.\d+(a|b)\d+', version))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _should_enable_telemetry(sdk_version: str) -> bool:
|
|
97
|
+
"""Determine if SDK telemetry should be enabled based on version and env vars.
|
|
98
|
+
|
|
99
|
+
Default behavior:
|
|
100
|
+
- Alpha/Beta releases: ENABLED by default (users can opt-out)
|
|
101
|
+
- Stable releases: DISABLED by default (users can opt-in)
|
|
102
|
+
|
|
103
|
+
Environment variable overrides:
|
|
104
|
+
- AGNT5_DISABLE_SDK_TELEMETRY="true" → Force disable
|
|
105
|
+
- AGNT5_ENABLE_SDK_TELEMETRY="true" → Force enable
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
sdk_version: SDK version string
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if telemetry should be enabled, False otherwise
|
|
112
|
+
"""
|
|
113
|
+
# Check explicit disable flag (takes precedence)
|
|
114
|
+
disable_flag = os.getenv("AGNT5_DISABLE_SDK_TELEMETRY", "").lower()
|
|
115
|
+
if disable_flag in ("true", "1", "yes"):
|
|
116
|
+
logger.debug("SDK telemetry explicitly disabled via AGNT5_DISABLE_SDK_TELEMETRY")
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Check explicit enable flag
|
|
120
|
+
enable_flag = os.getenv("AGNT5_ENABLE_SDK_TELEMETRY", "").lower()
|
|
121
|
+
if enable_flag in ("true", "1", "yes"):
|
|
122
|
+
logger.debug("SDK telemetry explicitly enabled via AGNT5_ENABLE_SDK_TELEMETRY")
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
# Default behavior based on version
|
|
126
|
+
is_prerelease = _is_prerelease_version(sdk_version)
|
|
127
|
+
|
|
128
|
+
if is_prerelease:
|
|
129
|
+
logger.debug(f"SDK version {sdk_version} is pre-release → telemetry enabled by default")
|
|
130
|
+
return True
|
|
131
|
+
else:
|
|
132
|
+
logger.debug(f"SDK version {sdk_version} is stable → telemetry disabled by default")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _anonymize_event(event, hint):
|
|
137
|
+
"""Remove potentially sensitive data before sending to Sentry.
|
|
138
|
+
|
|
139
|
+
This ensures no user secrets, environment variables, or personal data
|
|
140
|
+
is sent to Sentry. This includes:
|
|
141
|
+
- IP addresses
|
|
142
|
+
- Environment variables
|
|
143
|
+
- Stack trace local variables (may contain API keys, passwords, etc.)
|
|
144
|
+
- Request data and headers
|
|
145
|
+
- Sensitive breadcrumb data
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
event: Sentry event dict
|
|
149
|
+
hint: Event hint with exception info
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Sanitized event or None to drop the event
|
|
153
|
+
"""
|
|
154
|
+
# Remove user IP address
|
|
155
|
+
if 'user' in event:
|
|
156
|
+
event['user'].pop('ip_address', None)
|
|
157
|
+
|
|
158
|
+
# Remove environment variables (might contain secrets)
|
|
159
|
+
if 'contexts' in event:
|
|
160
|
+
if 'os' in event['contexts']:
|
|
161
|
+
event['contexts']['os'].pop('env', None)
|
|
162
|
+
|
|
163
|
+
# Remove sensitive runtime context
|
|
164
|
+
if 'runtime' in event['contexts']:
|
|
165
|
+
event['contexts']['runtime'].pop('env', None)
|
|
166
|
+
|
|
167
|
+
# CRITICAL: Remove stack trace local variables (may contain secrets)
|
|
168
|
+
# Example: api_key = "sk-abc123..." in local scope
|
|
169
|
+
if 'exception' in event:
|
|
170
|
+
for exc in event['exception'].get('values', []):
|
|
171
|
+
if 'stacktrace' in exc:
|
|
172
|
+
for frame in exc['stacktrace'].get('frames', []):
|
|
173
|
+
# Remove all local variables from stack frames
|
|
174
|
+
if 'vars' in frame:
|
|
175
|
+
frame.pop('vars')
|
|
176
|
+
|
|
177
|
+
# Remove request data if present (may contain secrets in POST data)
|
|
178
|
+
if 'request' in event:
|
|
179
|
+
event['request'].pop('data', None)
|
|
180
|
+
event['request'].pop('env', None)
|
|
181
|
+
event['request'].pop('headers', None)
|
|
182
|
+
# Keep only safe request metadata
|
|
183
|
+
safe_request_keys = {'url', 'method', 'query_string'}
|
|
184
|
+
event['request'] = {k: v for k, v in event['request'].items() if k in safe_request_keys}
|
|
185
|
+
|
|
186
|
+
# Sanitize breadcrumbs (remove any data fields that might be sensitive)
|
|
187
|
+
if 'breadcrumbs' in event:
|
|
188
|
+
for crumb in event['breadcrumbs'].get('values', []):
|
|
189
|
+
if 'data' in crumb:
|
|
190
|
+
# Keep only safe metadata
|
|
191
|
+
safe_keys = {'category', 'level', 'message', 'timestamp', 'type'}
|
|
192
|
+
crumb['data'] = {k: v for k, v in crumb['data'].items() if k in safe_keys}
|
|
193
|
+
|
|
194
|
+
return event
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def initialize_sentry(
|
|
198
|
+
service_name: str,
|
|
199
|
+
service_version: str,
|
|
200
|
+
sdk_version: str,
|
|
201
|
+
environment: Optional[str] = None,
|
|
202
|
+
traces_sample_rate: Optional[float] = None,
|
|
203
|
+
) -> bool:
|
|
204
|
+
"""Initialize Sentry SDK for automatic SDK error tracking.
|
|
205
|
+
|
|
206
|
+
This function is idempotent - calling it multiple times will not reinitialize Sentry.
|
|
207
|
+
|
|
208
|
+
**Telemetry Behavior:**
|
|
209
|
+
- Alpha/Beta releases: ENABLED by default (opt-out with AGNT5_DISABLE_SDK_TELEMETRY=true)
|
|
210
|
+
- Stable releases: DISABLED by default (opt-in with AGNT5_ENABLE_SDK_TELEMETRY=true)
|
|
211
|
+
|
|
212
|
+
**What's Collected:**
|
|
213
|
+
- SDK initialization failures and crashes
|
|
214
|
+
- Component registration errors
|
|
215
|
+
- Anonymized metadata (no user code, secrets, or personal data)
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
service_name: Name of the service (used in event context)
|
|
219
|
+
service_version: Version of the service (used in event context)
|
|
220
|
+
sdk_version: AGNT5 SDK version (determines default telemetry behavior)
|
|
221
|
+
environment: Environment tag (if None, reads from AGNT5_SENTRY_ENVIRONMENT, defaults to "production")
|
|
222
|
+
traces_sample_rate: APM sampling rate 0.0-1.0 (if None, reads from AGNT5_SENTRY_TRACES_SAMPLE_RATE, defaults to 0.1)
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if Sentry was initialized, False if disabled or unavailable
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
>>> initialize_sentry("my-service", "1.0.0", "0.2.8a12")
|
|
229
|
+
True # Telemetry enabled (alpha version)
|
|
230
|
+
|
|
231
|
+
>>> initialize_sentry("my-service", "1.0.0", "1.0.0")
|
|
232
|
+
False # Telemetry disabled (stable version)
|
|
233
|
+
"""
|
|
234
|
+
global _sentry_initialized
|
|
235
|
+
|
|
236
|
+
# Check if already initialized
|
|
237
|
+
if _sentry_initialized:
|
|
238
|
+
logger.debug("Sentry already initialized, skipping")
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# Check if Sentry SDK is available
|
|
242
|
+
if not _sentry_available:
|
|
243
|
+
logger.debug("Sentry SDK not available, skipping initialization")
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
# Check if AGNT5 team has configured the DSN
|
|
247
|
+
if not AGNT5_SDK_SENTRY_DSN:
|
|
248
|
+
logger.debug("AGNT5_SDK_SENTRY_DSN not configured, telemetry disabled")
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
# Determine if telemetry should be enabled based on version and env vars
|
|
252
|
+
if not _should_enable_telemetry(sdk_version):
|
|
253
|
+
is_prerelease = _is_prerelease_version(sdk_version)
|
|
254
|
+
if is_prerelease:
|
|
255
|
+
logger.info(
|
|
256
|
+
f"SDK telemetry disabled for pre-release version {sdk_version} "
|
|
257
|
+
f"(set AGNT5_ENABLE_SDK_TELEMETRY=true to enable)"
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
logger.debug(
|
|
261
|
+
f"SDK telemetry disabled by default for stable version {sdk_version} "
|
|
262
|
+
f"(set AGNT5_ENABLE_SDK_TELEMETRY=true to help AGNT5 team)"
|
|
263
|
+
)
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
# Get environment and sampling rate
|
|
267
|
+
sentry_env = environment or os.getenv("AGNT5_SENTRY_ENVIRONMENT", "production")
|
|
268
|
+
sample_rate_str = os.getenv("AGNT5_SENTRY_TRACES_SAMPLE_RATE", "0.1")
|
|
269
|
+
if traces_sample_rate is None:
|
|
270
|
+
try:
|
|
271
|
+
traces_sample_rate = float(sample_rate_str)
|
|
272
|
+
except ValueError:
|
|
273
|
+
logger.warning(
|
|
274
|
+
f"Invalid AGNT5_SENTRY_TRACES_SAMPLE_RATE: {sample_rate_str}, using default 0.1"
|
|
275
|
+
)
|
|
276
|
+
traces_sample_rate = 0.1
|
|
277
|
+
|
|
278
|
+
# Configure logging integration
|
|
279
|
+
# Capture ERROR and above automatically
|
|
280
|
+
logging_integration = LoggingIntegration(
|
|
281
|
+
level=logging.INFO, # Capture info and above as breadcrumbs
|
|
282
|
+
event_level=logging.ERROR, # Send errors and above as events
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# Initialize Sentry SDK with AGNT5's hardcoded DSN
|
|
287
|
+
sentry_sdk.init(
|
|
288
|
+
dsn=AGNT5_SDK_SENTRY_DSN, # Hardcoded AGNT5 Sentry project
|
|
289
|
+
environment=sentry_env,
|
|
290
|
+
release=f"agnt5-python-sdk@{sdk_version}", # SDK version, not service version
|
|
291
|
+
traces_sample_rate=traces_sample_rate,
|
|
292
|
+
integrations=[logging_integration],
|
|
293
|
+
# Anonymize all events before sending
|
|
294
|
+
before_send=_anonymize_event,
|
|
295
|
+
# Add default tags
|
|
296
|
+
default_integrations=True,
|
|
297
|
+
# Enable performance monitoring
|
|
298
|
+
enable_tracing=True,
|
|
299
|
+
# Attach stack traces to messages
|
|
300
|
+
attach_stacktrace=True,
|
|
301
|
+
# Max breadcrumbs to keep
|
|
302
|
+
max_breadcrumbs=50,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Set global tags for filtering
|
|
306
|
+
sentry_sdk.set_tag("sdk_version", sdk_version)
|
|
307
|
+
sentry_sdk.set_tag("sdk_component", "python")
|
|
308
|
+
sentry_sdk.set_tag("is_prerelease", str(_is_prerelease_version(sdk_version)))
|
|
309
|
+
|
|
310
|
+
_sentry_initialized = True
|
|
311
|
+
|
|
312
|
+
# Log different messages based on version
|
|
313
|
+
is_prerelease = _is_prerelease_version(sdk_version)
|
|
314
|
+
if is_prerelease:
|
|
315
|
+
logger.info(
|
|
316
|
+
f"SDK telemetry enabled for alpha/beta version {sdk_version} "
|
|
317
|
+
f"(helps AGNT5 team find bugs). To disable: export AGNT5_DISABLE_SDK_TELEMETRY=true"
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
logger.info(
|
|
321
|
+
f"SDK telemetry enabled for version {sdk_version} (thank you for helping improve AGNT5!)"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return True
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error(f"Failed to initialize SDK telemetry: {e}", exc_info=True)
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def capture_exception(
|
|
332
|
+
exception: Exception,
|
|
333
|
+
context: Optional[Dict[str, Any]] = None,
|
|
334
|
+
tags: Optional[Dict[str, str]] = None,
|
|
335
|
+
level: str = "error",
|
|
336
|
+
) -> Optional[str]:
|
|
337
|
+
"""Capture an exception and send it to Sentry.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
exception: The exception to capture
|
|
341
|
+
context: Additional context data to attach
|
|
342
|
+
tags: Tags to add to this event
|
|
343
|
+
level: Severity level (error, warning, info)
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Event ID if captured, None if Sentry not initialized
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
>>> try:
|
|
350
|
+
... risky_operation()
|
|
351
|
+
... except Exception as e:
|
|
352
|
+
... capture_exception(e, context={"run_id": "123"}, tags={"component": "workflow"})
|
|
353
|
+
"""
|
|
354
|
+
if not is_sentry_enabled():
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
with sentry_sdk.push_scope() as scope:
|
|
358
|
+
# Add tags
|
|
359
|
+
if tags:
|
|
360
|
+
for key, value in tags.items():
|
|
361
|
+
scope.set_tag(key, value)
|
|
362
|
+
|
|
363
|
+
# Add context
|
|
364
|
+
if context:
|
|
365
|
+
scope.set_context("additional_context", context)
|
|
366
|
+
|
|
367
|
+
# Set level
|
|
368
|
+
scope.level = level
|
|
369
|
+
|
|
370
|
+
# Capture exception
|
|
371
|
+
event_id = sentry_sdk.capture_exception(exception)
|
|
372
|
+
return event_id
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def capture_message(
|
|
376
|
+
message: str,
|
|
377
|
+
level: str = "info",
|
|
378
|
+
context: Optional[Dict[str, Any]] = None,
|
|
379
|
+
tags: Optional[Dict[str, str]] = None,
|
|
380
|
+
) -> Optional[str]:
|
|
381
|
+
"""Capture a message and send it to Sentry.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
message: The message to capture
|
|
385
|
+
level: Severity level (error, warning, info, debug)
|
|
386
|
+
context: Additional context data to attach
|
|
387
|
+
tags: Tags to add to this event
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Event ID if captured, None if Sentry not initialized
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
>>> capture_message("Unusual behavior detected", level="warning", tags={"component": "agent"})
|
|
394
|
+
"""
|
|
395
|
+
if not is_sentry_enabled():
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
with sentry_sdk.push_scope() as scope:
|
|
399
|
+
# Add tags
|
|
400
|
+
if tags:
|
|
401
|
+
for key, value in tags.items():
|
|
402
|
+
scope.set_tag(key, value)
|
|
403
|
+
|
|
404
|
+
# Add context
|
|
405
|
+
if context:
|
|
406
|
+
scope.set_context("additional_context", context)
|
|
407
|
+
|
|
408
|
+
# Set level
|
|
409
|
+
scope.level = level
|
|
410
|
+
|
|
411
|
+
# Capture message
|
|
412
|
+
event_id = sentry_sdk.capture_message(message, level=level)
|
|
413
|
+
return event_id
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def add_breadcrumb(
|
|
417
|
+
message: str,
|
|
418
|
+
category: str = "default",
|
|
419
|
+
level: str = "info",
|
|
420
|
+
data: Optional[Dict[str, Any]] = None,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""Add a breadcrumb to the current scope.
|
|
423
|
+
|
|
424
|
+
Breadcrumbs are a trail of events that led up to an error.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
message: Breadcrumb message
|
|
428
|
+
category: Breadcrumb category (e.g., "execution", "state", "api")
|
|
429
|
+
level: Severity level
|
|
430
|
+
data: Additional data
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
>>> add_breadcrumb("Starting workflow execution", category="workflow", data={"workflow_id": "123"})
|
|
434
|
+
"""
|
|
435
|
+
if not is_sentry_enabled():
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
sentry_sdk.add_breadcrumb(
|
|
439
|
+
message=message,
|
|
440
|
+
category=category,
|
|
441
|
+
level=level,
|
|
442
|
+
data=data or {},
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def set_user(user_id: Optional[str] = None, **kwargs: Any) -> None:
|
|
447
|
+
"""Set user information for the current scope.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
user_id: User ID
|
|
451
|
+
**kwargs: Additional user attributes (email, username, etc.)
|
|
452
|
+
|
|
453
|
+
Example:
|
|
454
|
+
>>> set_user(user_id="user123", email="user@example.com")
|
|
455
|
+
"""
|
|
456
|
+
if not is_sentry_enabled():
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
user_data = {}
|
|
460
|
+
if user_id:
|
|
461
|
+
user_data["id"] = user_id
|
|
462
|
+
user_data.update(kwargs)
|
|
463
|
+
|
|
464
|
+
sentry_sdk.set_user(user_data)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def set_context(name: str, context: Dict[str, Any]) -> None:
|
|
468
|
+
"""Set context information for the current scope.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
name: Context name (e.g., "runtime", "execution")
|
|
472
|
+
context: Context data
|
|
473
|
+
|
|
474
|
+
Example:
|
|
475
|
+
>>> set_context("runtime", {"run_id": "123", "tenant_id": "tenant456"})
|
|
476
|
+
"""
|
|
477
|
+
if not is_sentry_enabled():
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
sentry_sdk.set_context(name, context)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def set_tag(key: str, value: str) -> None:
|
|
484
|
+
"""Set a tag for the current scope.
|
|
485
|
+
|
|
486
|
+
Tags are searchable key-value pairs.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
key: Tag key
|
|
490
|
+
value: Tag value
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
>>> set_tag("component_type", "workflow")
|
|
494
|
+
"""
|
|
495
|
+
if not is_sentry_enabled():
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
sentry_sdk.set_tag(key, value)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def flush(timeout: float = 2.0) -> None:
|
|
502
|
+
"""Flush pending Sentry events.
|
|
503
|
+
|
|
504
|
+
This should be called before shutdown to ensure all events are sent.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
timeout: Maximum time to wait in seconds
|
|
508
|
+
|
|
509
|
+
Example:
|
|
510
|
+
>>> flush(timeout=5.0)
|
|
511
|
+
"""
|
|
512
|
+
if not is_sentry_enabled():
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
sentry_sdk.flush(timeout=timeout)
|
agnt5/_telemetry.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenTelemetry integration for Python logging.
|
|
3
|
+
|
|
4
|
+
This module bridges Python's standard logging to Rust's tracing/OpenTelemetry system,
|
|
5
|
+
ensuring all logs from ctx.logger are sent to both the console and OTLP exporters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenTelemetryHandler(logging.Handler):
|
|
13
|
+
"""
|
|
14
|
+
Custom logging handler that forwards Python logs to Rust OpenTelemetry system.
|
|
15
|
+
|
|
16
|
+
This handler routes all Python log records through the Rust `log_from_python()`
|
|
17
|
+
function, which integrates with the tracing ecosystem. This ensures:
|
|
18
|
+
|
|
19
|
+
1. Logs are sent to OpenTelemetry OTLP exporter
|
|
20
|
+
2. Logs appear in console output (via Rust's fmt layer)
|
|
21
|
+
3. Logs inherit span context (invocation.id, trace_id, etc.)
|
|
22
|
+
4. Structured logging with proper attributes
|
|
23
|
+
|
|
24
|
+
The Rust side handles both console output and OTLP export, so we only
|
|
25
|
+
need one handler on the Python side.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, level=logging.NOTSET):
|
|
29
|
+
"""Initialize the OpenTelemetry handler.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
level: Minimum log level to process (default: NOTSET processes all)
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(level)
|
|
35
|
+
|
|
36
|
+
# Import Rust bridge function
|
|
37
|
+
try:
|
|
38
|
+
from ._core import log_from_python
|
|
39
|
+
self._log_from_python = log_from_python
|
|
40
|
+
except ImportError as e:
|
|
41
|
+
# Fallback if Rust core not available (development/testing)
|
|
42
|
+
import warnings
|
|
43
|
+
warnings.warn(
|
|
44
|
+
f"Failed to import Rust telemetry bridge: {e}. "
|
|
45
|
+
"Logs will not be sent to OpenTelemetry.",
|
|
46
|
+
RuntimeWarning
|
|
47
|
+
)
|
|
48
|
+
self._log_from_python = None
|
|
49
|
+
|
|
50
|
+
def emit(self, record: logging.LogRecord):
|
|
51
|
+
"""
|
|
52
|
+
Process a log record and forward to Rust telemetry.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
record: Python logging record to process
|
|
56
|
+
"""
|
|
57
|
+
if self._log_from_python is None:
|
|
58
|
+
# No Rust bridge available, silently skip
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Filter out gRPC internal logs to avoid noise
|
|
62
|
+
# These are low-level HTTP/2 protocol logs that aren't useful for application debugging
|
|
63
|
+
if record.name.startswith(('grpc.', 'h2.', '_grpc_', 'h2-')):
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Format the message (applies any formatters)
|
|
68
|
+
message = self.format(record)
|
|
69
|
+
|
|
70
|
+
# Include exception traceback if present (from logger.exception() or exc_info=True)
|
|
71
|
+
if record.exc_info:
|
|
72
|
+
# Use formatter to format the exception, or fall back to basic formatting
|
|
73
|
+
if self.formatter:
|
|
74
|
+
exc_text = self.formatter.formatException(record.exc_info)
|
|
75
|
+
else:
|
|
76
|
+
# Fallback: use basic traceback formatting
|
|
77
|
+
import traceback
|
|
78
|
+
exc_text = ''.join(traceback.format_exception(*record.exc_info))
|
|
79
|
+
message = f"{message}\n{exc_text}"
|
|
80
|
+
|
|
81
|
+
# Extract correlation IDs from LogRecord attributes (added by _CorrelationFilter)
|
|
82
|
+
# These ensure logs can be correlated with distributed traces in observability backends
|
|
83
|
+
trace_id = getattr(record, 'trace_id', None)
|
|
84
|
+
span_id = getattr(record, 'span_id', None)
|
|
85
|
+
run_id = getattr(record, 'run_id', None)
|
|
86
|
+
|
|
87
|
+
# Extract streaming context for real-time SSE delivery
|
|
88
|
+
is_streaming = getattr(record, 'is_streaming', None)
|
|
89
|
+
tenant_id = getattr(record, 'tenant_id', None)
|
|
90
|
+
deployment_id = getattr(record, 'deployment_id', None)
|
|
91
|
+
|
|
92
|
+
# Forward to Rust tracing system
|
|
93
|
+
# Rust side will:
|
|
94
|
+
# - Add to current span context (inherits invocation.id)
|
|
95
|
+
# - Attach correlation IDs as span attributes for OTLP export
|
|
96
|
+
# - Send to OTLP exporter with trace context
|
|
97
|
+
# - Print to console via fmt layer
|
|
98
|
+
# - Export to journal for SSE streaming if is_streaming=True
|
|
99
|
+
self._log_from_python(
|
|
100
|
+
level=record.levelname,
|
|
101
|
+
message=message,
|
|
102
|
+
target=record.name,
|
|
103
|
+
module_path=record.module,
|
|
104
|
+
filename=record.pathname,
|
|
105
|
+
line=record.lineno,
|
|
106
|
+
trace_id=trace_id,
|
|
107
|
+
span_id=span_id,
|
|
108
|
+
run_id=run_id,
|
|
109
|
+
is_streaming=is_streaming,
|
|
110
|
+
tenant_id=tenant_id,
|
|
111
|
+
deployment_id=deployment_id,
|
|
112
|
+
)
|
|
113
|
+
except Exception:
|
|
114
|
+
# Don't let logging errors crash the application
|
|
115
|
+
# Use handleError to report the issue via logging system
|
|
116
|
+
self.handleError(record)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def setup_context_logger(logger: logging.Logger, log_level: Optional[int] = None) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Configure a Context logger with OpenTelemetry integration.
|
|
122
|
+
|
|
123
|
+
This function:
|
|
124
|
+
1. Removes any existing handlers (avoid duplicates)
|
|
125
|
+
2. Adds OpenTelemetry handler for OTLP + console output (when Worker is running)
|
|
126
|
+
3. Adds console handler for local testing (fallback)
|
|
127
|
+
4. Sets appropriate log level
|
|
128
|
+
5. Disables propagation to avoid duplicate logs
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
logger: Logger instance to configure
|
|
132
|
+
log_level: Optional log level (default: DEBUG)
|
|
133
|
+
"""
|
|
134
|
+
# Remove existing handlers to avoid duplicate logs
|
|
135
|
+
logger.handlers.clear()
|
|
136
|
+
|
|
137
|
+
# Add OpenTelemetry handler (for Worker/platform execution)
|
|
138
|
+
otel_handler = OpenTelemetryHandler()
|
|
139
|
+
otel_handler.setLevel(logging.DEBUG)
|
|
140
|
+
|
|
141
|
+
# Use simple formatter - Rust side handles structured logging
|
|
142
|
+
formatter = logging.Formatter('%(message)s')
|
|
143
|
+
otel_handler.setFormatter(formatter)
|
|
144
|
+
|
|
145
|
+
logger.addHandler(otel_handler)
|
|
146
|
+
|
|
147
|
+
# Add console handler for local testing (fallback when Rust bridge not available)
|
|
148
|
+
# This ensures logs appear when testing functions locally without Worker
|
|
149
|
+
console_handler = logging.StreamHandler()
|
|
150
|
+
console_handler.setLevel(logging.DEBUG)
|
|
151
|
+
|
|
152
|
+
# Console format includes level, message, and exception info if present
|
|
153
|
+
# exc_info=True in the format string means "include traceback if present"
|
|
154
|
+
console_formatter = logging.Formatter(
|
|
155
|
+
'[%(levelname)s] %(message)s',
|
|
156
|
+
# Python automatically appends exception traceback when exc_info is set
|
|
157
|
+
)
|
|
158
|
+
console_handler.setFormatter(console_formatter)
|
|
159
|
+
|
|
160
|
+
logger.addHandler(console_handler)
|
|
161
|
+
|
|
162
|
+
# Set log level (default to DEBUG to let handlers filter)
|
|
163
|
+
if log_level is None:
|
|
164
|
+
log_level = logging.DEBUG
|
|
165
|
+
logger.setLevel(log_level)
|
|
166
|
+
|
|
167
|
+
# Don't propagate to root logger (we handle everything ourselves)
|
|
168
|
+
logger.propagate = False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def setup_module_logger(module_name: str, log_level: Optional[int] = None) -> logging.Logger:
|
|
172
|
+
"""
|
|
173
|
+
Create and configure a logger for a module with OpenTelemetry integration.
|
|
174
|
+
|
|
175
|
+
Convenience function for setting up loggers in SDK modules.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
module_name: Name of the module (e.g., "agnt5.worker")
|
|
179
|
+
log_level: Optional log level (default: INFO for modules)
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Configured logger instance
|
|
183
|
+
"""
|
|
184
|
+
logger = logging.getLogger(module_name)
|
|
185
|
+
|
|
186
|
+
# For module loggers, default to INFO level
|
|
187
|
+
if log_level is None:
|
|
188
|
+
log_level = logging.INFO
|
|
189
|
+
|
|
190
|
+
setup_context_logger(logger, log_level)
|
|
191
|
+
return logger
|