devalerts 0.2.0__tar.gz → 0.2.2__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.
- {devalerts-0.2.0 → devalerts-0.2.2}/PKG-INFO +21 -5
- {devalerts-0.2.0 → devalerts-0.2.2}/README.md +20 -4
- {devalerts-0.2.0 → devalerts-0.2.2}/pyproject.toml +1 -1
- {devalerts-0.2.0 → devalerts-0.2.2}/src/devalerts/__init__.py +14 -2
- {devalerts-0.2.0 → devalerts-0.2.2}/src/devalerts/_alert.py +23 -10
- {devalerts-0.2.0 → devalerts-0.2.2}/src/devalerts/_telegram.py +3 -1
- {devalerts-0.2.0 → devalerts-0.2.2}/LICENSE +0 -0
- {devalerts-0.2.0 → devalerts-0.2.2}/src/devalerts/_celery.py +0 -0
- {devalerts-0.2.0 → devalerts-0.2.2}/src/devalerts/_store.py +0 -0
- {devalerts-0.2.0 → devalerts-0.2.2}/src/devalerts/cli.py +0 -0
- {devalerts-0.2.0 → devalerts-0.2.2}/src/devalerts/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devalerts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Send unhandled Python exceptions straight to a Telegram chat. No backend, no account, no database — just your own bot token.
|
|
5
5
|
Keywords: telegram,error-tracking,exception-monitoring,alerts,logging,monitoring,asgi
|
|
6
6
|
Author: sslinNn
|
|
@@ -94,6 +94,10 @@ devalerts.init(bot_token="123456:ABC-DEF...", chat_id=123456789)
|
|
|
94
94
|
That's it — any unhandled exception (including ones raised in threads) now
|
|
95
95
|
also lands in your Telegram chat.
|
|
96
96
|
|
|
97
|
+
The traceback itself arrives folded into a collapsed quote — the exception
|
|
98
|
+
type, message, and host are visible right away, tap to expand the full
|
|
99
|
+
traceback. Keeps a big stack trace from taking over the chat.
|
|
100
|
+
|
|
97
101
|
## Grouping, rate limiting, and the dashboard
|
|
98
102
|
|
|
99
103
|
Exceptions are grouped by fingerprint (exception type + file + line where it
|
|
@@ -147,15 +151,27 @@ devalerts.init(bot_token="...", chat_id=123456789, tags={"env": "production"})
|
|
|
147
151
|
🖥️ prod-web-2 (env=production)
|
|
148
152
|
```
|
|
149
153
|
|
|
150
|
-
Add ad-hoc tags to a single call — they override `init()`'s tags
|
|
151
|
-
collision:
|
|
154
|
+
Add ad-hoc tags to a single call — they override `init()`'s tags (and each
|
|
155
|
+
other) on a key collision:
|
|
152
156
|
|
|
153
157
|
```python
|
|
154
158
|
devalerts.report(extra={"request_id": "abc123"})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Used as a decorator, `capture()` tags the alert with the wrapped function's
|
|
162
|
+
name as `job` automatically — no `extra` needed:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
@devalerts.capture()
|
|
166
|
+
def nightly_sync(): ...
|
|
167
|
+
```
|
|
155
168
|
|
|
156
|
-
@devalerts.capture(extra={"job": "nightly-sync"})
|
|
157
|
-
def run_job(): ...
|
|
158
169
|
```
|
|
170
|
+
🔴 ValueError: boom
|
|
171
|
+
🖥️ prod-web-2 (job=nightly_sync)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Pass `extra={"job": "..."}` explicitly to override the auto-detected name.
|
|
159
175
|
|
|
160
176
|
## Manually reporting a caught exception
|
|
161
177
|
|
|
@@ -67,6 +67,10 @@ devalerts.init(bot_token="123456:ABC-DEF...", chat_id=123456789)
|
|
|
67
67
|
That's it — any unhandled exception (including ones raised in threads) now
|
|
68
68
|
also lands in your Telegram chat.
|
|
69
69
|
|
|
70
|
+
The traceback itself arrives folded into a collapsed quote — the exception
|
|
71
|
+
type, message, and host are visible right away, tap to expand the full
|
|
72
|
+
traceback. Keeps a big stack trace from taking over the chat.
|
|
73
|
+
|
|
70
74
|
## Grouping, rate limiting, and the dashboard
|
|
71
75
|
|
|
72
76
|
Exceptions are grouped by fingerprint (exception type + file + line where it
|
|
@@ -120,15 +124,27 @@ devalerts.init(bot_token="...", chat_id=123456789, tags={"env": "production"})
|
|
|
120
124
|
🖥️ prod-web-2 (env=production)
|
|
121
125
|
```
|
|
122
126
|
|
|
123
|
-
Add ad-hoc tags to a single call — they override `init()`'s tags
|
|
124
|
-
collision:
|
|
127
|
+
Add ad-hoc tags to a single call — they override `init()`'s tags (and each
|
|
128
|
+
other) on a key collision:
|
|
125
129
|
|
|
126
130
|
```python
|
|
127
131
|
devalerts.report(extra={"request_id": "abc123"})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Used as a decorator, `capture()` tags the alert with the wrapped function's
|
|
135
|
+
name as `job` automatically — no `extra` needed:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
@devalerts.capture()
|
|
139
|
+
def nightly_sync(): ...
|
|
140
|
+
```
|
|
128
141
|
|
|
129
|
-
@devalerts.capture(extra={"job": "nightly-sync"})
|
|
130
|
-
def run_job(): ...
|
|
131
142
|
```
|
|
143
|
+
🔴 ValueError: boom
|
|
144
|
+
🖥️ prod-web-2 (job=nightly_sync)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Pass `extra={"job": "..."}` explicitly to override the auto-detected name.
|
|
132
148
|
|
|
133
149
|
## Manually reporting a caught exception
|
|
134
150
|
|
|
@@ -130,17 +130,29 @@ def report(
|
|
|
130
130
|
class capture(contextlib.ContextDecorator):
|
|
131
131
|
"""Context manager / decorator: report any exception raised inside the block or
|
|
132
132
|
function, then re-raise it. Use ``@capture()`` on a function instead of wrapping
|
|
133
|
-
its body in a manual ``try/except`` or ``with`` block.
|
|
133
|
+
its body in a manual ``try/except`` or ``with`` block.
|
|
134
|
+
|
|
135
|
+
Used as a decorator, the wrapped function's name is tagged automatically as
|
|
136
|
+
``job`` -- no need to pass ``extra={"job": ...}`` by hand. Used as a bare
|
|
137
|
+
``with capture():`` block, there's no function to name, so only explicit
|
|
138
|
+
``extra`` tags apply."""
|
|
134
139
|
|
|
135
140
|
def __init__(self, *, extra: dict[str, str] | None = None) -> None:
|
|
136
141
|
self._extra = extra
|
|
142
|
+
self._job_name: str | None = None
|
|
143
|
+
|
|
144
|
+
def __call__(self, func):
|
|
145
|
+
self._job_name = func.__qualname__
|
|
146
|
+
return super().__call__(func)
|
|
137
147
|
|
|
138
148
|
def __enter__(self) -> "capture":
|
|
139
149
|
return self
|
|
140
150
|
|
|
141
151
|
def __exit__(self, exc_type, exc_value, tb) -> Literal[False]:
|
|
142
152
|
if exc_type is not None:
|
|
143
|
-
|
|
153
|
+
tags = {"job": self._job_name} if self._job_name else {}
|
|
154
|
+
tags.update(self._extra or {})
|
|
155
|
+
_send_exception(exc_type, exc_value, tb, extra=tags)
|
|
144
156
|
return False
|
|
145
157
|
|
|
146
158
|
|
|
@@ -9,6 +9,10 @@ import traceback
|
|
|
9
9
|
_MAX_MESSAGE_LENGTH = 4096
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def _escape_html(text: str) -> str:
|
|
13
|
+
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
14
|
+
|
|
15
|
+
|
|
12
16
|
def _format_context(tags: dict[str, str] | None) -> str:
|
|
13
17
|
try:
|
|
14
18
|
hostname = socket.gethostname()
|
|
@@ -27,16 +31,25 @@ def _format_alert(
|
|
|
27
31
|
if skipped:
|
|
28
32
|
header += f"\n⚠️ Повторилась ещё {skipped} раз(а) с последнего алерта"
|
|
29
33
|
body = "".join(traceback.format_exception(exc_type, exc_value, tb))
|
|
30
|
-
|
|
31
|
-
if len(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
|
|
35
|
+
if len(header) + 2 + len(body) > _MAX_MESSAGE_LENGTH:
|
|
36
|
+
marker = "\n\n...(truncated)...\n"
|
|
37
|
+
# ponytail: header itself can exceed the limit if exc_value's str() is huge
|
|
38
|
+
# (e.g. a validation error echoing a large payload) — keep can go negative,
|
|
39
|
+
# so clamp it; the header itself gets hard-truncated as a backstop. The "2"
|
|
40
|
+
# accounts for the "\n\n" separator joined in below.
|
|
41
|
+
keep = max(_MAX_MESSAGE_LENGTH - len(header) - 2 - len(marker), 0)
|
|
42
|
+
body = f"{marker}{body[-keep:]}" if keep else ""
|
|
43
|
+
header = header[:_MAX_MESSAGE_LENGTH]
|
|
44
|
+
|
|
45
|
+
# Budgeted above against the plain text -- Telegram's 4096-char limit
|
|
46
|
+
# applies to the parsed (tag-stripped) text, not the raw HTML we send.
|
|
47
|
+
escaped_header = _escape_html(header)
|
|
48
|
+
if not body:
|
|
49
|
+
return escaped_header
|
|
50
|
+
return (
|
|
51
|
+
f"{escaped_header}\n\n<blockquote expandable>{_escape_html(body)}</blockquote>"
|
|
52
|
+
)
|
|
40
53
|
|
|
41
54
|
|
|
42
55
|
# ponytail: fixed pattern list, not exhaustive — catches common
|
|
@@ -40,7 +40,9 @@ def _log_failed_delivery(text: str) -> None:
|
|
|
40
40
|
|
|
41
41
|
def _send_telegram_message(bot_token: str, chat_id: int | str, text: str) -> bool:
|
|
42
42
|
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
|
43
|
-
payload = json.dumps(
|
|
43
|
+
payload = json.dumps(
|
|
44
|
+
{"chat_id": chat_id, "text": text, "parse_mode": "HTML"}
|
|
45
|
+
).encode("utf-8")
|
|
44
46
|
last_error: Exception | None = None
|
|
45
47
|
|
|
46
48
|
for attempt in range(_MAX_ATTEMPTS):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|