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.
- auratrace_sdk-2.0.0/PKG-INFO +39 -0
- auratrace_sdk-2.0.0/README.md +29 -0
- auratrace_sdk-2.0.0/auratrace/__init__.py +57 -0
- auratrace_sdk-2.0.0/auratrace/client.py +265 -0
- auratrace_sdk-2.0.0/auratrace_sdk.egg-info/PKG-INFO +39 -0
- auratrace_sdk-2.0.0/auratrace_sdk.egg-info/SOURCES.txt +14 -0
- auratrace_sdk-2.0.0/auratrace_sdk.egg-info/dependency_links.txt +1 -0
- auratrace_sdk-2.0.0/auratrace_sdk.egg-info/requires.txt +1 -0
- auratrace_sdk-2.0.0/auratrace_sdk.egg-info/top_level.txt +3 -0
- auratrace_sdk-2.0.0/autotrace/__init__.py +18 -0
- auratrace_sdk-2.0.0/autotrace/client.py +21 -0
- auratrace_sdk-2.0.0/pyproject.toml +20 -0
- auratrace_sdk-2.0.0/setup.cfg +4 -0
- auratrace_sdk-2.0.0/trace_sdk/__init__.py +7 -0
- auratrace_sdk-2.0.0/trace_sdk/client.py +179 -0
- auratrace_sdk-2.0.0/trace_sdk/interceptor.py +58 -0
|
@@ -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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.24.0
|
|
@@ -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,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
|