logalert 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.
Files changed (58) hide show
  1. logalert-0.1.0/.github/workflows/ci.yml +75 -0
  2. logalert-0.1.0/.github/workflows/release.yml +19 -0
  3. logalert-0.1.0/.gitignore +25 -0
  4. logalert-0.1.0/LICENSE +21 -0
  5. logalert-0.1.0/Makefile +37 -0
  6. logalert-0.1.0/PKG-INFO +336 -0
  7. logalert-0.1.0/README.md +289 -0
  8. logalert-0.1.0/docker-compose.yml +14 -0
  9. logalert-0.1.0/examples/custom_sink.py +123 -0
  10. logalert-0.1.0/examples/dictconfig.py +71 -0
  11. logalert-0.1.0/examples/hooks.py +117 -0
  12. logalert-0.1.0/examples/matrix.py +66 -0
  13. logalert-0.1.0/examples/redis_worker.py +72 -0
  14. logalert-0.1.0/examples/telegram.py +83 -0
  15. logalert-0.1.0/pyproject.toml +100 -0
  16. logalert-0.1.0/src/logalert/__init__.py +356 -0
  17. logalert-0.1.0/src/logalert/_dedupe.py +274 -0
  18. logalert-0.1.0/src/logalert/_delivery.py +424 -0
  19. logalert-0.1.0/src/logalert/_fingerprint.py +230 -0
  20. logalert-0.1.0/src/logalert/_guard.py +126 -0
  21. logalert-0.1.0/src/logalert/_privacy.py +252 -0
  22. logalert-0.1.0/src/logalert/_queue.py +279 -0
  23. logalert-0.1.0/src/logalert/_state.py +62 -0
  24. logalert-0.1.0/src/logalert/_version.py +7 -0
  25. logalert-0.1.0/src/logalert/capture.py +728 -0
  26. logalert-0.1.0/src/logalert/cli.py +453 -0
  27. logalert-0.1.0/src/logalert/client.py +473 -0
  28. logalert-0.1.0/src/logalert/exceptions.py +91 -0
  29. logalert-0.1.0/src/logalert/handler.py +213 -0
  30. logalert-0.1.0/src/logalert/options.py +275 -0
  31. logalert-0.1.0/src/logalert/py.typed +0 -0
  32. logalert-0.1.0/src/logalert/render.py +548 -0
  33. logalert-0.1.0/src/logalert/scope.py +306 -0
  34. logalert-0.1.0/src/logalert/sinks/__init__.py +71 -0
  35. logalert-0.1.0/src/logalert/sinks/base.py +135 -0
  36. logalert-0.1.0/src/logalert/sinks/matrix.py +308 -0
  37. logalert-0.1.0/src/logalert/sinks/matrix_e2ee.py +539 -0
  38. logalert-0.1.0/src/logalert/sinks/stderr.py +88 -0
  39. logalert-0.1.0/src/logalert/sinks/telegram.py +359 -0
  40. logalert-0.1.0/src/logalert/transport/__init__.py +34 -0
  41. logalert-0.1.0/src/logalert/transport/base.py +205 -0
  42. logalert-0.1.0/src/logalert/transport/redis.py +525 -0
  43. logalert-0.1.0/src/logalert/types.py +430 -0
  44. logalert-0.1.0/src/logalert/worker.py +161 -0
  45. logalert-0.1.0/tests/conftest.py +193 -0
  46. logalert-0.1.0/tests/live/test_redis.py +409 -0
  47. logalert-0.1.0/tests/test_api.py +341 -0
  48. logalert-0.1.0/tests/test_capture.py +439 -0
  49. logalert-0.1.0/tests/test_dedupe.py +176 -0
  50. logalert-0.1.0/tests/test_fingerprint.py +341 -0
  51. logalert-0.1.0/tests/test_inert.py +143 -0
  52. logalert-0.1.0/tests/test_matrix.py +322 -0
  53. logalert-0.1.0/tests/test_matrix_e2ee.py +796 -0
  54. logalert-0.1.0/tests/test_privacy.py +190 -0
  55. logalert-0.1.0/tests/test_queue.py +145 -0
  56. logalert-0.1.0/tests/test_split.py +116 -0
  57. logalert-0.1.0/tests/test_telegram.py +344 -0
  58. logalert-0.1.0/uv.lock +2038 -0
@@ -0,0 +1,75 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: Tests (Python ${{ matrix.python }})
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python: ["3.12", "3.13", "3.14"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v5
19
+ with:
20
+ python-version: ${{ matrix.python }}
21
+ - run: uv sync --all-extras
22
+ - run: uv run pytest -q
23
+
24
+ lint:
25
+ name: Linter
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v5
30
+ - run: uv sync --all-extras
31
+ - run: uv run ruff check .
32
+ - run: uv run ruff format --check .
33
+
34
+ typecheck:
35
+ name: Types
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - uses: astral-sh/setup-uv@v5
40
+ - run: uv sync --all-extras
41
+ - run: uv run mypy src
42
+
43
+ live:
44
+ name: Tests with Redis
45
+ runs-on: ubuntu-latest
46
+ services:
47
+ redis:
48
+ image: redis:7-alpine
49
+ ports: ["6379:6379"]
50
+ options: >-
51
+ --health-cmd "redis-cli ping"
52
+ --health-interval 5s
53
+ --health-timeout 3s
54
+ --health-retries 5
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ - uses: astral-sh/setup-uv@v5
58
+ - run: uv sync --all-extras
59
+ - run: uv run pytest -q -m live
60
+ env:
61
+ LOGALERT_TEST_REDIS_URL: redis://localhost:6379/0
62
+
63
+ build:
64
+ name: Build
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+ - uses: astral-sh/setup-uv@v5
69
+ - run: uv sync --all-extras
70
+ - run: uv build
71
+ - run: uv run twine check dist/*
72
+ - name: Verify the wheel in a clean environment
73
+ run: |
74
+ uv venv --python 3.12 /tmp/clean
75
+ uv pip install --python /tmp/clean/bin/python --quiet dist/*.whl
@@ -0,0 +1,19 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ name: Publish
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ - run: uv build
18
+ - run: uvx twine check dist/*
19
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+
6
+ .venv/
7
+ venv/
8
+
9
+ dist/
10
+ build/
11
+
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .hypothesis/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ .env
20
+ .env.*
21
+
22
+ *.db
23
+ *.sqlite3
24
+
25
+ .DS_Store
logalert-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Aiko Sora
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ .PHONY: setup test lint format typecheck check test-live build clean redis-up redis-down
2
+
3
+ setup: ## install every extra plus dev dependencies
4
+ uv sync --all-extras
5
+
6
+ test:
7
+ uv run pytest -q
8
+
9
+ lint:
10
+ uv run ruff check .
11
+ uv run ruff format --check .
12
+
13
+ format:
14
+ uv run ruff check --fix .
15
+ uv run ruff format .
16
+
17
+ typecheck:
18
+ uv run mypy src
19
+
20
+ check: lint typecheck test ## what CI runs
21
+
22
+ test-live: ## tests that need real Telegram/Matrix/Redis
23
+ uv run pytest -q -m live
24
+
25
+ redis-up: ## local Redis for the live tests and the worker
26
+ docker compose up -d
27
+
28
+ redis-down:
29
+ docker compose down
30
+
31
+ build:
32
+ uv build
33
+ uv run twine check dist/*
34
+
35
+ clean:
36
+ rm -rf dist build .pytest_cache .mypy_cache .ruff_cache
37
+ find . -name __pycache__ -type d -prune -exec rm -rf {} +
@@ -0,0 +1,336 @@
1
+ Metadata-Version: 2.5
2
+ Name: logalert
3
+ Version: 0.1.0
4
+ Summary: Capture Python errors and deliver them to Telegram or Matrix, with an optional Redis-backed durable queue
5
+ Project-URL: Homepage, https://github.com/Quirkink/logalert
6
+ Project-URL: Repository, https://github.com/Quirkink/logalert
7
+ Project-URL: Issues, https://github.com/Quirkink/logalert/issues
8
+ Author-email: Aiko Sora <altairteam@bk.ru>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: alerts,error-tracking,logging,matrix,observability,redis,telegram,traceback
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: System :: Logging
20
+ Classifier: Topic :: System :: Monitoring
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Provides-Extra: all
24
+ Requires-Dist: httpx>=0.27; extra == 'all'
25
+ Requires-Dist: matrix-nio[e2e]>=0.25; extra == 'all'
26
+ Requires-Dist: redis>=5.2; extra == 'all'
27
+ Provides-Extra: dev
28
+ Requires-Dist: build; extra == 'dev'
29
+ Requires-Dist: fakeredis>=2.26; extra == 'dev'
30
+ Requires-Dist: httpx>=0.27; extra == 'dev'
31
+ Requires-Dist: hypothesis>=6.100; extra == 'dev'
32
+ Requires-Dist: mypy>=1.13; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
34
+ Requires-Dist: pytest-socket>=0.7; extra == 'dev'
35
+ Requires-Dist: pytest>=8.3; extra == 'dev'
36
+ Requires-Dist: ruff>=0.8; extra == 'dev'
37
+ Requires-Dist: twine; extra == 'dev'
38
+ Provides-Extra: matrix
39
+ Requires-Dist: httpx>=0.27; extra == 'matrix'
40
+ Provides-Extra: matrix-e2ee
41
+ Requires-Dist: matrix-nio[e2e]>=0.25; extra == 'matrix-e2ee'
42
+ Provides-Extra: redis
43
+ Requires-Dist: redis>=5.2; extra == 'redis'
44
+ Provides-Extra: telegram
45
+ Requires-Dist: httpx>=0.27; extra == 'telegram'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # Logalert
49
+
50
+ **logalert** is a library that intercepts errors, stack traces, and unhandled exceptions in a Python application and sends them to a configured messenger.
51
+
52
+ ![PyPI - Version](https://img.shields.io/pypi/v/logalert)
53
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/logalert)
54
+
55
+ ___
56
+
57
+ Key features of this module:
58
+
59
+ * **Sinks** - Telegram, Matrix, an encrypted Matrix room, stderr, or your own
60
+ * **Automatic capture** - logging, `excepthook`, threads, asyncio, unraisable hooks
61
+ * **Deduplication** - a suppression window that reports a summary instead of flooding
62
+ * **Durable delivery** - an optional Redis Streams queue that survives a restart
63
+ * **No required dependencies** - extras only, so it never conflicts with the host project
64
+
65
+ ## Example
66
+
67
+ ```python
68
+ import logalert
69
+
70
+ logalert.init(telegram={'token': '...', 'chat_id': '...'})
71
+ ```
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ pip install logalert[telegram] # Telegram
77
+ pip install logalert[matrix] # Matrix, unencrypted room
78
+ pip install logalert[matrix-e2ee] # Matrix with E2E encryption
79
+ pip install logalert[redis] # durable queue
80
+ pip install logalert[all]
81
+ ```
82
+
83
+ ## Quick start
84
+
85
+ ### Telegram
86
+
87
+ A bot cannot message first: until you press `/start`, `sendMessage` returns
88
+ `chat not found`. You can find `chat_id` via
89
+ `https://api.telegram.org/bot<TOKEN>/getUpdates` after writing to the bot.
90
+
91
+ ```python
92
+ import logalert
93
+
94
+ logalert.init(
95
+ telegram={
96
+ 'token': '123456:ABC-DEF...',
97
+ 'chat_id': '-1001234567890',
98
+ # "message_thread_id": 42, # when alerts go to a forum topic
99
+ },
100
+ environment='prod',
101
+ release='order-service@1.4.2',
102
+ )
103
+ ```
104
+
105
+ ### Matrix
106
+
107
+ ```python
108
+ logalert.init(
109
+ matrix={
110
+ 'homeserver': 'https://matrix.example.org',
111
+ 'room_id': '!abcdef:example.org',
112
+ 'access_token': 'syt_...',
113
+ }
114
+ )
115
+ ```
116
+
117
+ Both sinks can be configured at once - the message goes to each:
118
+
119
+ ```python
120
+ logalert.init(telegram={...}, matrix={...})
121
+ ```
122
+
123
+ ### Matrix in an encrypted room
124
+
125
+ The plain sink can only send to unencrypted rooms and will **refuse loudly** to work with
126
+ an encrypted one rather than send plaintext. E2EE needs an extra package and a fuller set
127
+ of parameters:
128
+
129
+ ```bash
130
+ pip install logalert[matrix-e2ee]
131
+ ```
132
+
133
+ ```python
134
+ logalert.init(
135
+ matrix={
136
+ 'homeserver': 'https://matrix.example.org',
137
+ 'room_id': '!abcdef:example.org',
138
+ 'access_token': 'syt_...',
139
+ 'e2ee': True,
140
+ 'user_id': '@bot:example.org', # required
141
+ 'device_id': 'LOGALERT', # required, see below
142
+ 'store_path': '/var/lib/logalert/matrix-store', # required
143
+ }
144
+ )
145
+ ```
146
+
147
+ ### Checking your setup
148
+
149
+ ```bash
150
+ logalert check # validates tokens and room access without sending anything
151
+ logalert test # sends a test alert
152
+ ```
153
+
154
+ For Matrix, `logalert check` also reports whether the room is encrypted and tells you which
155
+ extra is needed.
156
+
157
+ ## What gets captured
158
+
159
+ | Source | What it catches |
160
+ |---|---|
161
+ | `logging` | every record at `level` (default `ERROR`) and above, together with `exc_info` |
162
+ | `sys.excepthook` | the exception that killed the main thread |
163
+ | `threading.excepthook` | an exception inside a `Thread` |
164
+ | asyncio handler | an exception in a task or event loop callback |
165
+ | `sys.unraisablehook` | exceptions in `__del__` (off by default, enable with `capture_unraisable=True`) |
166
+
167
+ Not sent by default: `KeyboardInterrupt`, `SystemExit`, `asyncio.CancelledError`.
168
+
169
+ ## Sending manually
170
+
171
+ ```python
172
+ try:
173
+ charge(order)
174
+ except Exception as exc:
175
+ logalert.alert(exc)
176
+ raise
177
+
178
+ logalert.alert('payment gateway did not respond within 30 seconds', level='warning')
179
+ ```
180
+
181
+ ## Context
182
+
183
+ ```python
184
+ logalert.tag('region', 'eu-west')
185
+ logalert.context('order', {'id': 4217, 'total': 1990})
186
+ logalert.breadcrumb('cache miss', category='cache')
187
+
188
+ logalert.alert(exc)
189
+ ```
190
+
191
+ `breadcrumb` writes into a ring buffer that is attached to the next alert - this shows what
192
+ was happening before the error, not just the traceback itself.
193
+
194
+ ## Hooks
195
+
196
+ Two hooks with one job each, instead of one overloaded one:
197
+
198
+ ```python
199
+ def only_payments(alert: logalert.Alert) -> bool:
200
+ return 'payment' in alert.logger
201
+
202
+
203
+ def add_region(alert: logalert.Alert) -> logalert.Alert:
204
+ from dataclasses import replace
205
+
206
+ return replace(alert, tags={**alert.tags, 'dc': 'eu-1'})
207
+
208
+
209
+ logalert.init(
210
+ telegram={...},
211
+ filter=only_payments, # return False to drop the event
212
+ enrich=add_region, # return a new event
213
+ )
214
+ ```
215
+
216
+ ## Durable delivery via Redis
217
+
218
+ By default alerts go out from a background thread inside the process: fast and with no
219
+ infrastructure, but if the process dies the unsent queue is lost.
220
+
221
+ When losing alerts is not acceptable, turn on Redis:
222
+
223
+ ```python
224
+ logalert.init(
225
+ telegram={...},
226
+ redis_url='redis://localhost:6379/0',
227
+ project='order-service', # required: keys are isolated from other applications
228
+ )
229
+ ```
230
+
231
+ ```bash
232
+ logalert worker --redis-url redis://localhost:6379/0 --project order-service
233
+ ```
234
+
235
+ Requires **Redis 6.2+**
236
+
237
+ ## Deduplication
238
+
239
+ Custom grouping is set explicitly:
240
+
241
+ ```python
242
+ from dataclasses import replace
243
+
244
+ logalert.alert(replace(captured, fingerprint=('my-group',)))
245
+ ```
246
+
247
+ With Redis the window is shared across all workers. Without it, each process has its own.
248
+
249
+ ## Privacy
250
+
251
+ By default only the technical part leaves the process: exception type, traceback, logger,
252
+ host, version. Local variables in the traceback are **not** sent (`include_locals=False`) -
253
+ they regularly contain passwords, tokens and personal data, so they stay out of the alert
254
+ unless you ask for them.
255
+
256
+ What is available when you need more:
257
+
258
+ ```python
259
+ logalert.init(
260
+ telegram={...},
261
+ include_locals='safe', # primitives only, denylisted names skipped
262
+ send_default_pii=True, # allow user fields
263
+ include_source_context=True,
264
+ )
265
+ ```
266
+
267
+ ## Writing your own sink
268
+
269
+ The `Sink` protocol is public
270
+
271
+ ```python
272
+ import json
273
+ import pathlib
274
+
275
+ from logalert import Alert
276
+
277
+
278
+ class FileSink:
279
+ name = 'file'
280
+ idempotent = False
281
+
282
+ def __init__(self, path: str = 'alerts.jsonl') -> None:
283
+ self._path = pathlib.Path(path)
284
+
285
+ def send(self, alert: Alert, *, key: str, timeout: float) -> None:
286
+ with self._path.open('a', encoding='utf-8') as fh:
287
+ fh.write(json.dumps(alert.to_dict(), ensure_ascii=False) + '\n')
288
+
289
+ def healthcheck(self) -> None:
290
+ return None
291
+
292
+ def close(self) -> None:
293
+ return None
294
+ ```
295
+
296
+ ```python
297
+ import logalert
298
+
299
+ logalert.init(sinks=[FileSink('/var/log/alerts.jsonl')])
300
+ ```
301
+
302
+ A sink is handed over as an object, and it carries its own configuration.
303
+
304
+ The `send()` contract: called only from the worker thread; must be synchronous and finish
305
+ within `timeout`; reports failure **by raising**, not by returning `False`:
306
+
307
+ - `SinkTransientError` - temporary failure, retry;
308
+ - `SinkRateLimited(retry_after=N)` - the receiver asks you to wait N seconds;
309
+ - `SinkPermanentError` - retrying is pointless, goes to the dead-letter stream.
310
+
311
+ A sink must not log above `DEBUG` - that would open an amplification loop.
312
+
313
+ ## CLI
314
+
315
+ ```bash
316
+ logalert check # validate the configuration
317
+ logalert test # send a test alert
318
+ logalert worker # run the consumer for Redis mode
319
+ logalert topics # discover chat_id and message_thread_id for forum topics
320
+ logalert version
321
+ ```
322
+
323
+ ## Troubleshooting
324
+
325
+ If alerts are not arriving, turn on the library's debug output:
326
+
327
+ ```python
328
+ logalert.init(telegram={...}, debug=True)
329
+ ```
330
+
331
+ It shows what was filtered out, what was suppressed by deduplication, and with what error
332
+ delivery failed.
333
+
334
+ ## License
335
+
336
+ [MIT](https://github.com/Quirkink/logalert/blob/main/LICENSE)