auratrace-sdk 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: auratrace-sdk
3
+ Version: 2.0.0
4
+ Summary: Official Python Telemetry & AI Diagnostic SDK for AuraTrace Autonomous Backend Diagnostics
5
+ Author-email: Sunil Singh <sunilsinghrajput192@gmail.com>
6
+ Project-URL: Homepage, https://github.com/sunilsingh175/auratrace
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx>=0.24.0
10
+
11
+ # Trace Python SDK
12
+
13
+ Official Python telemetry and unhandled crash diagnostics SDK for Trace.
14
+
15
+ ## Quickstart
16
+
17
+ ```python
18
+ from trace_sdk import Trace, TraceMiddleware
19
+
20
+ # Initialize Trace Client
21
+ trace = Trace(
22
+ service_id="payment-service",
23
+ api_key="trace_payment_secret_456",
24
+ endpoint="http://localhost:8000"
25
+ )
26
+
27
+ # 1. Log metrics & structured messages
28
+ trace.info("User checkout initiated", latency_ms=45.2, metadata={"user_id": "usr_99"})
29
+
30
+ # 2. Capture and report caught exceptions
31
+ try:
32
+ process_payment()
33
+ except Exception as e:
34
+ trace.capture_exception(e, message="Payment processing failure")
35
+
36
+ # 3. Use as a FastAPI / Starlette middleware
37
+ # Automatically records latency and reports unhandled exceptions
38
+ app.add_middleware(TraceMiddleware, client=trace)
39
+ ```
@@ -0,0 +1,29 @@
1
+ # Trace Python SDK
2
+
3
+ Official Python telemetry and unhandled crash diagnostics SDK for Trace.
4
+
5
+ ## Quickstart
6
+
7
+ ```python
8
+ from trace_sdk import Trace, TraceMiddleware
9
+
10
+ # Initialize Trace Client
11
+ trace = Trace(
12
+ service_id="payment-service",
13
+ api_key="trace_payment_secret_456",
14
+ endpoint="http://localhost:8000"
15
+ )
16
+
17
+ # 1. Log metrics & structured messages
18
+ trace.info("User checkout initiated", latency_ms=45.2, metadata={"user_id": "usr_99"})
19
+
20
+ # 2. Capture and report caught exceptions
21
+ try:
22
+ process_payment()
23
+ except Exception as e:
24
+ trace.capture_exception(e, message="Payment processing failure")
25
+
26
+ # 3. Use as a FastAPI / Starlette middleware
27
+ # Automatically records latency and reports unhandled exceptions
28
+ app.add_middleware(TraceMiddleware, client=trace)
29
+ ```
@@ -0,0 +1,57 @@
1
+ """
2
+ AutoTrace Python SDK
3
+ """
4
+
5
+ from typing import Optional
6
+ from .client import AutoTrace, AuraTrace, TraceClient, Trace, AutomaticBackendDetection
7
+
8
+ _default_client: Optional[AutoTrace] = None
9
+
10
+ def init(
11
+ api_key: Optional[str] = None,
12
+ service_name: Optional[str] = None,
13
+ endpoint: Optional[str] = None,
14
+ environment: Optional[str] = None,
15
+ version: Optional[str] = None,
16
+ batch_size: int = 50,
17
+ flush_interval_seconds: float = 1.0,
18
+ install_global_hook: bool = True,
19
+ ) -> AutoTrace:
20
+ global _default_client
21
+ _default_client = AutoTrace(
22
+ api_key=api_key,
23
+ service_name=service_name,
24
+ endpoint=endpoint,
25
+ environment=environment,
26
+ version=version,
27
+ batch_size=batch_size,
28
+ flush_interval_seconds=flush_interval_seconds,
29
+ install_global_hook=install_global_hook,
30
+ )
31
+ return _default_client
32
+
33
+ def capture_message(message: str, metadata: Optional[dict] = None) -> None:
34
+ if _default_client is None:
35
+ init()
36
+ _default_client.capture_message(message, metadata=metadata)
37
+
38
+ def capture_exception(exception: BaseException, metadata: Optional[dict] = None) -> None:
39
+ if _default_client is None:
40
+ init()
41
+ _default_client.capture_exception(exception, metadata=metadata)
42
+
43
+ def flush() -> None:
44
+ if _default_client is not None:
45
+ _default_client.flush()
46
+
47
+ __all__ = [
48
+ "AutoTrace",
49
+ "AuraTrace",
50
+ "TraceClient",
51
+ "Trace",
52
+ "AutomaticBackendDetection",
53
+ "init",
54
+ "capture_message",
55
+ "capture_exception",
56
+ "flush",
57
+ ]
@@ -0,0 +1,265 @@
1
+ """
2
+ AuraTrace Python SDK
3
+ Zero-configuration telemetry, automatic service discovery, and AI root cause capture.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import time
9
+ import queue
10
+ import threading
11
+ import traceback
12
+ import json
13
+ from datetime import datetime, timezone
14
+ from typing import Optional, Dict, Any, List
15
+ import urllib.request
16
+ import urllib.error
17
+
18
+ try:
19
+ import httpx
20
+ except ImportError:
21
+ httpx = None
22
+
23
+
24
+ def auto_detect_service_name() -> str:
25
+ """Auto-detects the service identifier from environment variables or running script."""
26
+ name = (
27
+ os.getenv("AUTOTRACE_SERVICE_NAME")
28
+ or os.getenv("AURATRACE_SERVICE_NAME")
29
+ or os.getenv("SERVICE_NAME")
30
+ or os.getenv("APP_NAME")
31
+ )
32
+ if name:
33
+ return name.strip()
34
+
35
+ if sys.argv and sys.argv[0]:
36
+ base = os.path.basename(sys.argv[0])
37
+ clean = os.path.splitext(base)[0]
38
+ if clean and clean not in ("python", "python3", "pytest", "uvicorn", "gunicorn", "__main__", "-c"):
39
+ return clean
40
+
41
+ def _sanitize_stack_trace(trace_str: str) -> str:
42
+ if not trace_str:
43
+ return ""
44
+ import re
45
+ def _clean_path(match):
46
+ full_path = match.group(1).replace("\\", "/")
47
+ parts = full_path.split("/")
48
+ for marker in ["scripts", "app", "backend", "services", "controllers", "models", "workers"]:
49
+ if marker in parts:
50
+ idx = parts.index(marker)
51
+ return f'File "{"/".join(parts[idx:])}"'
52
+ if len(parts) > 1:
53
+ return f'File "{"/".join(parts[-2:])}"'
54
+ return f'File "{parts[-1]}"'
55
+ return re.sub(r'File "([^"]+)"', _clean_path, trace_str)
56
+
57
+
58
+ class AutoTrace:
59
+ def __init__(
60
+ self,
61
+ api_key: Optional[str] = None,
62
+ service_name: Optional[str] = None,
63
+ endpoint: Optional[str] = None,
64
+ environment: Optional[str] = None,
65
+ version: Optional[str] = None,
66
+ batch_size: int = 50,
67
+ flush_interval_seconds: float = 1.0,
68
+ install_global_hook: bool = True,
69
+ ):
70
+ self.api_key = (
71
+ api_key
72
+ or os.getenv("AUTOTRACE_API_KEY")
73
+ or os.getenv("AURATRACE_API_KEY")
74
+ or os.getenv("AURA_MASTER_API_KEY")
75
+ or ""
76
+ )
77
+ self.service_name = service_name or auto_detect_service_name()
78
+ self.endpoint = (
79
+ endpoint
80
+ or os.getenv("AUTOTRACE_ENDPOINT")
81
+ or os.getenv("AURATRACE_ENDPOINT")
82
+ or "http://localhost:8000"
83
+ ).rstrip("/")
84
+ self.environment = environment or os.getenv("ENV") or os.getenv("ENVIRONMENT") or "production"
85
+ self.version = version or os.getenv("APP_VERSION") or "1.0.0"
86
+ self.runtime = "python"
87
+ self.batch_size = batch_size
88
+ self.flush_interval_seconds = flush_interval_seconds
89
+
90
+ self._queue: queue.Queue = queue.Queue(maxsize=10000)
91
+ self._is_running = True
92
+ self._worker_thread = threading.Thread(target=self._flusher_loop, daemon=True)
93
+ self._worker_thread.start()
94
+
95
+ if install_global_hook:
96
+ self._install_excepthook()
97
+
98
+ def log(
99
+ self,
100
+ level: str,
101
+ message: str,
102
+ latency_ms: float = 0.0,
103
+ status_code: int = 200,
104
+ error_type: Optional[str] = None,
105
+ stack_trace: Optional[str] = None,
106
+ metadata: Optional[Dict[str, Any]] = None,
107
+ ):
108
+ """Enqueues a telemetry log for non-blocking background dispatch."""
109
+ item = {
110
+ "service_id": self.service_name,
111
+ "runtime": self.runtime,
112
+ "environment": self.environment,
113
+ "version": self.version,
114
+ "timestamp": datetime.now(timezone.utc).isoformat(),
115
+ "level": level.upper(),
116
+ "latency_ms": float(latency_ms),
117
+ "status_code": int(status_code),
118
+ "error_type": error_type,
119
+ "message": message,
120
+ "stack_trace": stack_trace,
121
+ "raw_stack_trace": stack_trace,
122
+ "metadata": metadata or {},
123
+ }
124
+ try:
125
+ self._queue.put_nowait(item)
126
+ except queue.Full:
127
+ pass # Drop under extreme backpressure to protect host app
128
+
129
+ def capture_message(
130
+ self,
131
+ message: str,
132
+ level: str = "INFO",
133
+ latency_ms: float = 0.0,
134
+ status_code: int = 200,
135
+ metadata: Optional[Dict[str, Any]] = None,
136
+ ):
137
+ """Dispatches a custom log message with metadata tags."""
138
+ self.log(
139
+ level=level,
140
+ message=message,
141
+ latency_ms=latency_ms,
142
+ status_code=status_code,
143
+ metadata=metadata,
144
+ )
145
+
146
+ def info(self, message: str, latency_ms: float = 0.0, **metadata):
147
+ self.log("INFO", message, latency_ms=latency_ms, metadata=metadata)
148
+
149
+ def warn(self, message: str, latency_ms: float = 0.0, **metadata):
150
+ self.log("WARN", message, latency_ms=latency_ms, metadata=metadata)
151
+
152
+ def error(self, message: str, error_type: str = "ServerError", stack_trace: Optional[str] = None, **metadata):
153
+ self.log("ERROR", message, error_type=error_type, stack_trace=stack_trace, status_code=500, metadata=metadata)
154
+
155
+ def critical(self, message: str, error_type: str = "CriticalFailure", stack_trace: Optional[str] = None, **metadata):
156
+ self.log("CRITICAL", message, error_type=error_type, stack_trace=stack_trace, status_code=500, metadata=metadata)
157
+
158
+
159
+ def capture_exception(
160
+ self,
161
+ exc: BaseException,
162
+ message: Optional[str] = None,
163
+ latency_ms: float = 0.0,
164
+ status_code: int = 500,
165
+ metadata: Optional[Dict[str, Any]] = None,
166
+ ):
167
+ """Extracts stack trace and error type from an Exception instance and dispatches it."""
168
+ tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
169
+ formatted_trace = _sanitize_stack_trace("".join(tb_lines))
170
+ error_type = exc.__class__.__name__
171
+ msg = message or str(exc) or error_type
172
+
173
+ self.log(
174
+ level="ERROR",
175
+ message=msg,
176
+ latency_ms=latency_ms,
177
+ status_code=status_code,
178
+ error_type=error_type,
179
+ stack_trace=formatted_trace,
180
+ metadata=metadata,
181
+ )
182
+
183
+ def _install_excepthook(self):
184
+ """Installs unhandled exception hook to automatically report fatal crashes."""
185
+ original_hook = sys.excepthook
186
+
187
+ def unhandled_handler(exc_type, exc_value, exc_traceback):
188
+ try:
189
+ formatted_trace = _sanitize_stack_trace(
190
+ "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
191
+ )
192
+ self.critical(
193
+ message=f"Unhandled crash: {exc_value}",
194
+ error_type=exc_type.__name__,
195
+ stack_trace=formatted_trace,
196
+ )
197
+ time.sleep(0.3) # Allow flusher thread to flush before shutdown
198
+ except Exception:
199
+ pass
200
+ original_hook(exc_type, exc_value, exc_traceback)
201
+
202
+ sys.excepthook = unhandled_handler
203
+
204
+ def _send_batch(self, batch: List[Dict[str, Any]]):
205
+ if not batch:
206
+ return
207
+ url = f"{self.endpoint}/api/v1/telemetry/batch"
208
+ headers = {
209
+ "Content-Type": "application/json",
210
+ "X-API-Key": self.api_key,
211
+ "X-Project-Key": self.api_key,
212
+ }
213
+ try:
214
+ if httpx is not None:
215
+ with httpx.Client(timeout=5.0) as client:
216
+ client.post(url, json={"events": batch}, headers=headers)
217
+ else:
218
+ req_data = json.dumps({"events": batch}).encode("utf-8")
219
+ req = urllib.request.Request(url, data=req_data, headers=headers, method="POST")
220
+ with urllib.request.urlopen(req, timeout=5.0) as resp:
221
+ pass
222
+ except Exception:
223
+ pass # Fail silent to avoid degrading host application
224
+
225
+ def flush(self, timeout: float = 2.0):
226
+ """Flushes all queued items synchronously."""
227
+ batch = []
228
+ while not self._queue.empty():
229
+ try:
230
+ item = self._queue.get_nowait()
231
+ batch.append(item)
232
+ except queue.Empty:
233
+ break
234
+ if batch:
235
+ self._send_batch(batch)
236
+
237
+ def _flusher_loop(self):
238
+ """Background daemon sending buffered logs to AuraTrace Ingestion Gateway."""
239
+ while self._is_running:
240
+ batch = []
241
+ try:
242
+ item = self._queue.get(timeout=self.flush_interval_seconds)
243
+ batch.append(item)
244
+ while len(batch) < self.batch_size:
245
+ try:
246
+ batch.append(self._queue.get_nowait())
247
+ except queue.Empty:
248
+ break
249
+ except queue.Empty:
250
+ pass
251
+
252
+ if batch:
253
+ self._send_batch(batch)
254
+
255
+ def shutdown(self):
256
+ self._is_running = False
257
+ if self._worker_thread.is_alive():
258
+ self._worker_thread.join(timeout=1.0)
259
+
260
+
261
+ # Backward compatibility & ergonomic aliases
262
+ AuraTrace = AutoTrace
263
+ TraceClient = AutoTrace
264
+ Trace = AutoTrace
265
+ AutomaticBackendDetection = AutoTrace
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: auratrace-sdk
3
+ Version: 2.0.0
4
+ Summary: Official Python Telemetry & AI Diagnostic SDK for AuraTrace Autonomous Backend Diagnostics
5
+ Author-email: Sunil Singh <sunilsinghrajput192@gmail.com>
6
+ Project-URL: Homepage, https://github.com/sunilsingh175/auratrace
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx>=0.24.0
10
+
11
+ # Trace Python SDK
12
+
13
+ Official Python telemetry and unhandled crash diagnostics SDK for Trace.
14
+
15
+ ## Quickstart
16
+
17
+ ```python
18
+ from trace_sdk import Trace, TraceMiddleware
19
+
20
+ # Initialize Trace Client
21
+ trace = Trace(
22
+ service_id="payment-service",
23
+ api_key="trace_payment_secret_456",
24
+ endpoint="http://localhost:8000"
25
+ )
26
+
27
+ # 1. Log metrics & structured messages
28
+ trace.info("User checkout initiated", latency_ms=45.2, metadata={"user_id": "usr_99"})
29
+
30
+ # 2. Capture and report caught exceptions
31
+ try:
32
+ process_payment()
33
+ except Exception as e:
34
+ trace.capture_exception(e, message="Payment processing failure")
35
+
36
+ # 3. Use as a FastAPI / Starlette middleware
37
+ # Automatically records latency and reports unhandled exceptions
38
+ app.add_middleware(TraceMiddleware, client=trace)
39
+ ```
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ auratrace/__init__.py
4
+ auratrace/client.py
5
+ auratrace_sdk.egg-info/PKG-INFO
6
+ auratrace_sdk.egg-info/SOURCES.txt
7
+ auratrace_sdk.egg-info/dependency_links.txt
8
+ auratrace_sdk.egg-info/requires.txt
9
+ auratrace_sdk.egg-info/top_level.txt
10
+ autotrace/__init__.py
11
+ autotrace/client.py
12
+ trace_sdk/__init__.py
13
+ trace_sdk/client.py
14
+ trace_sdk/interceptor.py
@@ -0,0 +1 @@
1
+ httpx>=0.24.0
@@ -0,0 +1,3 @@
1
+ auratrace
2
+ autotrace
3
+ trace_sdk
@@ -0,0 +1,18 @@
1
+ """
2
+ AutoTrace Python SDK
3
+ """
4
+
5
+ from auratrace.client import AutoTrace, AuraTrace, TraceClient, Trace, AutomaticBackendDetection
6
+ from auratrace import init, capture_message, capture_exception, flush
7
+
8
+ __all__ = [
9
+ "AutoTrace",
10
+ "AuraTrace",
11
+ "TraceClient",
12
+ "Trace",
13
+ "AutomaticBackendDetection",
14
+ "init",
15
+ "capture_message",
16
+ "capture_exception",
17
+ "flush",
18
+ ]
@@ -0,0 +1,21 @@
1
+ """
2
+ AutoTrace Python SDK Client Module
3
+ """
4
+
5
+ from auratrace.client import (
6
+ AutoTrace,
7
+ AuraTrace,
8
+ TraceClient,
9
+ Trace,
10
+ AutomaticBackendDetection,
11
+ auto_detect_service_name,
12
+ )
13
+
14
+ __all__ = [
15
+ "AutoTrace",
16
+ "AuraTrace",
17
+ "TraceClient",
18
+ "Trace",
19
+ "AutomaticBackendDetection",
20
+ "auto_detect_service_name",
21
+ ]
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "auratrace-sdk"
7
+ version = "2.0.0"
8
+ authors = [{ name="Sunil Singh", email="sunilsinghrajput192@gmail.com" }]
9
+ description = "Official Python Telemetry & AI Diagnostic SDK for AuraTrace Autonomous Backend Diagnostics"
10
+ readme = "README.md"
11
+ requires-python = ">=3.9"
12
+ dependencies = [
13
+ "httpx>=0.24.0",
14
+ ]
15
+
16
+ [tool.setuptools]
17
+ packages = ["autotrace", "auratrace", "trace_sdk"]
18
+
19
+ [project.urls]
20
+ "Homepage" = "https://github.com/sunilsingh175/auratrace"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """
2
+ Trace SDK backward-compatibility module
3
+ """
4
+
5
+ from auratrace.client import AuraTrace, TraceClient, Trace, AutomaticBackendDetection
6
+
7
+ __all__ = ["AuraTrace", "TraceClient", "Trace", "AutomaticBackendDetection"]
@@ -0,0 +1,179 @@
1
+ """
2
+ Trace Python Telemetry Client
3
+ Provides non-blocking async batching, structured logging, and unhandled exception capture.
4
+ """
5
+
6
+ import sys
7
+ import time
8
+ import queue
9
+ import threading
10
+ import traceback
11
+ import json
12
+ from datetime import datetime, timezone
13
+ from typing import Optional, Dict, Any, List
14
+ import urllib.request
15
+ import urllib.error
16
+ try:
17
+ import httpx
18
+ except ImportError:
19
+ httpx = None
20
+
21
+
22
+
23
+ def _sanitize_stack_trace(trace_str: str) -> str:
24
+ if not trace_str:
25
+ return ""
26
+ import re
27
+ def _clean_path(match):
28
+ full_path = match.group(1).replace("\\", "/")
29
+ parts = full_path.split("/")
30
+ for marker in ["scripts", "app", "backend", "services", "controllers", "models", "workers"]:
31
+ if marker in parts:
32
+ idx = parts.index(marker)
33
+ return f'File "{"/".join(parts[idx:])}"'
34
+ if len(parts) > 1:
35
+ return f'File "{"/".join(parts[-2:])}"'
36
+ return f'File "{parts[-1]}"'
37
+ return re.sub(r'File "([^"]+)"', _clean_path, trace_str)
38
+
39
+
40
+ class Trace:
41
+ def __init__(
42
+ self,
43
+ service_id: str,
44
+ api_key: str,
45
+ endpoint: str = "http://localhost:8000",
46
+ batch_size: int = 50,
47
+ flush_interval_seconds: float = 1.0,
48
+ install_global_hook: bool = True,
49
+ ):
50
+ self.service_id = service_id
51
+ self.api_key = api_key
52
+ self.endpoint = endpoint.rstrip("/")
53
+ self.batch_size = batch_size
54
+ self.flush_interval_seconds = flush_interval_seconds
55
+
56
+ self._queue: queue.Queue = queue.Queue(maxsize=10000)
57
+ self._is_running = True
58
+ self._worker_thread = threading.Thread(target=self._flusher_loop, daemon=True)
59
+ self._worker_thread.start()
60
+
61
+ if install_global_hook:
62
+ self._install_excepthook()
63
+
64
+ def log(
65
+ self,
66
+ level: str,
67
+ message: str,
68
+ latency_ms: float = 0.0,
69
+ error_type: Optional[str] = None,
70
+ stack_trace: Optional[str] = None,
71
+ metadata: Optional[Dict[str, Any]] = None,
72
+ ):
73
+ """Enqueues a telemetry log for non-blocking background dispatch."""
74
+ item = {
75
+ "service_id": self.service_id,
76
+ "timestamp": datetime.now(timezone.utc).isoformat(),
77
+ "level": level.upper(),
78
+ "latency_ms": float(latency_ms),
79
+ "error_type": error_type,
80
+ "message": message,
81
+ "stack_trace": stack_trace,
82
+ "metadata": metadata or {},
83
+ }
84
+ try:
85
+ self._queue.put_nowait(item)
86
+ except queue.Full:
87
+ pass # Drop under extreme backpressure to protect host app
88
+
89
+ def info(self, message: str, latency_ms: float = 0.0, **metadata):
90
+ self.log("INFO", message, latency_ms=latency_ms, metadata=metadata)
91
+
92
+ def warn(self, message: str, latency_ms: float = 0.0, **metadata):
93
+ self.log("WARN", message, latency_ms=latency_ms, metadata=metadata)
94
+
95
+ def error(self, message: str, error_type: str = "ServerError", stack_trace: Optional[str] = None, **metadata):
96
+ self.log("ERROR", message, error_type=error_type, stack_trace=stack_trace, metadata=metadata)
97
+
98
+ def critical(self, message: str, error_type: str = "CriticalFailure", stack_trace: Optional[str] = None, **metadata):
99
+ self.log("CRITICAL", message, error_type=error_type, stack_trace=stack_trace, metadata=metadata)
100
+
101
+ def capture_exception(
102
+ self,
103
+ exc: BaseException,
104
+ message: Optional[str] = None,
105
+ latency_ms: float = 0.0,
106
+ metadata: Optional[Dict[str, Any]] = None,
107
+ ):
108
+ """Extracts stack trace and error type from an Exception instance and dispatches it."""
109
+ tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
110
+ formatted_trace = _sanitize_stack_trace("".join(tb_lines))
111
+ error_type = exc.__class__.__name__
112
+ msg = message or str(exc) or error_type
113
+
114
+ self.log(
115
+ level="ERROR",
116
+ message=msg,
117
+ latency_ms=latency_ms,
118
+ error_type=error_type,
119
+ stack_trace=formatted_trace,
120
+ metadata=metadata,
121
+ )
122
+
123
+ def _install_excepthook(self):
124
+ """Installs unhandled exception hook to automatically report fatal crashes."""
125
+ original_hook = sys.excepthook
126
+
127
+ def unhandled_handler(exc_type, exc_value, exc_traceback):
128
+ try:
129
+ formatted_trace = _sanitize_stack_trace(
130
+ "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
131
+ )
132
+ self.critical(
133
+ message=f"Unhandled crash: {exc_value}",
134
+ error_type=exc_type.__name__,
135
+ stack_trace=formatted_trace,
136
+ )
137
+ time.sleep(0.2) # Allow flusher thread to flush before shutdown
138
+ except Exception:
139
+ pass
140
+ original_hook(exc_type, exc_value, exc_traceback)
141
+
142
+ sys.excepthook = unhandled_handler
143
+
144
+ def _flusher_loop(self):
145
+ """Background daemon sending buffered logs to Trace Ingestion Gateway."""
146
+ client = httpx.Client(timeout=5.0) if httpx is not None else None
147
+ url = f"{self.endpoint}/api/v1/telemetry/batch"
148
+ headers = {
149
+ "Content-Type": "application/json",
150
+ "X-API-Key": self.api_key,
151
+ }
152
+
153
+ while self._is_running:
154
+ batch = []
155
+ deadline = time.time() + self.flush_interval_seconds
156
+
157
+ while len(batch) < self.batch_size and time.time() < deadline:
158
+ try:
159
+ item = self._queue.get(timeout=0.1)
160
+ batch.append(item)
161
+ except queue.Empty:
162
+ break
163
+
164
+ if batch:
165
+ try:
166
+ if client is not None:
167
+ client.post(url, json={"events": batch}, headers=headers)
168
+ else:
169
+ req_data = json.dumps({"events": batch}).encode("utf-8")
170
+ req = urllib.request.Request(url, data=req_data, headers=headers, method="POST")
171
+ with urllib.request.urlopen(req, timeout=5.0) as resp:
172
+ pass
173
+ except Exception:
174
+ pass # Fail silent to avoid degrading primary service
175
+
176
+ def shutdown(self):
177
+ self._is_running = False
178
+ if self._worker_thread.is_alive():
179
+ self._worker_thread.join(timeout=1.0)
@@ -0,0 +1,58 @@
1
+ """
2
+ Trace Middleware Interceptors for FastAPI / Starlette / Flask
3
+ """
4
+
5
+ import time
6
+ import traceback
7
+ from typing import Callable
8
+ from starlette.middleware.base import BaseHTTPMiddleware
9
+ from starlette.requests import Request
10
+ from starlette.responses import Response
11
+
12
+ from .client import Trace
13
+
14
+
15
+ class TraceMiddleware(BaseHTTPMiddleware):
16
+ """
17
+ FastAPI / Starlette middleware that times requests, captures latency metrics,
18
+ and reports unhandled 500 exceptions directly to Trace.
19
+ """
20
+
21
+ def __init__(self, app, client: Trace):
22
+ super().__init__(app)
23
+ self.client = client
24
+
25
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
26
+ start_time = time.perf_counter()
27
+ method = request.method
28
+ path = request.url.path
29
+
30
+ try:
31
+ response = await call_next(request)
32
+ latency_ms = (time.perf_counter() - start_time) * 1000.0
33
+
34
+ level = "ERROR" if response.status_code >= 500 else "WARN" if response.status_code >= 400 else "INFO"
35
+ self.client.log(
36
+ level=level,
37
+ message=f"{method} {path} - {response.status_code}",
38
+ latency_ms=latency_ms,
39
+ metadata={
40
+ "status_code": response.status_code,
41
+ "method": method,
42
+ "path": path,
43
+ },
44
+ )
45
+ return response
46
+
47
+ except Exception as exc:
48
+ latency_ms = (time.perf_counter() - start_time) * 1000.0
49
+ tb_str = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
50
+ self.client.critical(
51
+ message=f"Unhandled Exception in {method} {path}: {str(exc)}",
52
+ error_type=exc.__class__.__name__,
53
+ stack_trace=tb_str,
54
+ latency_ms=latency_ms,
55
+ path=path,
56
+ method=method,
57
+ )
58
+ raise exc from None