devalerts 0.2.1__tar.gz → 0.2.3__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.1 → devalerts-0.2.3}/PKG-INFO +19 -5
- {devalerts-0.2.1 → devalerts-0.2.3}/README.md +18 -4
- {devalerts-0.2.1 → devalerts-0.2.3}/pyproject.toml +1 -1
- {devalerts-0.2.1 → devalerts-0.2.3}/src/devalerts/__init__.py +14 -2
- {devalerts-0.2.1 → devalerts-0.2.3}/src/devalerts/cli.py +5 -2
- {devalerts-0.2.1 → devalerts-0.2.3}/LICENSE +0 -0
- {devalerts-0.2.1 → devalerts-0.2.3}/src/devalerts/_alert.py +0 -0
- {devalerts-0.2.1 → devalerts-0.2.3}/src/devalerts/_celery.py +0 -0
- {devalerts-0.2.1 → devalerts-0.2.3}/src/devalerts/_store.py +0 -0
- {devalerts-0.2.1 → devalerts-0.2.3}/src/devalerts/_telegram.py +0 -0
- {devalerts-0.2.1 → devalerts-0.2.3}/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.3
|
|
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
|
|
@@ -51,6 +51,8 @@ That's the whole setup. Two minutes with [@BotFather](https://t.me/BotFather)
|
|
|
51
51
|
and every unhandled crash — including ones raised in threads — lands in your
|
|
52
52
|
chat instead of a log file nobody's watching.
|
|
53
53
|
|
|
54
|
+

|
|
55
|
+
|
|
54
56
|
## Why devalerts
|
|
55
57
|
|
|
56
58
|
- **Zero infrastructure.** No SaaS signup, no ingestion server, no API key
|
|
@@ -151,15 +153,27 @@ devalerts.init(bot_token="...", chat_id=123456789, tags={"env": "production"})
|
|
|
151
153
|
🖥️ prod-web-2 (env=production)
|
|
152
154
|
```
|
|
153
155
|
|
|
154
|
-
Add ad-hoc tags to a single call — they override `init()`'s tags
|
|
155
|
-
collision:
|
|
156
|
+
Add ad-hoc tags to a single call — they override `init()`'s tags (and each
|
|
157
|
+
other) on a key collision:
|
|
156
158
|
|
|
157
159
|
```python
|
|
158
160
|
devalerts.report(extra={"request_id": "abc123"})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Used as a decorator, `capture()` tags the alert with the wrapped function's
|
|
164
|
+
name as `job` automatically — no `extra` needed:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
@devalerts.capture()
|
|
168
|
+
def nightly_sync(): ...
|
|
169
|
+
```
|
|
159
170
|
|
|
160
|
-
@devalerts.capture(extra={"job": "nightly-sync"})
|
|
161
|
-
def run_job(): ...
|
|
162
171
|
```
|
|
172
|
+
🔴 ValueError: boom
|
|
173
|
+
🖥️ prod-web-2 (job=nightly_sync)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Pass `extra={"job": "..."}` explicitly to override the auto-detected name.
|
|
163
177
|
|
|
164
178
|
## Manually reporting a caught exception
|
|
165
179
|
|
|
@@ -24,6 +24,8 @@ That's the whole setup. Two minutes with [@BotFather](https://t.me/BotFather)
|
|
|
24
24
|
and every unhandled crash — including ones raised in threads — lands in your
|
|
25
25
|
chat instead of a log file nobody's watching.
|
|
26
26
|
|
|
27
|
+

|
|
28
|
+
|
|
27
29
|
## Why devalerts
|
|
28
30
|
|
|
29
31
|
- **Zero infrastructure.** No SaaS signup, no ingestion server, no API key
|
|
@@ -124,15 +126,27 @@ devalerts.init(bot_token="...", chat_id=123456789, tags={"env": "production"})
|
|
|
124
126
|
🖥️ prod-web-2 (env=production)
|
|
125
127
|
```
|
|
126
128
|
|
|
127
|
-
Add ad-hoc tags to a single call — they override `init()`'s tags
|
|
128
|
-
collision:
|
|
129
|
+
Add ad-hoc tags to a single call — they override `init()`'s tags (and each
|
|
130
|
+
other) on a key collision:
|
|
129
131
|
|
|
130
132
|
```python
|
|
131
133
|
devalerts.report(extra={"request_id": "abc123"})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Used as a decorator, `capture()` tags the alert with the wrapped function's
|
|
137
|
+
name as `job` automatically — no `extra` needed:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
@devalerts.capture()
|
|
141
|
+
def nightly_sync(): ...
|
|
142
|
+
```
|
|
132
143
|
|
|
133
|
-
@devalerts.capture(extra={"job": "nightly-sync"})
|
|
134
|
-
def run_job(): ...
|
|
135
144
|
```
|
|
145
|
+
🔴 ValueError: boom
|
|
146
|
+
🖥️ prod-web-2 (job=nightly_sync)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Pass `extra={"job": "..."}` explicitly to override the auto-detected name.
|
|
136
150
|
|
|
137
151
|
## Manually reporting a caught exception
|
|
138
152
|
|
|
@@ -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
|
|
|
@@ -27,7 +27,7 @@ _LOCATION_WIDTH = 34
|
|
|
27
27
|
def _supports_unicode() -> bool:
|
|
28
28
|
encoding = getattr(sys.stdout, "encoding", None) or ""
|
|
29
29
|
try:
|
|
30
|
-
"
|
|
30
|
+
"─●…×".encode(encoding)
|
|
31
31
|
return True
|
|
32
32
|
except (LookupError, UnicodeEncodeError, TypeError):
|
|
33
33
|
return False
|
|
@@ -153,6 +153,7 @@ def _dashboard(as_json: bool = False) -> int:
|
|
|
153
153
|
sep_char = "─" if unicode_ok else "-"
|
|
154
154
|
dot_char = "●" if unicode_ok else "*"
|
|
155
155
|
ellipsis = "…" if unicode_ok else "..."
|
|
156
|
+
times_char = "×" if unicode_ok else "x"
|
|
156
157
|
|
|
157
158
|
style = _Style(_color_enabled())
|
|
158
159
|
type_width = max(len("TYPE"), *(len(r[1]) for r in rows))
|
|
@@ -178,7 +179,9 @@ def _dashboard(as_json: bool = False) -> int:
|
|
|
178
179
|
muted,
|
|
179
180
|
backoff_multiplier,
|
|
180
181
|
) in rows:
|
|
181
|
-
backoff_suffix =
|
|
182
|
+
backoff_suffix = (
|
|
183
|
+
f" {times_char}{backoff_multiplier}" if backoff_multiplier > 1 else ""
|
|
184
|
+
)
|
|
182
185
|
if muted:
|
|
183
186
|
muted_count += 1
|
|
184
187
|
status = style.dim(f"{dot_char} muted")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|