ghosttrap-sdk 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ghosttrap/__init__.py
ADDED
ghosttrap/client.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Drop-in error reporter for ghosttrap.io.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
import ghosttrap
|
|
5
|
+
|
|
6
|
+
# with a token (recommended):
|
|
7
|
+
ghosttrap.init("t_abc123def456")
|
|
8
|
+
|
|
9
|
+
# or with a full URL:
|
|
10
|
+
ghosttrap.init("https://ghosttrap.io/trap/owner/repo/")
|
|
11
|
+
|
|
12
|
+
Hooks into sys.excepthook (unhandled exceptions), Python logging
|
|
13
|
+
(logger.exception / logger.error with exc_info), and Celery task
|
|
14
|
+
failures (if Celery is installed). For additional coverage, use the
|
|
15
|
+
Django middleware: ghosttrap.middleware.GhostTrapMiddleware.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import sys
|
|
21
|
+
import traceback
|
|
22
|
+
import urllib.request
|
|
23
|
+
|
|
24
|
+
GHOSTTRAP_SERVER = "https://ghosttrap.io"
|
|
25
|
+
|
|
26
|
+
_endpoint = None
|
|
27
|
+
_original_excepthook = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def init(dsn, server=None):
|
|
31
|
+
"""Configure the reporter.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
dsn: Either a token (e.g. "t_abc123") or a full URL
|
|
35
|
+
(e.g. "https://ghosttrap.io/trap/owner/repo/")
|
|
36
|
+
server: Base server URL. Only needed if dsn is a token and
|
|
37
|
+
you're not using ghosttrap.io.
|
|
38
|
+
"""
|
|
39
|
+
global _endpoint, _original_excepthook
|
|
40
|
+
if dsn.startswith("http://") or dsn.startswith("https://"):
|
|
41
|
+
_endpoint = dsn.rstrip("/") + "/"
|
|
42
|
+
else:
|
|
43
|
+
base = (server or GHOSTTRAP_SERVER).rstrip("/")
|
|
44
|
+
_endpoint = f"{base}/trap/{dsn}/"
|
|
45
|
+
_original_excepthook = sys.excepthook
|
|
46
|
+
sys.excepthook = _error_hook
|
|
47
|
+
_install_logging_handler()
|
|
48
|
+
_install_celery_hook()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def report(exc):
|
|
52
|
+
"""Report a caught exception to the ghosttrap server.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
exc: the exception instance from an `except Exception as exc` block
|
|
56
|
+
"""
|
|
57
|
+
if _endpoint is None:
|
|
58
|
+
return
|
|
59
|
+
_post(_build_payload(type(exc), exc, exc.__traceback__))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _error_hook(exc_type, exc_value, exc_tb):
|
|
63
|
+
_post(_build_payload(exc_type, exc_value, exc_tb))
|
|
64
|
+
_original_excepthook(exc_type, exc_value, exc_tb)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _install_logging_handler():
|
|
68
|
+
handler = _GhostTrapLogHandler()
|
|
69
|
+
handler.setLevel(logging.ERROR)
|
|
70
|
+
logging.getLogger().addHandler(handler)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _install_celery_hook():
|
|
74
|
+
try:
|
|
75
|
+
from celery.signals import task_failure
|
|
76
|
+
task_failure.connect(_on_task_failure)
|
|
77
|
+
except ImportError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _on_task_failure(sender, exception, traceback, **kwargs):
|
|
82
|
+
report(exception)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _GhostTrapLogHandler(logging.Handler):
|
|
86
|
+
def emit(self, record):
|
|
87
|
+
if record.exc_info and record.exc_info[1]:
|
|
88
|
+
report(record.exc_info[1])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _build_payload(exc_type, exc_value, exc_tb):
|
|
92
|
+
frames = traceback.extract_tb(exc_tb) if exc_tb else []
|
|
93
|
+
return {
|
|
94
|
+
"type": exc_type.__name__,
|
|
95
|
+
"message": str(exc_value),
|
|
96
|
+
"traceback": traceback.format_exception(exc_type, exc_value, exc_tb) if exc_tb else [],
|
|
97
|
+
"frames": [
|
|
98
|
+
{
|
|
99
|
+
"file": f.filename,
|
|
100
|
+
"line": f.lineno,
|
|
101
|
+
"function": f.name,
|
|
102
|
+
"code": f.line,
|
|
103
|
+
}
|
|
104
|
+
for f in frames
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _post(payload):
|
|
110
|
+
try:
|
|
111
|
+
req = urllib.request.Request(
|
|
112
|
+
_endpoint,
|
|
113
|
+
data=json.dumps(payload).encode(),
|
|
114
|
+
headers={"Content-Type": "application/json"},
|
|
115
|
+
)
|
|
116
|
+
urllib.request.urlopen(req, timeout=5)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
ghosttrap/django.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Django integration for ghosttrap.
|
|
2
|
+
|
|
3
|
+
Add to INSTALLED_APPS:
|
|
4
|
+
|
|
5
|
+
INSTALLED_APPS = [
|
|
6
|
+
...
|
|
7
|
+
"ghosttrap.django.GhostTrapApp",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
This re-attaches the ghosttrap logging handler after Django's
|
|
11
|
+
dictConfig runs (which typically clobbers handlers added during
|
|
12
|
+
init()). Also provides the GhostTrapMiddleware for catching
|
|
13
|
+
unhandled view exceptions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from django.apps import AppConfig
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GhostTrapApp(AppConfig):
|
|
22
|
+
name = "ghosttrap.django"
|
|
23
|
+
label = "ghosttrap_django"
|
|
24
|
+
verbose_name = "Ghosttrap"
|
|
25
|
+
|
|
26
|
+
def ready(self):
|
|
27
|
+
from ghosttrap.client import _install_logging_handler
|
|
28
|
+
_install_logging_handler()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GhostTrapMiddleware:
|
|
32
|
+
"""Catches unhandled view exceptions and reports them."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, get_response):
|
|
35
|
+
self.get_response = get_response
|
|
36
|
+
|
|
37
|
+
def __call__(self, request):
|
|
38
|
+
return self.get_response(request)
|
|
39
|
+
|
|
40
|
+
def process_exception(self, request, exception):
|
|
41
|
+
from ghosttrap.client import report
|
|
42
|
+
report(exception)
|
|
43
|
+
return None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ghosttrap-sdk
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Drop-in error reporter for ghosttrap.io
|
|
5
|
+
Project-URL: Homepage, https://github.com/arowley-predictive-power/ghosttrap-sdk
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# ghosttrap-sdk
|
|
10
|
+
|
|
11
|
+
Drop-in error reporter for Python apps.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pip install ghosttrap-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import ghosttrap
|
|
23
|
+
|
|
24
|
+
# with a token (get one from `ghosttrap watch`):
|
|
25
|
+
ghosttrap.init("t_abc123def456")
|
|
26
|
+
|
|
27
|
+
# or with a full URL:
|
|
28
|
+
ghosttrap.init("https://ghosttrap.io/trap/owner/repo/")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Unhandled exceptions are POSTed to ghosttrap.io with full tracebacks.
|
|
32
|
+
The original `sys.excepthook` is preserved so errors still print to
|
|
33
|
+
stderr as normal.
|
|
34
|
+
|
|
35
|
+
For caught exceptions inside web frameworks:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
try:
|
|
39
|
+
do_something()
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
ghosttrap.report(exc)
|
|
42
|
+
raise
|
|
43
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ghosttrap/__init__.py,sha256=ajn2AhpcLvTMBSwP_lJkzfCYVIqdy7UEbiU6FCsMlac,72
|
|
2
|
+
ghosttrap/client.py,sha256=r2_N7OE5Wk_3dYdg-1Y5-exWJTNcd2DVzTz0XRd1MYU,3181
|
|
3
|
+
ghosttrap/django.py,sha256=KahzTZJzk_6VAgtSlCM1kD-PjOlzM6CATzv_CQgliAE,1048
|
|
4
|
+
ghosttrap_sdk-0.4.0.dist-info/METADATA,sha256=sXGSaik13WJAEwmgz9o8TxBCCX2dUTFq7TVBfSV-vEE,861
|
|
5
|
+
ghosttrap_sdk-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
ghosttrap_sdk-0.4.0.dist-info/top_level.txt,sha256=6ke_r_vrKe4pvkNG8uXCLoWy3PT1FnuZiMjbvcJa_5c,10
|
|
7
|
+
ghosttrap_sdk-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ghosttrap
|