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/_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