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.
- logalert-0.1.0/.github/workflows/ci.yml +75 -0
- logalert-0.1.0/.github/workflows/release.yml +19 -0
- logalert-0.1.0/.gitignore +25 -0
- logalert-0.1.0/LICENSE +21 -0
- logalert-0.1.0/Makefile +37 -0
- logalert-0.1.0/PKG-INFO +336 -0
- logalert-0.1.0/README.md +289 -0
- logalert-0.1.0/docker-compose.yml +14 -0
- logalert-0.1.0/examples/custom_sink.py +123 -0
- logalert-0.1.0/examples/dictconfig.py +71 -0
- logalert-0.1.0/examples/hooks.py +117 -0
- logalert-0.1.0/examples/matrix.py +66 -0
- logalert-0.1.0/examples/redis_worker.py +72 -0
- logalert-0.1.0/examples/telegram.py +83 -0
- logalert-0.1.0/pyproject.toml +100 -0
- logalert-0.1.0/src/logalert/__init__.py +356 -0
- logalert-0.1.0/src/logalert/_dedupe.py +274 -0
- logalert-0.1.0/src/logalert/_delivery.py +424 -0
- logalert-0.1.0/src/logalert/_fingerprint.py +230 -0
- logalert-0.1.0/src/logalert/_guard.py +126 -0
- logalert-0.1.0/src/logalert/_privacy.py +252 -0
- logalert-0.1.0/src/logalert/_queue.py +279 -0
- logalert-0.1.0/src/logalert/_state.py +62 -0
- logalert-0.1.0/src/logalert/_version.py +7 -0
- logalert-0.1.0/src/logalert/capture.py +728 -0
- logalert-0.1.0/src/logalert/cli.py +453 -0
- logalert-0.1.0/src/logalert/client.py +473 -0
- logalert-0.1.0/src/logalert/exceptions.py +91 -0
- logalert-0.1.0/src/logalert/handler.py +213 -0
- logalert-0.1.0/src/logalert/options.py +275 -0
- logalert-0.1.0/src/logalert/py.typed +0 -0
- logalert-0.1.0/src/logalert/render.py +548 -0
- logalert-0.1.0/src/logalert/scope.py +306 -0
- logalert-0.1.0/src/logalert/sinks/__init__.py +71 -0
- logalert-0.1.0/src/logalert/sinks/base.py +135 -0
- logalert-0.1.0/src/logalert/sinks/matrix.py +308 -0
- logalert-0.1.0/src/logalert/sinks/matrix_e2ee.py +539 -0
- logalert-0.1.0/src/logalert/sinks/stderr.py +88 -0
- logalert-0.1.0/src/logalert/sinks/telegram.py +359 -0
- logalert-0.1.0/src/logalert/transport/__init__.py +34 -0
- logalert-0.1.0/src/logalert/transport/base.py +205 -0
- logalert-0.1.0/src/logalert/transport/redis.py +525 -0
- logalert-0.1.0/src/logalert/types.py +430 -0
- logalert-0.1.0/src/logalert/worker.py +161 -0
- logalert-0.1.0/tests/conftest.py +193 -0
- logalert-0.1.0/tests/live/test_redis.py +409 -0
- logalert-0.1.0/tests/test_api.py +341 -0
- logalert-0.1.0/tests/test_capture.py +439 -0
- logalert-0.1.0/tests/test_dedupe.py +176 -0
- logalert-0.1.0/tests/test_fingerprint.py +341 -0
- logalert-0.1.0/tests/test_inert.py +143 -0
- logalert-0.1.0/tests/test_matrix.py +322 -0
- logalert-0.1.0/tests/test_matrix_e2ee.py +796 -0
- logalert-0.1.0/tests/test_privacy.py +190 -0
- logalert-0.1.0/tests/test_queue.py +145 -0
- logalert-0.1.0/tests/test_split.py +116 -0
- logalert-0.1.0/tests/test_telegram.py +344 -0
- 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.
|
logalert-0.1.0/Makefile
ADDED
|
@@ -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 {} +
|
logalert-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
53
|
+

|
|
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)
|