watchdock-errors 0.2.2__tar.gz → 0.2.4__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.
- {watchdock_errors-0.2.2/src/watchdock_errors.egg-info → watchdock_errors-0.2.4}/PKG-INFO +23 -1
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/README.md +22 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/__init__.py +26 -5
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/event.py +13 -1
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/utils.py +27 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4/src/watchdock_errors.egg-info}/PKG-INFO +23 -1
- watchdock_errors-0.2.4/src/watchdock_errors.egg-info/scm_version.json +8 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/tests/test_event.py +69 -0
- watchdock_errors-0.2.2/src/watchdock_errors.egg-info/scm_version.json +0 -8
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/.github/workflows/ci.yml +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/.github/workflows/release.yml +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/.gitignore +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/pyproject.toml +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/setup.cfg +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/client.py +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/config.py +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/__init__.py +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/django.py +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/fastapi.py +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/SOURCES.txt +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/dependency_links.txt +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/requires.txt +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/scm_file_list.json +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/top_level.txt +0 -0
- {watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: watchdock-errors
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Application error tracking SDK for the Watchdock platform
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: error-tracking,observability,watchdock
|
|
@@ -88,8 +88,30 @@ watchdock_errors.capture_exception(exc)
|
|
|
88
88
|
|
|
89
89
|
# Capture a message
|
|
90
90
|
watchdock_errors.capture_message("Stripe webhook signature invalid")
|
|
91
|
+
|
|
92
|
+
# Capture a message with a custom level
|
|
93
|
+
watchdock_errors.capture_message("Queue depth high", level="warning")
|
|
91
94
|
```
|
|
92
95
|
|
|
96
|
+
### Event levels
|
|
97
|
+
|
|
98
|
+
Every event carries a `level`. Exceptions default to `"error"`; messages default to `"info"` unless you pass `level` explicitly to `capture_message`.
|
|
99
|
+
|
|
100
|
+
## Correlating with nginx requests
|
|
101
|
+
|
|
102
|
+
If your app is behind nginx and you've added `$request_id` to your access log format (see the [nginx log collection docs](https://watchdock.cc/docs/nginx-log-collection)) and forwarded it to your app via `proxy_set_header X-Request-Id $request_id;`, this SDK automatically reads that header off every captured request and attaches it as `trace_id` — no code changes needed. This lets WatchDock link a failed request in your nginx access logs directly to the exception it produced.
|
|
103
|
+
|
|
104
|
+
You can also pass `trace_id` explicitly, which takes priority over the auto-extracted value:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
watchdock_errors.capture_exception(exc, trace_id=my_trace_id)
|
|
108
|
+
watchdock_errors.capture_message("Queue depth high", level="warning", trace_id=my_trace_id)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## SDK initialization
|
|
112
|
+
|
|
113
|
+
When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the platform (with the SDK version and environment) to register that it started up. This never blocks application startup and any failure is silently ignored.
|
|
114
|
+
|
|
93
115
|
## PII scrubbing
|
|
94
116
|
|
|
95
117
|
By default, `Authorization`, `Cookie`, and `X-Api-Key` headers are stripped and the request body is not sent. Set `send_pii=True` to disable scrubbing.
|
|
@@ -68,8 +68,30 @@ watchdock_errors.capture_exception(exc)
|
|
|
68
68
|
|
|
69
69
|
# Capture a message
|
|
70
70
|
watchdock_errors.capture_message("Stripe webhook signature invalid")
|
|
71
|
+
|
|
72
|
+
# Capture a message with a custom level
|
|
73
|
+
watchdock_errors.capture_message("Queue depth high", level="warning")
|
|
71
74
|
```
|
|
72
75
|
|
|
76
|
+
### Event levels
|
|
77
|
+
|
|
78
|
+
Every event carries a `level`. Exceptions default to `"error"`; messages default to `"info"` unless you pass `level` explicitly to `capture_message`.
|
|
79
|
+
|
|
80
|
+
## Correlating with nginx requests
|
|
81
|
+
|
|
82
|
+
If your app is behind nginx and you've added `$request_id` to your access log format (see the [nginx log collection docs](https://watchdock.cc/docs/nginx-log-collection)) and forwarded it to your app via `proxy_set_header X-Request-Id $request_id;`, this SDK automatically reads that header off every captured request and attaches it as `trace_id` — no code changes needed. This lets WatchDock link a failed request in your nginx access logs directly to the exception it produced.
|
|
83
|
+
|
|
84
|
+
You can also pass `trace_id` explicitly, which takes priority over the auto-extracted value:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
watchdock_errors.capture_exception(exc, trace_id=my_trace_id)
|
|
88
|
+
watchdock_errors.capture_message("Queue depth high", level="warning", trace_id=my_trace_id)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## SDK initialization
|
|
92
|
+
|
|
93
|
+
When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the platform (with the SDK version and environment) to register that it started up. This never blocks application startup and any failure is silently ignored.
|
|
94
|
+
|
|
73
95
|
## PII scrubbing
|
|
74
96
|
|
|
75
97
|
By default, `Authorization`, `Cookie`, and `X-Api-Key` headers are stripped and the request body is not sent. Set `send_pii=True` to disable scrubbing.
|
|
@@ -77,13 +77,21 @@ def init(
|
|
|
77
77
|
_send_init_ping(_config)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def capture_exception(
|
|
80
|
+
def capture_exception(
|
|
81
|
+
exc: BaseException | None = None,
|
|
82
|
+
request_context: dict | None = None,
|
|
83
|
+
trace_id: str | None = None,
|
|
84
|
+
) -> None:
|
|
81
85
|
"""
|
|
82
86
|
Capture an exception and send it to Watchdock.
|
|
83
87
|
|
|
84
88
|
If ``exc`` is None the current exception from ``sys.exc_info()`` is used.
|
|
85
89
|
Safe to call outside of an except block — does nothing if there is no
|
|
86
90
|
active exception and ``exc`` is not provided.
|
|
91
|
+
|
|
92
|
+
``trace_id`` correlates this event with the originating nginx request
|
|
93
|
+
(e.g. ``X-Request-Id``) and takes priority over any value auto-extracted
|
|
94
|
+
from ``request_context``'s headers if both are present.
|
|
87
95
|
"""
|
|
88
96
|
if _client is None or _config is None:
|
|
89
97
|
return
|
|
@@ -97,7 +105,7 @@ def capture_exception(exc: BaseException | None = None, request_context: dict |
|
|
|
97
105
|
|
|
98
106
|
from .event import build_event
|
|
99
107
|
|
|
100
|
-
event = build_event(exc, _config, request_context=request_context)
|
|
108
|
+
event = build_event(exc, _config, request_context=request_context, trace_id=trace_id)
|
|
101
109
|
if event is not None:
|
|
102
110
|
logger.info("watchdock_errors: capturing exception — %s: %s", type(exc).__name__, exc)
|
|
103
111
|
_client.capture(event)
|
|
@@ -105,14 +113,27 @@ def capture_exception(exc: BaseException | None = None, request_context: dict |
|
|
|
105
113
|
logger.debug("watchdock_errors: event dropped by before_send hook")
|
|
106
114
|
|
|
107
115
|
|
|
108
|
-
def capture_message(
|
|
109
|
-
|
|
116
|
+
def capture_message(
|
|
117
|
+
message: str,
|
|
118
|
+
level: str = "info",
|
|
119
|
+
request_context: dict | None = None,
|
|
120
|
+
trace_id: str | None = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Capture an arbitrary message string and send it to Watchdock.
|
|
124
|
+
|
|
125
|
+
``trace_id`` correlates this event with the originating nginx request
|
|
126
|
+
(e.g. ``X-Request-Id``) and takes priority over any value auto-extracted
|
|
127
|
+
from ``request_context``'s headers if both are present.
|
|
128
|
+
"""
|
|
110
129
|
if _client is None or _config is None:
|
|
111
130
|
return
|
|
112
131
|
|
|
113
132
|
from .event import build_event
|
|
114
133
|
|
|
115
|
-
event = build_event(
|
|
134
|
+
event = build_event(
|
|
135
|
+
None, _config, message=message, level=level, request_context=request_context, trace_id=trace_id
|
|
136
|
+
)
|
|
116
137
|
if event is not None:
|
|
117
138
|
logger.info("watchdock_errors: capturing message — %s", message)
|
|
118
139
|
_client.capture(event)
|
|
@@ -4,7 +4,7 @@ import datetime
|
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
6
|
from .config import SDKConfig
|
|
7
|
-
from .utils import extract_stacktrace, get_server_info
|
|
7
|
+
from .utils import extract_stacktrace, extract_trace_id, get_server_info
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger("watchdock_errors")
|
|
10
10
|
|
|
@@ -13,7 +13,9 @@ def build_event(
|
|
|
13
13
|
exc: BaseException | None,
|
|
14
14
|
config: SDKConfig,
|
|
15
15
|
message: str | None = None,
|
|
16
|
+
level: str | None = None,
|
|
16
17
|
request_context: dict | None = None,
|
|
18
|
+
trace_id: str | None = None,
|
|
17
19
|
) -> dict | None:
|
|
18
20
|
"""
|
|
19
21
|
Build a Watchdock error event payload.
|
|
@@ -22,6 +24,9 @@ def build_event(
|
|
|
22
24
|
"""
|
|
23
25
|
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
24
26
|
|
|
27
|
+
if level is None:
|
|
28
|
+
level = "error" if exc is not None else "info"
|
|
29
|
+
|
|
25
30
|
if exc is not None:
|
|
26
31
|
exception_data = {
|
|
27
32
|
"type": type(exc).__name__,
|
|
@@ -41,6 +46,7 @@ def build_event(
|
|
|
41
46
|
"project_key": config.api_key,
|
|
42
47
|
"timestamp": now,
|
|
43
48
|
"environment": config.environment,
|
|
49
|
+
"level": level,
|
|
44
50
|
"title": title,
|
|
45
51
|
"sdk": {
|
|
46
52
|
"name": config.sdk_name,
|
|
@@ -56,6 +62,12 @@ def build_event(
|
|
|
56
62
|
if config.server_name:
|
|
57
63
|
event["server"]["server_name"] = config.server_name
|
|
58
64
|
|
|
65
|
+
resolved_trace_id = trace_id or (
|
|
66
|
+
extract_trace_id(request_context.get("request", {}).get("headers", {})) if request_context else None
|
|
67
|
+
)
|
|
68
|
+
if resolved_trace_id:
|
|
69
|
+
event["trace_id"] = resolved_trace_id
|
|
70
|
+
|
|
59
71
|
if request_context:
|
|
60
72
|
if config.send_pii:
|
|
61
73
|
event["request"] = request_context.get("request", {})
|
|
@@ -31,3 +31,30 @@ def get_server_info() -> dict:
|
|
|
31
31
|
"hostname": socket.gethostname(),
|
|
32
32
|
"python_version": platform.python_version(),
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def extract_trace_id(headers: dict | None) -> str | None:
|
|
37
|
+
"""
|
|
38
|
+
Pull a correlation ID off incoming request headers so this event can be
|
|
39
|
+
linked back to the nginx access/error log line for the same request.
|
|
40
|
+
|
|
41
|
+
Prefers ``X-Request-Id`` (nginx's built-in ``$request_id``, zero extra
|
|
42
|
+
modules required) and falls back to the trace-id segment of a W3C
|
|
43
|
+
``traceparent`` header if present.
|
|
44
|
+
"""
|
|
45
|
+
if not headers:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
normalized = {str(k).lower(): v for k, v in headers.items()}
|
|
49
|
+
|
|
50
|
+
request_id = normalized.get("x-request-id")
|
|
51
|
+
if request_id:
|
|
52
|
+
return request_id
|
|
53
|
+
|
|
54
|
+
traceparent = normalized.get("traceparent")
|
|
55
|
+
if traceparent:
|
|
56
|
+
parts = traceparent.split("-")
|
|
57
|
+
if len(parts) >= 2 and parts[1]:
|
|
58
|
+
return parts[1]
|
|
59
|
+
|
|
60
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: watchdock-errors
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Application error tracking SDK for the Watchdock platform
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: error-tracking,observability,watchdock
|
|
@@ -88,8 +88,30 @@ watchdock_errors.capture_exception(exc)
|
|
|
88
88
|
|
|
89
89
|
# Capture a message
|
|
90
90
|
watchdock_errors.capture_message("Stripe webhook signature invalid")
|
|
91
|
+
|
|
92
|
+
# Capture a message with a custom level
|
|
93
|
+
watchdock_errors.capture_message("Queue depth high", level="warning")
|
|
91
94
|
```
|
|
92
95
|
|
|
96
|
+
### Event levels
|
|
97
|
+
|
|
98
|
+
Every event carries a `level`. Exceptions default to `"error"`; messages default to `"info"` unless you pass `level` explicitly to `capture_message`.
|
|
99
|
+
|
|
100
|
+
## Correlating with nginx requests
|
|
101
|
+
|
|
102
|
+
If your app is behind nginx and you've added `$request_id` to your access log format (see the [nginx log collection docs](https://watchdock.cc/docs/nginx-log-collection)) and forwarded it to your app via `proxy_set_header X-Request-Id $request_id;`, this SDK automatically reads that header off every captured request and attaches it as `trace_id` — no code changes needed. This lets WatchDock link a failed request in your nginx access logs directly to the exception it produced.
|
|
103
|
+
|
|
104
|
+
You can also pass `trace_id` explicitly, which takes priority over the auto-extracted value:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
watchdock_errors.capture_exception(exc, trace_id=my_trace_id)
|
|
108
|
+
watchdock_errors.capture_message("Queue depth high", level="warning", trace_id=my_trace_id)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## SDK initialization
|
|
112
|
+
|
|
113
|
+
When `init()` is called, the SDK sends a one-time, fire-and-forget ping to the platform (with the SDK version and environment) to register that it started up. This never blocks application startup and any failure is silently ignored.
|
|
114
|
+
|
|
93
115
|
## PII scrubbing
|
|
94
116
|
|
|
95
117
|
By default, `Authorization`, `Cookie`, and `X-Api-Key` headers are stripped and the request body is not sent. Set `send_pii=True` to disable scrubbing.
|
|
@@ -30,6 +30,24 @@ def test_build_event_from_message(config):
|
|
|
30
30
|
assert event is not None
|
|
31
31
|
assert event["exception"]["message"] == "Stripe webhook failed"
|
|
32
32
|
assert event["exception"]["type"] == "Message"
|
|
33
|
+
assert event["level"] == "info"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_build_event_level_override(config):
|
|
37
|
+
event = build_event(None, config, message="disk almost full", level="warning")
|
|
38
|
+
|
|
39
|
+
assert event is not None
|
|
40
|
+
assert event["level"] == "warning"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_build_event_exception_default_level(config):
|
|
44
|
+
try:
|
|
45
|
+
raise ValueError("bad")
|
|
46
|
+
except ValueError as exc:
|
|
47
|
+
event = build_event(exc, config)
|
|
48
|
+
|
|
49
|
+
assert event is not None
|
|
50
|
+
assert event["level"] == "error"
|
|
33
51
|
|
|
34
52
|
|
|
35
53
|
def test_before_send_can_drop_event(config):
|
|
@@ -76,3 +94,54 @@ def test_pii_headers_scrubbed_by_default(config):
|
|
|
76
94
|
|
|
77
95
|
assert "Authorization" not in event["request"]["headers"]
|
|
78
96
|
assert event["request"]["headers"]["Content-Type"] == "application/json"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_trace_id_extracted_from_x_request_id_header(config):
|
|
100
|
+
req_ctx = {
|
|
101
|
+
"request": {
|
|
102
|
+
"method": "GET",
|
|
103
|
+
"url": "/checkout/",
|
|
104
|
+
"headers": {"X-Request-Id": "abc123"},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
raise ValueError("x")
|
|
110
|
+
except ValueError as exc:
|
|
111
|
+
event = build_event(exc, config, request_context=req_ctx)
|
|
112
|
+
|
|
113
|
+
assert event["trace_id"] == "abc123"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_trace_id_falls_back_to_traceparent_header(config):
|
|
117
|
+
req_ctx = {
|
|
118
|
+
"request": {
|
|
119
|
+
"headers": {"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
event = build_event(None, config, message="hi", request_context=req_ctx)
|
|
124
|
+
|
|
125
|
+
assert event["trace_id"] == "4bf92f3577b34da6a3ce929d0e0e4736"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_trace_id_absent_when_no_matching_header(config):
|
|
129
|
+
req_ctx = {"request": {"headers": {"Content-Type": "application/json"}}}
|
|
130
|
+
|
|
131
|
+
event = build_event(None, config, message="hi", request_context=req_ctx)
|
|
132
|
+
|
|
133
|
+
assert "trace_id" not in event
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_explicit_trace_id_wins_over_header_extraction(config):
|
|
137
|
+
req_ctx = {"request": {"headers": {"X-Request-Id": "from-header"}}}
|
|
138
|
+
|
|
139
|
+
event = build_event(None, config, message="hi", request_context=req_ctx, trace_id="explicit-id")
|
|
140
|
+
|
|
141
|
+
assert event["trace_id"] == "explicit-id"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_explicit_trace_id_works_without_request_context(config):
|
|
145
|
+
event = build_event(None, config, message="hi", trace_id="explicit-id")
|
|
146
|
+
|
|
147
|
+
assert event["trace_id"] == "explicit-id"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/__init__.py
RENAMED
|
File without changes
|
{watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/django.py
RENAMED
|
File without changes
|
{watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/requires.txt
RENAMED
|
File without changes
|
{watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/scm_file_list.json
RENAMED
|
File without changes
|
{watchdock_errors-0.2.2 → watchdock_errors-0.2.4}/src/watchdock_errors.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|