autotrace-py 0.1.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.
- autotrace_py-0.1.0/MANIFEST.in +8 -0
- autotrace_py-0.1.0/PKG-INFO +101 -0
- autotrace_py-0.1.0/README.md +73 -0
- autotrace_py-0.1.0/autotrace/__init__.py +27 -0
- autotrace_py-0.1.0/autotrace/client.py +318 -0
- autotrace_py-0.1.0/autotrace/integrations/__init__.py +1 -0
- autotrace_py-0.1.0/autotrace/integrations/django.py +77 -0
- autotrace_py-0.1.0/autotrace/integrations/fastapi.py +55 -0
- autotrace_py-0.1.0/autotrace/middleware.py +284 -0
- autotrace_py-0.1.0/autotrace_py.egg-info/PKG-INFO +101 -0
- autotrace_py-0.1.0/autotrace_py.egg-info/SOURCES.txt +20 -0
- autotrace_py-0.1.0/autotrace_py.egg-info/dependency_links.txt +1 -0
- autotrace_py-0.1.0/autotrace_py.egg-info/requires.txt +6 -0
- autotrace_py-0.1.0/autotrace_py.egg-info/top_level.txt +1 -0
- autotrace_py-0.1.0/pyproject.toml +46 -0
- autotrace_py-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autotrace-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AutoTrace Python SDK for autonomous error tracking and AI triage
|
|
5
|
+
Author-email: AutoTrace Team <maintainers@autotrace.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://autotrace.io
|
|
8
|
+
Project-URL: Repository, https://github.com/autotrace/autotrace-py
|
|
9
|
+
Project-URL: Documentation, https://docs.autotrace.io
|
|
10
|
+
Keywords: error-tracking,ai-triage,gemini,monitoring,exceptions
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Classifier: Topic :: Software Development :: Bug Tracking
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Provides-Extra: django
|
|
25
|
+
Requires-Dist: django>=3.2; extra == "django"
|
|
26
|
+
Provides-Extra: fastapi
|
|
27
|
+
Requires-Dist: starlette>=0.14.0; extra == "fastapi"
|
|
28
|
+
|
|
29
|
+
# AutoTrace Python SDK (`autotrace-py`)
|
|
30
|
+
|
|
31
|
+
Lightweight, zero-dependency Python client for autonomous error tracking and AI triage with [AutoTrace](https://autotrace.io).
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install autotrace
|
|
37
|
+
# or from source:
|
|
38
|
+
pip install -e ./sdk/python
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import autotrace
|
|
45
|
+
|
|
46
|
+
# 1. Initialize once at application startup
|
|
47
|
+
autotrace.init(
|
|
48
|
+
api_key="<YOUR_API_KEY>",
|
|
49
|
+
endpoint_url="http://localhost:8000/api/ingest/",
|
|
50
|
+
environment="production",
|
|
51
|
+
context={"service": "payment-api", "version": "2.1.0"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 2. Manual Exception Capture
|
|
55
|
+
try:
|
|
56
|
+
process_order()
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
autotrace.capture_exception(
|
|
59
|
+
exc_info=exc,
|
|
60
|
+
endpoint="/api/v1/orders",
|
|
61
|
+
context={"user_id": "usr_123"},
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Function Decorator
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
@autotrace.trace(endpoint="/jobs/sync-inventory")
|
|
69
|
+
def sync_inventory():
|
|
70
|
+
# If this raises, AutoTrace captures the error and traceback
|
|
71
|
+
perform_risky_task()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Framework Integrations
|
|
75
|
+
|
|
76
|
+
### Django
|
|
77
|
+
|
|
78
|
+
In your `settings.py`:
|
|
79
|
+
```python
|
|
80
|
+
AUTOTRACE_API_KEY = "<YOUR_API_KEY>"
|
|
81
|
+
AUTOTRACE_ENDPOINT = "http://localhost:8000/api/ingest/"
|
|
82
|
+
|
|
83
|
+
MIDDLEWARE = [
|
|
84
|
+
# ... other middleware
|
|
85
|
+
"autotrace.integrations.django.AutoTraceMiddleware",
|
|
86
|
+
]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### FastAPI / Starlette
|
|
90
|
+
|
|
91
|
+
In your `main.py`:
|
|
92
|
+
```python
|
|
93
|
+
from fastapi import FastAPI
|
|
94
|
+
import autotrace
|
|
95
|
+
from autotrace.integrations.fastapi import AutoTraceMiddleware
|
|
96
|
+
|
|
97
|
+
autotrace.init(api_key="<YOUR_API_KEY>")
|
|
98
|
+
|
|
99
|
+
app = FastAPI()
|
|
100
|
+
app.add_middleware(AutoTraceMiddleware)
|
|
101
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# AutoTrace Python SDK (`autotrace-py`)
|
|
2
|
+
|
|
3
|
+
Lightweight, zero-dependency Python client for autonomous error tracking and AI triage with [AutoTrace](https://autotrace.io).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install autotrace
|
|
9
|
+
# or from source:
|
|
10
|
+
pip install -e ./sdk/python
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import autotrace
|
|
17
|
+
|
|
18
|
+
# 1. Initialize once at application startup
|
|
19
|
+
autotrace.init(
|
|
20
|
+
api_key="<YOUR_API_KEY>",
|
|
21
|
+
endpoint_url="http://localhost:8000/api/ingest/",
|
|
22
|
+
environment="production",
|
|
23
|
+
context={"service": "payment-api", "version": "2.1.0"},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# 2. Manual Exception Capture
|
|
27
|
+
try:
|
|
28
|
+
process_order()
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
autotrace.capture_exception(
|
|
31
|
+
exc_info=exc,
|
|
32
|
+
endpoint="/api/v1/orders",
|
|
33
|
+
context={"user_id": "usr_123"},
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Function Decorator
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
@autotrace.trace(endpoint="/jobs/sync-inventory")
|
|
41
|
+
def sync_inventory():
|
|
42
|
+
# If this raises, AutoTrace captures the error and traceback
|
|
43
|
+
perform_risky_task()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Framework Integrations
|
|
47
|
+
|
|
48
|
+
### Django
|
|
49
|
+
|
|
50
|
+
In your `settings.py`:
|
|
51
|
+
```python
|
|
52
|
+
AUTOTRACE_API_KEY = "<YOUR_API_KEY>"
|
|
53
|
+
AUTOTRACE_ENDPOINT = "http://localhost:8000/api/ingest/"
|
|
54
|
+
|
|
55
|
+
MIDDLEWARE = [
|
|
56
|
+
# ... other middleware
|
|
57
|
+
"autotrace.integrations.django.AutoTraceMiddleware",
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### FastAPI / Starlette
|
|
62
|
+
|
|
63
|
+
In your `main.py`:
|
|
64
|
+
```python
|
|
65
|
+
from fastapi import FastAPI
|
|
66
|
+
import autotrace
|
|
67
|
+
from autotrace.integrations.fastapi import AutoTraceMiddleware
|
|
68
|
+
|
|
69
|
+
autotrace.init(api_key="<YOUR_API_KEY>")
|
|
70
|
+
|
|
71
|
+
app = FastAPI()
|
|
72
|
+
app.add_middleware(AutoTraceMiddleware)
|
|
73
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoTrace Python SDK (autotrace-py)
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Autonomous error tracking and AI-powered incident triage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from autotrace.client import (
|
|
8
|
+
AutoTraceClient,
|
|
9
|
+
capture_exception,
|
|
10
|
+
flush,
|
|
11
|
+
get_client,
|
|
12
|
+
init,
|
|
13
|
+
sanitize_data,
|
|
14
|
+
trace,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"init",
|
|
19
|
+
"capture_exception",
|
|
20
|
+
"trace",
|
|
21
|
+
"flush",
|
|
22
|
+
"get_client",
|
|
23
|
+
"AutoTraceClient",
|
|
24
|
+
"sanitize_data",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoTrace Python SDK Client (autotrace-py)
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Lightweight, zero-dependency client for capturing and reporting
|
|
5
|
+
application exceptions asynchronously to the AutoTrace backend.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import atexit
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import queue
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
import threading
|
|
19
|
+
import traceback
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from functools import wraps
|
|
24
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("autotrace")
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Constants & Defaults
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
DEFAULT_ENDPOINT = "http://localhost:8000/api/ingest/"
|
|
33
|
+
DEFAULT_ENVIRONMENT = "production"
|
|
34
|
+
TIMEOUT_SECONDS = 5.0
|
|
35
|
+
|
|
36
|
+
SENSITIVE_PATTERNS = re.compile(
|
|
37
|
+
r"(password|secret|token|authorization|api_key|access_token)", re.IGNORECASE
|
|
38
|
+
)
|
|
39
|
+
MASK_VALUE = "********"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Data Sanitization
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def _is_sensitive(key: str) -> bool:
|
|
47
|
+
return bool(SENSITIVE_PATTERNS.search(str(key)))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sanitize_data(data: Any) -> Any:
|
|
51
|
+
"""Recursively scrub sensitive keys in dictionaries and lists."""
|
|
52
|
+
if isinstance(data, dict):
|
|
53
|
+
return {
|
|
54
|
+
k: (MASK_VALUE if _is_sensitive(k) else sanitize_data(v))
|
|
55
|
+
for k, v in data.items()
|
|
56
|
+
}
|
|
57
|
+
elif isinstance(data, (list, tuple)):
|
|
58
|
+
return [sanitize_data(item) for item in data]
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# AutoTrace Client
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
class AutoTraceClient:
|
|
67
|
+
"""Core SDK client managing asynchronous dispatch of error events."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
api_key: str,
|
|
72
|
+
endpoint_url: str = DEFAULT_ENDPOINT,
|
|
73
|
+
environment: str = DEFAULT_ENVIRONMENT,
|
|
74
|
+
default_context: Optional[Dict[str, Any]] = None,
|
|
75
|
+
max_queue_size: int = 1000,
|
|
76
|
+
) -> None:
|
|
77
|
+
self.api_key = api_key.strip()
|
|
78
|
+
self.endpoint_url = endpoint_url
|
|
79
|
+
self.environment = environment
|
|
80
|
+
self.default_context = default_context or {}
|
|
81
|
+
|
|
82
|
+
# Asynchronous background worker queue
|
|
83
|
+
self._queue: queue.Queue = queue.Queue(maxsize=max_queue_size)
|
|
84
|
+
self._worker_thread = threading.Thread(
|
|
85
|
+
target=self._worker_loop,
|
|
86
|
+
daemon=True,
|
|
87
|
+
name="autotrace-worker",
|
|
88
|
+
)
|
|
89
|
+
self._worker_thread.start()
|
|
90
|
+
|
|
91
|
+
# Register exit handler to flush pending events
|
|
92
|
+
atexit.register(self.flush)
|
|
93
|
+
|
|
94
|
+
def _worker_loop(self) -> None:
|
|
95
|
+
"""Background daemon thread that consumes and dispatches events."""
|
|
96
|
+
while True:
|
|
97
|
+
try:
|
|
98
|
+
payload, callback = self._queue.get()
|
|
99
|
+
if payload is None:
|
|
100
|
+
# Poison pill to shut down worker if needed
|
|
101
|
+
self._queue.task_done()
|
|
102
|
+
break
|
|
103
|
+
status_code, response_data = self._send_http(payload)
|
|
104
|
+
if callback:
|
|
105
|
+
callback(status_code, response_data)
|
|
106
|
+
self._queue.task_done()
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
logger.debug("AutoTrace worker encountered an error (suppressed): %s", exc)
|
|
109
|
+
|
|
110
|
+
def _send_http(self, payload: Dict[str, Any]) -> Tuple[int, Optional[Dict[str, Any]]]:
|
|
111
|
+
"""Perform a blocking HTTP POST with a short timeout. Fails silently."""
|
|
112
|
+
try:
|
|
113
|
+
body = json.dumps(payload).encode("utf-8")
|
|
114
|
+
req = urllib.request.Request(
|
|
115
|
+
self.endpoint_url,
|
|
116
|
+
data=body,
|
|
117
|
+
headers={
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
"X-API-Key": self.api_key,
|
|
120
|
+
"User-Agent": f"autotrace-py/0.1.0 (Python {platform.python_version()})",
|
|
121
|
+
},
|
|
122
|
+
method="POST",
|
|
123
|
+
)
|
|
124
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as resp:
|
|
125
|
+
status_code = resp.status
|
|
126
|
+
resp_text = resp.read().decode("utf-8", errors="replace")
|
|
127
|
+
try:
|
|
128
|
+
data = json.loads(resp_text)
|
|
129
|
+
except Exception:
|
|
130
|
+
data = {"raw": resp_text}
|
|
131
|
+
logger.debug("AutoTrace: event reported (HTTP %s)", status_code)
|
|
132
|
+
return status_code, data
|
|
133
|
+
except urllib.error.HTTPError as exc:
|
|
134
|
+
err_body = exc.read().decode("utf-8", errors="replace")
|
|
135
|
+
logger.debug("AutoTrace: API error HTTP %s: %s", exc.code, err_body)
|
|
136
|
+
return exc.code, {"error": err_body}
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
logger.debug("AutoTrace: delivery failed (suppressed): %s", exc)
|
|
139
|
+
return 0, {"error": str(exc)}
|
|
140
|
+
|
|
141
|
+
def capture_exception(
|
|
142
|
+
self,
|
|
143
|
+
exc_info: Optional[Union[BaseException, Tuple[Any, ...], bool]] = None,
|
|
144
|
+
context: Optional[Dict[str, Any]] = None,
|
|
145
|
+
endpoint: Optional[str] = None,
|
|
146
|
+
sync: bool = False,
|
|
147
|
+
callback: Optional[Callable[[int, Optional[Dict[str, Any]]], None]] = None,
|
|
148
|
+
) -> Optional[Tuple[int, Optional[Dict[str, Any]]]]:
|
|
149
|
+
"""Capture and format an unhandled exception for AutoTrace ingestion."""
|
|
150
|
+
# 1. Resolve exception details
|
|
151
|
+
if exc_info is True or exc_info is None:
|
|
152
|
+
exc_type, exc_val, exc_tb = sys.exc_info()
|
|
153
|
+
elif isinstance(exc_info, BaseException):
|
|
154
|
+
exc_type, exc_val, exc_tb = type(exc_info), exc_info, exc_info.__traceback__
|
|
155
|
+
elif isinstance(exc_info, tuple) and len(exc_info) == 3:
|
|
156
|
+
exc_type, exc_val, exc_tb = exc_info
|
|
157
|
+
else:
|
|
158
|
+
exc_type, exc_val, exc_tb = None, None, None
|
|
159
|
+
|
|
160
|
+
if exc_val is None:
|
|
161
|
+
logger.debug("AutoTrace: capture_exception called with no active exception.")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
# 2. Format traceback array
|
|
165
|
+
tb_lines: List[str] = []
|
|
166
|
+
try:
|
|
167
|
+
raw_lines = traceback.format_exception(exc_type, exc_val, exc_tb)
|
|
168
|
+
for chunk in raw_lines:
|
|
169
|
+
tb_lines.extend(chunk.splitlines())
|
|
170
|
+
except Exception:
|
|
171
|
+
tb_lines = [f"{exc_type}: {exc_val}"]
|
|
172
|
+
|
|
173
|
+
# 3. Assemble runtime metadata
|
|
174
|
+
runtime_str = f"python {platform.python_version()}"
|
|
175
|
+
|
|
176
|
+
# 4. Merge context
|
|
177
|
+
merged_context: Dict[str, Any] = {
|
|
178
|
+
**self.default_context,
|
|
179
|
+
"environment": self.environment,
|
|
180
|
+
"os": f"{platform.system()} {platform.release()}",
|
|
181
|
+
"architecture": platform.machine(),
|
|
182
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
183
|
+
}
|
|
184
|
+
if context:
|
|
185
|
+
merged_context.update(context)
|
|
186
|
+
|
|
187
|
+
# 5. Build universal ingestion payload
|
|
188
|
+
payload = {
|
|
189
|
+
"error_type": exc_type.__name__ if exc_type else "Exception",
|
|
190
|
+
"error_message": str(exc_val),
|
|
191
|
+
"endpoint": endpoint or "",
|
|
192
|
+
"runtime": runtime_str,
|
|
193
|
+
"traceback": tb_lines,
|
|
194
|
+
"context": sanitize_data(merged_context),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# 6. Dispatch synchronously or queue for background thread
|
|
198
|
+
if sync:
|
|
199
|
+
return self._send_http(payload)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
self._queue.put_nowait((payload, callback))
|
|
203
|
+
except queue.Full:
|
|
204
|
+
logger.warning("AutoTrace: event queue is full, dropping error event.")
|
|
205
|
+
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def trace(
|
|
209
|
+
self,
|
|
210
|
+
endpoint: Optional[str] = None,
|
|
211
|
+
context: Optional[Dict[str, Any]] = None,
|
|
212
|
+
reraise: bool = False,
|
|
213
|
+
) -> Callable:
|
|
214
|
+
"""Decorator to automatically catch and report unhandled errors."""
|
|
215
|
+
def decorator(func: Callable) -> Callable:
|
|
216
|
+
@wraps(func)
|
|
217
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
218
|
+
try:
|
|
219
|
+
return func(*args, **kwargs)
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
self.capture_exception(
|
|
222
|
+
exc_info=exc,
|
|
223
|
+
endpoint=endpoint or func.__qualname__,
|
|
224
|
+
context=context,
|
|
225
|
+
)
|
|
226
|
+
if reraise:
|
|
227
|
+
raise
|
|
228
|
+
return None
|
|
229
|
+
return wrapper
|
|
230
|
+
return decorator
|
|
231
|
+
|
|
232
|
+
def flush(self, timeout: float = 3.0) -> None:
|
|
233
|
+
"""Block until all queued events are sent, up to *timeout* seconds."""
|
|
234
|
+
try:
|
|
235
|
+
self._queue.join()
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# Global Singleton Interface
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
_GLOBAL_CLIENT: Optional[AutoTraceClient] = None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def init(
|
|
248
|
+
api_key: str,
|
|
249
|
+
endpoint_url: str = DEFAULT_ENDPOINT,
|
|
250
|
+
environment: str = DEFAULT_ENVIRONMENT,
|
|
251
|
+
context: Optional[Dict[str, Any]] = None,
|
|
252
|
+
) -> AutoTraceClient:
|
|
253
|
+
"""Initialize the global AutoTrace client singleton."""
|
|
254
|
+
global _GLOBAL_CLIENT
|
|
255
|
+
_GLOBAL_CLIENT = AutoTraceClient(
|
|
256
|
+
api_key=api_key,
|
|
257
|
+
endpoint_url=endpoint_url,
|
|
258
|
+
environment=environment,
|
|
259
|
+
default_context=context,
|
|
260
|
+
)
|
|
261
|
+
logger.debug("AutoTrace SDK initialized for environment '%s'.", environment)
|
|
262
|
+
return _GLOBAL_CLIENT
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_client() -> Optional[AutoTraceClient]:
|
|
266
|
+
"""Retrieve the global AutoTrace client instance."""
|
|
267
|
+
return _GLOBAL_CLIENT
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def capture_exception(
|
|
271
|
+
exc_info: Optional[Union[BaseException, Tuple[Any, ...], bool]] = None,
|
|
272
|
+
context: Optional[Dict[str, Any]] = None,
|
|
273
|
+
endpoint: Optional[str] = None,
|
|
274
|
+
sync: bool = False,
|
|
275
|
+
callback: Optional[Callable[[int, Optional[Dict[str, Any]]], None]] = None,
|
|
276
|
+
) -> Optional[Tuple[int, Optional[Dict[str, Any]]]]:
|
|
277
|
+
"""Capture an exception with the global client."""
|
|
278
|
+
if _GLOBAL_CLIENT is None:
|
|
279
|
+
logger.warning("AutoTrace SDK is not initialized. Call autotrace.init(...) first.")
|
|
280
|
+
return None
|
|
281
|
+
return _GLOBAL_CLIENT.capture_exception(
|
|
282
|
+
exc_info=exc_info,
|
|
283
|
+
context=context,
|
|
284
|
+
endpoint=endpoint,
|
|
285
|
+
sync=sync,
|
|
286
|
+
callback=callback,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def trace(
|
|
291
|
+
endpoint: Optional[str] = None,
|
|
292
|
+
context: Optional[Dict[str, Any]] = None,
|
|
293
|
+
reraise: bool = False,
|
|
294
|
+
) -> Callable:
|
|
295
|
+
"""Decorator to catch and report exceptions using the global client."""
|
|
296
|
+
def decorator(func: Callable) -> Callable:
|
|
297
|
+
@wraps(func)
|
|
298
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
299
|
+
try:
|
|
300
|
+
return func(*args, **kwargs)
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
if _GLOBAL_CLIENT:
|
|
303
|
+
_GLOBAL_CLIENT.capture_exception(
|
|
304
|
+
exc_info=exc,
|
|
305
|
+
endpoint=endpoint or func.__qualname__,
|
|
306
|
+
context=context,
|
|
307
|
+
)
|
|
308
|
+
if reraise:
|
|
309
|
+
raise
|
|
310
|
+
return None
|
|
311
|
+
return wrapper
|
|
312
|
+
return decorator
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def flush(timeout: float = 3.0) -> None:
|
|
316
|
+
"""Flush pending events queued in the global client."""
|
|
317
|
+
if _GLOBAL_CLIENT:
|
|
318
|
+
_GLOBAL_CLIENT.flush(timeout=timeout)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AutoTrace framework integrations."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoTrace Django Integration Middleware
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Captures unhandled exceptions during Django request handling and reports
|
|
5
|
+
them to AutoTrace with sanitized request metadata and telemetry.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Callable, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from autotrace.client import capture_exception, init, get_client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AutoTraceMiddleware:
|
|
17
|
+
"""Django middleware to automatically report unhandled exceptions."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, get_response: Optional[Callable] = None) -> None:
|
|
20
|
+
self.get_response = get_response
|
|
21
|
+
|
|
22
|
+
# Auto-initialize if settings.AUTOTRACE_API_KEY is configured
|
|
23
|
+
try:
|
|
24
|
+
from django.conf import settings
|
|
25
|
+
if get_client() is None and hasattr(settings, "AUTOTRACE_API_KEY"):
|
|
26
|
+
init(
|
|
27
|
+
api_key=settings.AUTOTRACE_API_KEY,
|
|
28
|
+
endpoint_url=getattr(settings, "AUTOTRACE_ENDPOINT", "http://localhost:8000/api/ingest/"),
|
|
29
|
+
environment=getattr(settings, "AUTOTRACE_ENVIRONMENT", "production"),
|
|
30
|
+
)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def __call__(self, request: Any) -> Any:
|
|
35
|
+
try:
|
|
36
|
+
response = self.get_response(request) if self.get_response else None
|
|
37
|
+
return response
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
self.process_exception(request, exc)
|
|
40
|
+
raise
|
|
41
|
+
|
|
42
|
+
def process_exception(self, request: Any, exception: BaseException) -> None:
|
|
43
|
+
"""Capture request telemetry and dispatch error event to AutoTrace."""
|
|
44
|
+
headers: Dict[str, str] = {}
|
|
45
|
+
for key, value in getattr(request, "META", {}).items():
|
|
46
|
+
if key.startswith("HTTP_"):
|
|
47
|
+
header_name = key[5:].replace("_", "-").title()
|
|
48
|
+
headers[header_name] = str(value)
|
|
49
|
+
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH", "REMOTE_ADDR"):
|
|
50
|
+
headers[key.replace("_", "-").title()] = str(value)
|
|
51
|
+
|
|
52
|
+
# Parse request query params
|
|
53
|
+
query_params = dict(getattr(request, "GET", {}).items())
|
|
54
|
+
|
|
55
|
+
# Best-effort parse body payload
|
|
56
|
+
body_data: Any = None
|
|
57
|
+
try:
|
|
58
|
+
raw_body = getattr(request, "body", None)
|
|
59
|
+
if raw_body:
|
|
60
|
+
body_data = json.loads(raw_body.decode("utf-8"))
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
context: Dict[str, Any] = {
|
|
65
|
+
"method": getattr(request, "method", "UNKNOWN"),
|
|
66
|
+
"path": getattr(request, "path", ""),
|
|
67
|
+
"headers": headers,
|
|
68
|
+
"query_params": query_params,
|
|
69
|
+
"body": body_data,
|
|
70
|
+
"user": str(getattr(request, "user", "anonymous")),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
capture_exception(
|
|
74
|
+
exc_info=exception,
|
|
75
|
+
endpoint=getattr(request, "path", ""),
|
|
76
|
+
context=context,
|
|
77
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoTrace FastAPI / Starlette Integration Middleware
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
ASGI middleware that intercepts unhandled exceptions in FastAPI / Starlette
|
|
5
|
+
applications, captures route & header metadata, and reports them to AutoTrace.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, Dict
|
|
11
|
+
|
|
12
|
+
from autotrace.client import capture_exception
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AutoTraceMiddleware:
|
|
16
|
+
"""ASGI middleware for FastAPI and Starlette applications."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, app: Callable) -> None:
|
|
19
|
+
self.app = app
|
|
20
|
+
|
|
21
|
+
async def __call__(self, scope: Dict[str, Any], receive: Callable, send: Callable) -> None:
|
|
22
|
+
if scope["type"] != "http":
|
|
23
|
+
await self.app(scope, receive, send)
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
await self.app(scope, receive, send)
|
|
28
|
+
except Exception as exc:
|
|
29
|
+
# Extract headers from ASGI scope
|
|
30
|
+
headers: Dict[str, str] = {}
|
|
31
|
+
for raw_k, raw_v in scope.get("headers", []):
|
|
32
|
+
k = raw_k.decode("latin1").lower()
|
|
33
|
+
v = raw_v.decode("latin1")
|
|
34
|
+
headers[k] = v
|
|
35
|
+
|
|
36
|
+
endpoint = scope.get("path", "")
|
|
37
|
+
method = scope.get("method", "GET")
|
|
38
|
+
query_string = scope.get("query_string", b"").decode("latin1")
|
|
39
|
+
|
|
40
|
+
context = {
|
|
41
|
+
"method": method,
|
|
42
|
+
"path": endpoint,
|
|
43
|
+
"query_string": query_string,
|
|
44
|
+
"headers": headers,
|
|
45
|
+
"client": scope.get("client"),
|
|
46
|
+
"server": scope.get("server"),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
capture_exception(
|
|
50
|
+
exc_info=exc,
|
|
51
|
+
endpoint=endpoint,
|
|
52
|
+
context=context,
|
|
53
|
+
)
|
|
54
|
+
# Re-raise so FastAPI's error handlers / status responses continue
|
|
55
|
+
raise
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoTrace Django Middleware — Client SDK
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Drop-in Django middleware that captures unhandled exceptions and reports
|
|
6
|
+
them to the AutoTrace ingestion API.
|
|
7
|
+
|
|
8
|
+
Quick start
|
|
9
|
+
-----------
|
|
10
|
+
1. ``pip install autotrace`` (or add this package to your requirements)
|
|
11
|
+
|
|
12
|
+
2. Add to your Django settings::
|
|
13
|
+
|
|
14
|
+
MIDDLEWARE = [
|
|
15
|
+
...
|
|
16
|
+
'autotrace.middleware.AutoTraceMiddleware',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
AUTOTRACE_DSN = "https://api.autotrace.io/ingest/" # default
|
|
20
|
+
AUTOTRACE_API_KEY = "<your-project-api-key>"
|
|
21
|
+
|
|
22
|
+
3. That's it — unhandled 500s are now reported automatically.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import re
|
|
30
|
+
import traceback
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from typing import Any, Callable
|
|
33
|
+
|
|
34
|
+
from django.conf import settings
|
|
35
|
+
from django.http import HttpRequest, HttpResponse
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import urllib.request
|
|
39
|
+
import urllib.error
|
|
40
|
+
|
|
41
|
+
_HAS_URLLIB = True
|
|
42
|
+
except ImportError: # pragma: no cover — paranoia guard
|
|
43
|
+
_HAS_URLLIB = False
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger("autotrace.middleware")
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Configuration helpers
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
# Default ingestion endpoint
|
|
52
|
+
_DEFAULT_DSN = "https://api.autotrace.io/ingest/"
|
|
53
|
+
|
|
54
|
+
# Maximum traceback frames forwarded (keeps payloads focused)
|
|
55
|
+
_MAX_TRACEBACK_FRAMES = 15
|
|
56
|
+
|
|
57
|
+
# Keys whose *values* will be masked in headers / body payloads.
|
|
58
|
+
# Matching is case-insensitive and checks whether the key *contains* the word.
|
|
59
|
+
_SENSITIVE_PATTERNS: re.Pattern[str] = re.compile(
|
|
60
|
+
r"(password|secret|token|authorization)", re.IGNORECASE
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_MASK = "********"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Data scrubbing
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_sensitive_key(key: str) -> bool:
|
|
72
|
+
"""Return ``True`` if *key* looks like it holds a sensitive value."""
|
|
73
|
+
return bool(_SENSITIVE_PATTERNS.search(key))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _scrub(data: Any) -> Any:
|
|
77
|
+
"""Recursively walk *data* and mask values behind sensitive keys.
|
|
78
|
+
|
|
79
|
+
Handles nested dicts and lists. Everything else passes through
|
|
80
|
+
unchanged.
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(data, dict):
|
|
83
|
+
sanitised: dict[str, Any] = {}
|
|
84
|
+
for key, value in data.items():
|
|
85
|
+
if _is_sensitive_key(str(key)):
|
|
86
|
+
sanitised[key] = _MASK
|
|
87
|
+
else:
|
|
88
|
+
sanitised[key] = _scrub(value)
|
|
89
|
+
return sanitised
|
|
90
|
+
|
|
91
|
+
if isinstance(data, (list, tuple)):
|
|
92
|
+
return [_scrub(item) for item in data]
|
|
93
|
+
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Request helpers
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_headers(request: HttpRequest) -> dict[str, str]:
|
|
103
|
+
"""Pull HTTP headers from ``request.META``, scrubbing sensitive ones."""
|
|
104
|
+
headers: dict[str, str] = {}
|
|
105
|
+
for meta_key, meta_value in request.META.items():
|
|
106
|
+
if not meta_key.startswith("HTTP_"):
|
|
107
|
+
continue
|
|
108
|
+
# Convert META key (e.g. HTTP_ACCEPT_LANGUAGE) → header name
|
|
109
|
+
header_name = meta_key[5:].replace("_", "-").title()
|
|
110
|
+
if _is_sensitive_key(header_name):
|
|
111
|
+
headers[header_name] = _MASK
|
|
112
|
+
else:
|
|
113
|
+
headers[header_name] = str(meta_value)
|
|
114
|
+
return headers
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _extract_body(request: HttpRequest) -> dict[str, Any]:
|
|
118
|
+
"""Best-effort parse + scrub of the request body."""
|
|
119
|
+
try:
|
|
120
|
+
raw = request.body
|
|
121
|
+
if not raw:
|
|
122
|
+
return {}
|
|
123
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
124
|
+
except Exception:
|
|
125
|
+
# Body might be form-encoded, binary, or otherwise unparsable.
|
|
126
|
+
# Wrap it as a raw string so we still capture *something*.
|
|
127
|
+
try:
|
|
128
|
+
payload = {"_raw": request.body.decode("utf-8", errors="replace")}
|
|
129
|
+
except Exception:
|
|
130
|
+
return {}
|
|
131
|
+
|
|
132
|
+
return _scrub(payload)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _extract_traceback(exc: BaseException) -> list[str]:
|
|
136
|
+
"""Return a compact, relevant traceback as a list of formatted lines.
|
|
137
|
+
|
|
138
|
+
We limit output to ``_MAX_TRACEBACK_FRAMES`` innermost frames so that
|
|
139
|
+
payloads stay small and focused on application code rather than deep
|
|
140
|
+
framework internals.
|
|
141
|
+
"""
|
|
142
|
+
tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
143
|
+
# ``format_exception`` returns a list of multi-line strings; flatten.
|
|
144
|
+
flat: list[str] = []
|
|
145
|
+
for chunk in tb_lines:
|
|
146
|
+
flat.extend(chunk.splitlines())
|
|
147
|
+
|
|
148
|
+
# Keep only the tail (most relevant frames) + the final exception line.
|
|
149
|
+
if len(flat) > _MAX_TRACEBACK_FRAMES:
|
|
150
|
+
flat = ["... (truncated)"] + flat[-_MAX_TRACEBACK_FRAMES:]
|
|
151
|
+
|
|
152
|
+
return flat
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Payload construction
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _build_payload(
|
|
161
|
+
request: HttpRequest, exception: BaseException
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
"""Assemble the JSON-serialisable error payload."""
|
|
164
|
+
return {
|
|
165
|
+
"exception": {
|
|
166
|
+
"type": type(exception).__qualname__,
|
|
167
|
+
"message": str(exception),
|
|
168
|
+
"traceback": _extract_traceback(exception),
|
|
169
|
+
},
|
|
170
|
+
"request": {
|
|
171
|
+
"method": request.method,
|
|
172
|
+
"path": request.path,
|
|
173
|
+
"query_string": request.META.get("QUERY_STRING", ""),
|
|
174
|
+
"headers": _extract_headers(request),
|
|
175
|
+
"body": _extract_body(request),
|
|
176
|
+
},
|
|
177
|
+
"server": {
|
|
178
|
+
"server_name": request.META.get("SERVER_NAME", ""),
|
|
179
|
+
"server_port": request.META.get("SERVER_PORT", ""),
|
|
180
|
+
},
|
|
181
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
182
|
+
"sdk": {
|
|
183
|
+
"name": "autotrace-python",
|
|
184
|
+
"version": "0.1.0",
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Transport
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _send_payload(payload: dict[str, Any], dsn: str, api_key: str) -> None:
|
|
195
|
+
"""POST *payload* to the AutoTrace ingestion endpoint.
|
|
196
|
+
|
|
197
|
+
Uses only the stdlib ``urllib`` so we avoid adding ``requests`` as a
|
|
198
|
+
hard dependency for SDK consumers.
|
|
199
|
+
|
|
200
|
+
This function is wrapped in a blanket ``try / except`` at the call
|
|
201
|
+
site so that transport failures **never** propagate to the host app.
|
|
202
|
+
"""
|
|
203
|
+
body = json.dumps(payload).encode("utf-8")
|
|
204
|
+
req = urllib.request.Request(
|
|
205
|
+
dsn,
|
|
206
|
+
data=body,
|
|
207
|
+
headers={
|
|
208
|
+
"Content-Type": "application/json",
|
|
209
|
+
"X-AutoTrace-Key": api_key,
|
|
210
|
+
},
|
|
211
|
+
method="POST",
|
|
212
|
+
)
|
|
213
|
+
urllib.request.urlopen(req, timeout=5)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Middleware
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class AutoTraceMiddleware:
|
|
222
|
+
"""Django middleware that reports unhandled exceptions to AutoTrace.
|
|
223
|
+
|
|
224
|
+
Configuration is read from ``django.conf.settings``:
|
|
225
|
+
|
|
226
|
+
======================== ======================================
|
|
227
|
+
Setting Description
|
|
228
|
+
======================== ======================================
|
|
229
|
+
``AUTOTRACE_DSN`` Ingestion URL (default: ``https://api.autotrace.io/ingest/``)
|
|
230
|
+
``AUTOTRACE_API_KEY`` Project API key (required)
|
|
231
|
+
``AUTOTRACE_ENABLED`` Kill-switch — set ``False`` to disable (default: ``True``)
|
|
232
|
+
======================== ======================================
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
|
|
236
|
+
self.get_response = get_response
|
|
237
|
+
|
|
238
|
+
# Read configuration once at startup.
|
|
239
|
+
self.dsn: str = getattr(settings, "AUTOTRACE_DSN", _DEFAULT_DSN)
|
|
240
|
+
self.api_key: str = getattr(settings, "AUTOTRACE_API_KEY", "")
|
|
241
|
+
self.enabled: bool = getattr(settings, "AUTOTRACE_ENABLED", True)
|
|
242
|
+
|
|
243
|
+
if self.enabled and not self.api_key:
|
|
244
|
+
logger.warning(
|
|
245
|
+
"AutoTraceMiddleware is enabled but AUTOTRACE_API_KEY is not "
|
|
246
|
+
"set. Error reports will be sent without authentication."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# -- Django middleware interface ------------------------------------------
|
|
250
|
+
|
|
251
|
+
def __call__(self, request: HttpRequest) -> HttpResponse:
|
|
252
|
+
"""Wrap the downstream middleware / view chain."""
|
|
253
|
+
try:
|
|
254
|
+
response = self.get_response(request)
|
|
255
|
+
except Exception as exc:
|
|
256
|
+
# Capture → report → re-raise so Django's error handling
|
|
257
|
+
# (DEBUG page, logging, etc.) still works normally.
|
|
258
|
+
self._report(request, exc)
|
|
259
|
+
raise
|
|
260
|
+
|
|
261
|
+
return response
|
|
262
|
+
|
|
263
|
+
# -- Internal helpers ----------------------------------------------------
|
|
264
|
+
|
|
265
|
+
def _report(self, request: HttpRequest, exception: BaseException) -> None:
|
|
266
|
+
"""Build and ship the error payload — silently swallowing any errors
|
|
267
|
+
that occur during reporting so we never break the host application.
|
|
268
|
+
"""
|
|
269
|
+
if not self.enabled:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
payload = _build_payload(request, exception)
|
|
274
|
+
_send_payload(payload, self.dsn, self.api_key)
|
|
275
|
+
logger.debug("AutoTrace: error reported successfully.")
|
|
276
|
+
except Exception:
|
|
277
|
+
# ----- FAIL SILENTLY -----
|
|
278
|
+
# If AutoTrace's servers are unreachable, misconfigured, or the
|
|
279
|
+
# payload can't be serialised, we absolutely must not let that
|
|
280
|
+
# crash the client's application a *second* time.
|
|
281
|
+
logger.debug(
|
|
282
|
+
"AutoTrace: failed to report error (suppressed).",
|
|
283
|
+
exc_info=True,
|
|
284
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autotrace-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AutoTrace Python SDK for autonomous error tracking and AI triage
|
|
5
|
+
Author-email: AutoTrace Team <maintainers@autotrace.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://autotrace.io
|
|
8
|
+
Project-URL: Repository, https://github.com/autotrace/autotrace-py
|
|
9
|
+
Project-URL: Documentation, https://docs.autotrace.io
|
|
10
|
+
Keywords: error-tracking,ai-triage,gemini,monitoring,exceptions
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Classifier: Topic :: Software Development :: Bug Tracking
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Provides-Extra: django
|
|
25
|
+
Requires-Dist: django>=3.2; extra == "django"
|
|
26
|
+
Provides-Extra: fastapi
|
|
27
|
+
Requires-Dist: starlette>=0.14.0; extra == "fastapi"
|
|
28
|
+
|
|
29
|
+
# AutoTrace Python SDK (`autotrace-py`)
|
|
30
|
+
|
|
31
|
+
Lightweight, zero-dependency Python client for autonomous error tracking and AI triage with [AutoTrace](https://autotrace.io).
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install autotrace
|
|
37
|
+
# or from source:
|
|
38
|
+
pip install -e ./sdk/python
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import autotrace
|
|
45
|
+
|
|
46
|
+
# 1. Initialize once at application startup
|
|
47
|
+
autotrace.init(
|
|
48
|
+
api_key="<YOUR_API_KEY>",
|
|
49
|
+
endpoint_url="http://localhost:8000/api/ingest/",
|
|
50
|
+
environment="production",
|
|
51
|
+
context={"service": "payment-api", "version": "2.1.0"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 2. Manual Exception Capture
|
|
55
|
+
try:
|
|
56
|
+
process_order()
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
autotrace.capture_exception(
|
|
59
|
+
exc_info=exc,
|
|
60
|
+
endpoint="/api/v1/orders",
|
|
61
|
+
context={"user_id": "usr_123"},
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Function Decorator
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
@autotrace.trace(endpoint="/jobs/sync-inventory")
|
|
69
|
+
def sync_inventory():
|
|
70
|
+
# If this raises, AutoTrace captures the error and traceback
|
|
71
|
+
perform_risky_task()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Framework Integrations
|
|
75
|
+
|
|
76
|
+
### Django
|
|
77
|
+
|
|
78
|
+
In your `settings.py`:
|
|
79
|
+
```python
|
|
80
|
+
AUTOTRACE_API_KEY = "<YOUR_API_KEY>"
|
|
81
|
+
AUTOTRACE_ENDPOINT = "http://localhost:8000/api/ingest/"
|
|
82
|
+
|
|
83
|
+
MIDDLEWARE = [
|
|
84
|
+
# ... other middleware
|
|
85
|
+
"autotrace.integrations.django.AutoTraceMiddleware",
|
|
86
|
+
]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### FastAPI / Starlette
|
|
90
|
+
|
|
91
|
+
In your `main.py`:
|
|
92
|
+
```python
|
|
93
|
+
from fastapi import FastAPI
|
|
94
|
+
import autotrace
|
|
95
|
+
from autotrace.integrations.fastapi import AutoTraceMiddleware
|
|
96
|
+
|
|
97
|
+
autotrace.init(api_key="<YOUR_API_KEY>")
|
|
98
|
+
|
|
99
|
+
app = FastAPI()
|
|
100
|
+
app.add_middleware(AutoTraceMiddleware)
|
|
101
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
./autotrace/__init__.py
|
|
5
|
+
./autotrace/client.py
|
|
6
|
+
./autotrace/middleware.py
|
|
7
|
+
./autotrace/integrations/__init__.py
|
|
8
|
+
./autotrace/integrations/django.py
|
|
9
|
+
./autotrace/integrations/fastapi.py
|
|
10
|
+
autotrace/__init__.py
|
|
11
|
+
autotrace/client.py
|
|
12
|
+
autotrace/middleware.py
|
|
13
|
+
autotrace/integrations/__init__.py
|
|
14
|
+
autotrace/integrations/django.py
|
|
15
|
+
autotrace/integrations/fastapi.py
|
|
16
|
+
autotrace_py.egg-info/PKG-INFO
|
|
17
|
+
autotrace_py.egg-info/SOURCES.txt
|
|
18
|
+
autotrace_py.egg-info/dependency_links.txt
|
|
19
|
+
autotrace_py.egg-info/requires.txt
|
|
20
|
+
autotrace_py.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
autotrace
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "autotrace-py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AutoTrace Python SDK for autonomous error tracking and AI triage"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "AutoTrace Team", email = "maintainers@autotrace.io"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["error-tracking", "ai-triage", "gemini", "monitoring", "exceptions"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: System :: Monitoring",
|
|
27
|
+
"Topic :: Software Development :: Bug Tracking",
|
|
28
|
+
]
|
|
29
|
+
dependencies = []
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
django = ["django>=3.2"]
|
|
33
|
+
fastapi = ["starlette>=0.14.0"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://autotrace.io"
|
|
37
|
+
Repository = "https://github.com/autotrace/autotrace-py"
|
|
38
|
+
Documentation = "https://docs.autotrace.io"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
package-dir = {"" = "."}
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["."]
|
|
45
|
+
include = ["autotrace*"]
|
|
46
|
+
namespaces = false
|