richrelay 1.0.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.
- richrelay-1.0.2/.gitignore +212 -0
- richrelay-1.0.2/CHANGELOG.md +12 -0
- richrelay-1.0.2/LICENSE +21 -0
- richrelay-1.0.2/PKG-INFO +377 -0
- richrelay-1.0.2/README.md +333 -0
- richrelay-1.0.2/examples/basic.py +27 -0
- richrelay-1.0.2/examples/redis_rabbitmq.py +19 -0
- richrelay-1.0.2/examples/rich_handler.py +10 -0
- richrelay-1.0.2/pyproject.toml +132 -0
- richrelay-1.0.2/src/richrelay/__init__.py +33 -0
- richrelay-1.0.2/src/richrelay/__main__.py +5 -0
- richrelay-1.0.2/src/richrelay/_version.py +1 -0
- richrelay-1.0.2/src/richrelay/buffers/__init__.py +10 -0
- richrelay-1.0.2/src/richrelay/buffers/base.py +45 -0
- richrelay-1.0.2/src/richrelay/buffers/memory.py +62 -0
- richrelay-1.0.2/src/richrelay/buffers/rabbitmq_buffer.py +135 -0
- richrelay-1.0.2/src/richrelay/buffers/redis_buffer.py +138 -0
- richrelay-1.0.2/src/richrelay/buffers/sqlite.py +126 -0
- richrelay-1.0.2/src/richrelay/channel.py +351 -0
- richrelay-1.0.2/src/richrelay/cli.py +65 -0
- richrelay-1.0.2/src/richrelay/event.py +26 -0
- richrelay-1.0.2/src/richrelay/formats.py +231 -0
- richrelay-1.0.2/src/richrelay/integrations.py +259 -0
- richrelay-1.0.2/src/richrelay/listener.py +205 -0
- richrelay-1.0.2/src/richrelay/parsers.py +97 -0
- richrelay-1.0.2/src/richrelay/py.typed +0 -0
- richrelay-1.0.2/src/richrelay/relay.py +215 -0
- richrelay-1.0.2/src/richrelay/syslog.py +83 -0
- richrelay-1.0.2/src/richrelay/transports.py +253 -0
- richrelay-1.0.2/src/richrelay/urls.py +83 -0
- richrelay-1.0.2/tests/conftest.py +62 -0
- richrelay-1.0.2/tests/test_buffers.py +63 -0
- richrelay-1.0.2/tests/test_delivery.py +154 -0
- richrelay-1.0.2/tests/test_formats.py +65 -0
- richrelay-1.0.2/tests/test_integrations.py +140 -0
- richrelay-1.0.2/tests/test_rabbitmq.py +64 -0
- richrelay-1.0.2/tests/test_redis.py +100 -0
- richrelay-1.0.2/tests/test_urls_cli_tls.py +61 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Created by https://www.toptal.com/developers/gitignore/api/python
|
|
2
|
+
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
|
3
|
+
|
|
4
|
+
### Python ###
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
|
|
10
|
+
# C extensions
|
|
11
|
+
*.so
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
.Python
|
|
15
|
+
build/
|
|
16
|
+
develop-eggs/
|
|
17
|
+
dist/
|
|
18
|
+
downloads/
|
|
19
|
+
eggs/
|
|
20
|
+
.eggs/
|
|
21
|
+
lib/
|
|
22
|
+
lib64/
|
|
23
|
+
parts/
|
|
24
|
+
sdist/
|
|
25
|
+
var/
|
|
26
|
+
wheels/
|
|
27
|
+
share/python-wheels/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
.installed.cfg
|
|
30
|
+
*.egg
|
|
31
|
+
MANIFEST
|
|
32
|
+
|
|
33
|
+
# PyInstaller
|
|
34
|
+
# Usually these files are written by a python script from a template
|
|
35
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
36
|
+
*.manifest
|
|
37
|
+
*.spec
|
|
38
|
+
|
|
39
|
+
# Installer logs
|
|
40
|
+
pip-log.txt
|
|
41
|
+
pip-delete-this-directory.txt
|
|
42
|
+
|
|
43
|
+
# Unit test / coverage reports
|
|
44
|
+
htmlcov/
|
|
45
|
+
.tox/
|
|
46
|
+
.nox/
|
|
47
|
+
.coverage
|
|
48
|
+
.coverage.*
|
|
49
|
+
.cache
|
|
50
|
+
nosetests.xml
|
|
51
|
+
coverage.xml
|
|
52
|
+
*.cover
|
|
53
|
+
*.py,cover
|
|
54
|
+
.hypothesis/
|
|
55
|
+
.pytest_cache/
|
|
56
|
+
cover/
|
|
57
|
+
|
|
58
|
+
# Translations
|
|
59
|
+
*.mo
|
|
60
|
+
*.pot
|
|
61
|
+
|
|
62
|
+
# Django stuff:
|
|
63
|
+
*.log
|
|
64
|
+
local_settings.py
|
|
65
|
+
db.sqlite3
|
|
66
|
+
db.sqlite3-journal
|
|
67
|
+
|
|
68
|
+
# Flask stuff:
|
|
69
|
+
instance/
|
|
70
|
+
.webassets-cache
|
|
71
|
+
|
|
72
|
+
# Scrapy stuff:
|
|
73
|
+
.scrapy
|
|
74
|
+
|
|
75
|
+
# Sphinx documentation
|
|
76
|
+
docs/_build/
|
|
77
|
+
|
|
78
|
+
# PyBuilder
|
|
79
|
+
.pybuilder/
|
|
80
|
+
target/
|
|
81
|
+
|
|
82
|
+
# Jupyter Notebook
|
|
83
|
+
.ipynb_checkpoints
|
|
84
|
+
|
|
85
|
+
# IPython
|
|
86
|
+
profile_default/
|
|
87
|
+
ipython_config.py
|
|
88
|
+
|
|
89
|
+
# pyenv
|
|
90
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
91
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
92
|
+
# .python-version
|
|
93
|
+
|
|
94
|
+
# pipenv
|
|
95
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
96
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
97
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
98
|
+
# install all needed dependencies.
|
|
99
|
+
#Pipfile.lock
|
|
100
|
+
|
|
101
|
+
# poetry
|
|
102
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
103
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
104
|
+
# commonly ignored for libraries.
|
|
105
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
106
|
+
#poetry.lock
|
|
107
|
+
|
|
108
|
+
# pdm
|
|
109
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
110
|
+
#pdm.lock
|
|
111
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
112
|
+
# in version control.
|
|
113
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
114
|
+
.pdm.toml
|
|
115
|
+
|
|
116
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
117
|
+
__pypackages__/
|
|
118
|
+
|
|
119
|
+
# Celery stuff
|
|
120
|
+
celerybeat-schedule
|
|
121
|
+
celerybeat.pid
|
|
122
|
+
|
|
123
|
+
# SageMath parsed files
|
|
124
|
+
*.sage.py
|
|
125
|
+
|
|
126
|
+
# Environments
|
|
127
|
+
.env
|
|
128
|
+
.venv
|
|
129
|
+
env/
|
|
130
|
+
venv/
|
|
131
|
+
ENV/
|
|
132
|
+
env.bak/
|
|
133
|
+
venv.bak/
|
|
134
|
+
|
|
135
|
+
# Spyder project settings
|
|
136
|
+
.spyderproject
|
|
137
|
+
.spyproject
|
|
138
|
+
|
|
139
|
+
# Rope project settings
|
|
140
|
+
.ropeproject
|
|
141
|
+
|
|
142
|
+
# mkdocs documentation
|
|
143
|
+
/site
|
|
144
|
+
|
|
145
|
+
# mypy
|
|
146
|
+
.mypy_cache/
|
|
147
|
+
.dmypy.json
|
|
148
|
+
dmypy.json
|
|
149
|
+
|
|
150
|
+
# Pyre type checker
|
|
151
|
+
.pyre/
|
|
152
|
+
|
|
153
|
+
# pytype static type analyzer
|
|
154
|
+
.pytype/
|
|
155
|
+
|
|
156
|
+
# Cython debug symbols
|
|
157
|
+
cython_debug/
|
|
158
|
+
|
|
159
|
+
# PyCharm
|
|
160
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
161
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
162
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
163
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
164
|
+
#.idea/
|
|
165
|
+
|
|
166
|
+
### Python Patch ###
|
|
167
|
+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
|
168
|
+
poetry.toml
|
|
169
|
+
|
|
170
|
+
# ruff
|
|
171
|
+
.ruff_cache/
|
|
172
|
+
|
|
173
|
+
# LSP config files
|
|
174
|
+
pyrightconfig.json
|
|
175
|
+
|
|
176
|
+
# End of https://www.toptal.com/developers/gitignore/api/python
|
|
177
|
+
*.pyc
|
|
178
|
+
*.bak
|
|
179
|
+
*.zip
|
|
180
|
+
*.rar
|
|
181
|
+
*.7z
|
|
182
|
+
*.mp3
|
|
183
|
+
*.wav
|
|
184
|
+
*.sublime-workspace
|
|
185
|
+
.hg/
|
|
186
|
+
*.hgignore
|
|
187
|
+
*.hgtags
|
|
188
|
+
*dist/
|
|
189
|
+
traceback.log
|
|
190
|
+
|
|
191
|
+
# Added entries (3 items)
|
|
192
|
+
ANSWER*.md
|
|
193
|
+
QUEST*.md
|
|
194
|
+
PLAN*.md
|
|
195
|
+
|
|
196
|
+
# Added entries (8 items)
|
|
197
|
+
download/
|
|
198
|
+
.pt/
|
|
199
|
+
gitignore_backup/
|
|
200
|
+
gitignore_backups/
|
|
201
|
+
src/tmp*
|
|
202
|
+
tmp*
|
|
203
|
+
PLAN.md
|
|
204
|
+
CLAUDE.md
|
|
205
|
+
|
|
206
|
+
# Added entries (3 items)
|
|
207
|
+
*sublime*
|
|
208
|
+
*.patch
|
|
209
|
+
QUESTION*.md
|
|
210
|
+
|
|
211
|
+
# Added entries (1 items)
|
|
212
|
+
images/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Initial release.
|
|
6
|
+
- Hooks: `rich.print`, `Console.print`, `Console.log`, `Console.print_exception`, `logging`
|
|
7
|
+
(`RelayHandler`, `RichRelayHandler`), `sys.excepthook` / `threading.excepthook`.
|
|
8
|
+
- Formats: RFC 5424, RFC 3164, JSON, GELF 1.1, logfmt, plain.
|
|
9
|
+
- Transports: UDP, TCP (octet-counting / newline / NUL framing), TLS, Unix datagram.
|
|
10
|
+
- Store-and-forward buffers: memory, SQLite (durable), Redis list, RabbitMQ queue.
|
|
11
|
+
- `RedisCache` rolling cache of recent events.
|
|
12
|
+
- Bundled receiver / viewer and CLI (`richrelay send | drain | listen`).
|
richrelay-1.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hadi Cahyadi
|
|
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.
|
richrelay-1.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: richrelay
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Send rich.print, Console.log, logging and exceptions to syslog (RFC 5424/3164), TCP/UDP/TLS receivers, with store-and-forward via SQLite, Redis or RabbitMQ.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cumulus13/richrelay
|
|
6
|
+
Project-URL: Source, https://github.com/cumulus13/richrelay
|
|
7
|
+
Project-URL: Issues, https://github.com/cumulus13/richrelay/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/cumulus13/richrelay/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Hadi Cahyadi <cumulus13@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: gelf,log-shipping,logging,rabbitmq,redis,rfc3164,rfc5424,rich,store-and-forward,syslog
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: System :: Logging
|
|
24
|
+
Classifier: Topic :: System :: Networking :: Monitoring
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: rich>=12.0
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: pika>=1.3; extra == 'all'
|
|
30
|
+
Requires-Dist: redis>=4.5; extra == 'all'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build; extra == 'dev'
|
|
33
|
+
Requires-Dist: fakeredis>=2.20; extra == 'dev'
|
|
34
|
+
Requires-Dist: pika>=1.3; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
36
|
+
Requires-Dist: redis>=4.5; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
38
|
+
Requires-Dist: twine; extra == 'dev'
|
|
39
|
+
Provides-Extra: rabbitmq
|
|
40
|
+
Requires-Dist: pika>=1.3; extra == 'rabbitmq'
|
|
41
|
+
Provides-Extra: redis
|
|
42
|
+
Requires-Dist: redis>=4.5; extra == 'redis'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# richrelay
|
|
46
|
+
|
|
47
|
+
[](https://github.com/cumulus13/richrelay/actions/workflows/ci.yml)
|
|
48
|
+
[](https://pypi.org/project/richrelay/)
|
|
49
|
+
[](https://pypi.org/project/richrelay/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
|
|
52
|
+
**Ship [Rich](https://github.com/Textualize/rich) prints to syslog and other receivers, without losing logs when the receiver is down.**
|
|
53
|
+
|
|
54
|
+
`richrelay` is an extension for Rich, not a fork. Your terminal output stays exactly as it is; every
|
|
55
|
+
`rich.print`, `Console.print`, `Console.log`, `Console.print_exception`, `logging` record and uncaught
|
|
56
|
+
exception is *also* sent to one or more remote receivers, formatted as RFC 5424, RFC 3164, JSON, GELF,
|
|
57
|
+
logfmt or plain text.
|
|
58
|
+
|
|
59
|
+
- **Hooks:** `rich.print`, `Console.print`, `Console.log`, `Console.print_exception`, `RichHandler` (drop-in
|
|
60
|
+
`RichRelayHandler`), any `logging` logger, `sys.excepthook`, `threading.excepthook`.
|
|
61
|
+
- **Formats:** RFC 5424 (with structured data), RFC 3164 (BSD), JSON, GELF 1.1, logfmt, plain.
|
|
62
|
+
- **Transports:** UDP, TCP (octet-counting / newline / NUL framing), TLS, Unix datagram (`/dev/log`).
|
|
63
|
+
- **Store-and-forward** while the receiver is down, with hard size limits:
|
|
64
|
+
in-memory, **SQLite (durable, no extra dependency)**, **Redis** list, **RabbitMQ** queue.
|
|
65
|
+
- **Redis cache:** rolling list of the most recent events, independent of delivery.
|
|
66
|
+
- Multiple destinations at once, each with its own format, level filter and buffer.
|
|
67
|
+
- Non-blocking: your code only appends to an in-memory queue; a background thread does the I/O.
|
|
68
|
+
- Built-in receiver/viewer and CLI for testing (`richrelay listen`, `send`, `drain`).
|
|
69
|
+
|
|
70
|
+
> Why an extension? Rich has no log-shipping story and is not open to this kind of change right now.
|
|
71
|
+
> `richrelay` works purely by wrapping public Rich entry points, so it needs nothing from upstream.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install richrelay # core: SQLite + memory buffers, UDP/TCP/TLS
|
|
77
|
+
pip install "richrelay[redis]" # + RedisBuffer / RedisCache
|
|
78
|
+
pip install "richrelay[rabbitmq]" # + RabbitMQBuffer
|
|
79
|
+
pip install "richrelay[all]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Python 3.9+, Rich 12+. Windows, Linux and macOS.
|
|
83
|
+
|
|
84
|
+
## Quick start
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import logging
|
|
88
|
+
import rich
|
|
89
|
+
from rich.console import Console
|
|
90
|
+
import richrelay
|
|
91
|
+
|
|
92
|
+
relay = richrelay.setup(
|
|
93
|
+
[
|
|
94
|
+
"udp://127.0.0.1:514?format=rfc3164",
|
|
95
|
+
# TCP + durable SQLite spool: nothing is lost while 10.0.0.5 is down
|
|
96
|
+
"tcp://10.0.0.5:514?format=rfc5424&buffer=sqlite%3A%2F%2F%2Fspool.db%3Fmax_mb%3D50",
|
|
97
|
+
],
|
|
98
|
+
app_name="myapp",
|
|
99
|
+
facility="local0",
|
|
100
|
+
attach_logging=True, # also forward the root logger
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
rich.print("[bold green]started[/bold green]") # terminal + syslog
|
|
104
|
+
Console().log("loaded config", {"debug": True}) # terminal + syslog
|
|
105
|
+
logging.getLogger("db").error("connection lost") # forwarded
|
|
106
|
+
try:
|
|
107
|
+
1 / 0
|
|
108
|
+
except ZeroDivisionError:
|
|
109
|
+
Console().print_exception() # full traceback relayed (ERROR)
|
|
110
|
+
relay.info("direct API", user="alice") # extra keys -> structured data
|
|
111
|
+
# uncaught exceptions are relayed at CRITICAL and flushed before the interpreter exits
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Try it locally with the bundled viewer:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
richrelay listen --udp 5514 --tcp 5514 # terminal 1: colored live viewer
|
|
118
|
+
richrelay send -d "udp://127.0.0.1:5514?format=rfc5424" -l warning "hello" # terminal 2
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`setup()` with no arguments reads whitespace-separated URLs from `$RICHRELAY_URLS`.
|
|
122
|
+
|
|
123
|
+
## Architecture
|
|
124
|
+
|
|
125
|
+
```mermaid
|
|
126
|
+
flowchart LR
|
|
127
|
+
subgraph app["Your application"]
|
|
128
|
+
A1["rich.print / Console.print"]
|
|
129
|
+
A2["Console.log"]
|
|
130
|
+
A3["Console.print_exception"]
|
|
131
|
+
A4["logging / RichHandler"]
|
|
132
|
+
A5["sys.excepthook / threading.excepthook"]
|
|
133
|
+
A6["relay.info(...)"]
|
|
134
|
+
end
|
|
135
|
+
A1 & A2 & A3 --> P["patch_rich()<br/>plain-text capture"]
|
|
136
|
+
A4 --> H["RelayHandler / RichRelayHandler"]
|
|
137
|
+
A5 --> X["install_excepthook()"]
|
|
138
|
+
P & H & X & A6 --> E(["Event"])
|
|
139
|
+
E --> R{{"Relay<br/>fan-out"}}
|
|
140
|
+
R --> C1["Channel 1<br/>format: RFC 5424"]
|
|
141
|
+
R --> C2["Channel 2<br/>format: RFC 3164"]
|
|
142
|
+
R --> C3["Channel 3<br/>format: JSON"]
|
|
143
|
+
R -. "every event" .-> RC[("RedisCache<br/>recent N")]
|
|
144
|
+
C1 --> T1["TCP / TLS"]
|
|
145
|
+
C2 --> T2["UDP"]
|
|
146
|
+
C3 --> T3["TCP"]
|
|
147
|
+
C1 -. "receiver down" .-> B1[("SQLite spool")]
|
|
148
|
+
C3 -. "receiver down" .-> B3[("Redis list or<br/>RabbitMQ queue")]
|
|
149
|
+
T1 --> S1["rsyslog / syslog-ng / Graylog / ..."]
|
|
150
|
+
T2 --> S2["any UDP receiver"]
|
|
151
|
+
T3 --> S3["Logstash / Vector / custom"]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
[](https://files.catbox.moe/1cemxp.jpg)
|
|
155
|
+
|
|
156
|
+
Every destination is an independent **channel** (own thread, formatter, transport, buffer), so one dead
|
|
157
|
+
receiver never delays another.
|
|
158
|
+
|
|
159
|
+
### Store-and-forward
|
|
160
|
+
|
|
161
|
+
```mermaid
|
|
162
|
+
sequenceDiagram
|
|
163
|
+
autonumber
|
|
164
|
+
participant App as Your code
|
|
165
|
+
participant Q as In-memory queue
|
|
166
|
+
participant W as Channel worker
|
|
167
|
+
participant RX as Receiver
|
|
168
|
+
participant BUF as Buffer (SQLite / Redis / RabbitMQ)
|
|
169
|
+
|
|
170
|
+
App->>Q: emit(event) (non-blocking)
|
|
171
|
+
Q->>W: next event
|
|
172
|
+
W->>RX: send(payload)
|
|
173
|
+
RX--xW: connection refused / reset
|
|
174
|
+
W->>BUF: push(payload)
|
|
175
|
+
Note over W: state = offline, retry with exponential backoff + jitter
|
|
176
|
+
App->>Q: more events
|
|
177
|
+
W->>BUF: push (keeps ordering)
|
|
178
|
+
Note over RX: receiver comes back
|
|
179
|
+
W->>BUF: peek(batch)
|
|
180
|
+
W->>RX: send oldest first
|
|
181
|
+
RX-->>W: ok
|
|
182
|
+
W->>BUF: discard(batch) (ack only after sending)
|
|
183
|
+
Note over W: buffer empty, state = online, live sending resumes
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
[](https://files.catbox.moe/uaprl2.jpg)
|
|
187
|
+
|
|
188
|
+
### Channel state machine
|
|
189
|
+
|
|
190
|
+
```mermaid
|
|
191
|
+
stateDiagram-v2
|
|
192
|
+
[*] --> Online
|
|
193
|
+
Online --> Offline: send fails
|
|
194
|
+
Offline --> Offline: retry fails and backoff doubles
|
|
195
|
+
Offline --> Draining: retry timer fires
|
|
196
|
+
Draining --> Offline: send fails again
|
|
197
|
+
Draining --> Online: buffer empty
|
|
198
|
+
Online --> [*]: close() flushes, then stores whatever is unsent
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
[](https://files.catbox.moe/seo27a.jpg)
|
|
202
|
+
|
|
203
|
+
If the *buffer itself* fails (Redis down, broker down, disk error), events fall back to a bounded
|
|
204
|
+
in-memory spill list and are still delivered as soon as the receiver is reachable.
|
|
205
|
+
|
|
206
|
+
## Hooking Rich
|
|
207
|
+
|
|
208
|
+
| What you call | What is relayed | Default level |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `rich.print(...)`, `Console.print(...)`, `print_json`, `rule` | Rendered as plain text (no ANSI, 200 columns) | INFO |
|
|
211
|
+
| `Console.log(...)` | Plain text; `file`/`func` of the real caller added | INFO |
|
|
212
|
+
| `Console.print_exception()` | `Type: message` + full Python traceback | ERROR |
|
|
213
|
+
| `logging` (via `RelayHandler` / `RichRelayHandler`) | `record.getMessage()`, traceback, `extra=` fields | record level |
|
|
214
|
+
| Uncaught exception (main thread or thread) | Message + traceback, then flush | CRITICAL |
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
richrelay.setup([...], print_level=logging.DEBUG, log_level=logging.INFO,
|
|
218
|
+
exception_level=logging.ERROR, capture_width=120,
|
|
219
|
+
console_filter=lambda console: not console.is_jupyter,
|
|
220
|
+
rich_traceback=True) # also calls rich.traceback.install()
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Output that renders to nothing (for example `Live` / `Progress` control codes) is not relayed. Patching is
|
|
224
|
+
reversible: `with richrelay.patch_rich(relay): ...` or `richrelay.shutdown()`.
|
|
225
|
+
|
|
226
|
+
Use `RichRelayHandler` instead of `RichHandler`:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
logging.basicConfig(level="INFO", handlers=[RichRelayHandler(rich_tracebacks=True, relay=relay)])
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Formats
|
|
233
|
+
|
|
234
|
+
| `format=` | Wire format | TCP framing (default) |
|
|
235
|
+
|---|---|---|
|
|
236
|
+
| `rfc5424` (`5424`, `syslog`) | `<PRI>1 TIMESTAMP HOST APP PROCID MSGID [SD] MSG`, UTC microsecond timestamps | octet-counting (RFC 6587) |
|
|
237
|
+
| `rfc3164` (`3164`, `bsd`) | `<PRI>Mmm dd hh:mm:ss HOST TAG[PID]: MSG`, local time | newline (LF in message becomes `#012`) |
|
|
238
|
+
| `json` | one JSON object per message | newline |
|
|
239
|
+
| `gelf` | GELF 1.1 JSON (Graylog) | NUL-terminated |
|
|
240
|
+
| `logfmt` | `ts=... level=info msg="..."` | newline |
|
|
241
|
+
| `plain` | `timestamp LEVEL logger: message` | newline |
|
|
242
|
+
|
|
243
|
+
Override with `?framing=octet|newline|null|none`. RFC 5424 structured data uses the SD-ID
|
|
244
|
+
`richrelay@32473`; 32473 is the IANA number reserved for **documentation**. For production, use your own
|
|
245
|
+
Private Enterprise Number: `make_formatter("rfc5424", sd_id="myapp@<PEN>")`.
|
|
246
|
+
|
|
247
|
+
Facility is configurable per destination (`facility="local0"`), severity is derived from the level
|
|
248
|
+
(DEBUG 7, INFO 6, 25 = NOTICE 5, WARNING 4, ERROR 3, CRITICAL 2).
|
|
249
|
+
|
|
250
|
+
Messages are shrunk to `max_bytes` (UDP/Unix 8192, TCP 1 MiB; override per destination) by trimming the
|
|
251
|
+
traceback first, then the message, so JSON stays valid and the header intact.
|
|
252
|
+
|
|
253
|
+
## Destinations
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from richrelay import Relay, udp, tcp, tls, unix
|
|
257
|
+
|
|
258
|
+
Relay([
|
|
259
|
+
udp("syslog.lan", 514, format="rfc3164", facility="daemon"),
|
|
260
|
+
tcp("10.0.0.5", 514, format="rfc5424", min_level=30),
|
|
261
|
+
tls("logs.example.com", 6514, ca_file="ca.pem", cert_file="client.pem", key_file="client.key"),
|
|
262
|
+
unix("/dev/log"), # POSIX only
|
|
263
|
+
])
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
URL form (also usable in `$RICHRELAY_URLS`):
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
udp://host:514?format=rfc3164&facility=local3&level=warning&max_bytes=4096
|
|
270
|
+
tcp://host:514?format=json&framing=newline&name=main
|
|
271
|
+
tls://host:6514?verify=1&ca=/etc/ssl/ca.pem&cert=client.pem&key=client.key
|
|
272
|
+
unix:///dev/log
|
|
273
|
+
...&buffer=<URL-encoded buffer URL>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Buffers (what happens while the receiver is down)
|
|
277
|
+
|
|
278
|
+
| Buffer | Survives restart | Bound | Extra dependency | Use it for |
|
|
279
|
+
|---|---|---|---|---|
|
|
280
|
+
| `MemoryBuffer` (default spill) | no | items, bytes | none | short outages |
|
|
281
|
+
| `SQLiteBuffer` | **yes** | items, bytes | none (stdlib) | the non-Redis/RabbitMQ option |
|
|
282
|
+
| `RedisBuffer` | yes (per Redis persistence) | items | `redis` | shared queue, several producers |
|
|
283
|
+
| `RabbitMQBuffer` | yes (durable queue) | items (broker-side) | `pika` | existing RabbitMQ infrastructure |
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from richrelay import Relay, tcp, SQLiteBuffer, RedisBuffer, RabbitMQBuffer
|
|
287
|
+
|
|
288
|
+
tcp("10.0.0.5", 514, buffer=SQLiteBuffer("spool.db", max_items=100_000, max_bytes=64*1024*1024,
|
|
289
|
+
overflow="drop_oldest"))
|
|
290
|
+
tcp("10.0.0.5", 514, buffer=RedisBuffer("redis://localhost:6379/0", key="logs:spool", max_items=200_000))
|
|
291
|
+
tcp("10.0.0.5", 514, buffer=RabbitMQBuffer("amqp://guest:guest@localhost/%2F", queue="logs.spool"))
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- **Order** is preserved: buffered events are always replayed before new ones.
|
|
295
|
+
- **`overflow`**: `drop_oldest` (default, keep the newest) or `drop_newest` (keep the oldest). Dropped
|
|
296
|
+
counts are in `relay.stats()` (not tracked for RabbitMQ `drop_oldest`, where the broker discards).
|
|
297
|
+
- **Backoff**: `backoff_initial=1.0` doubling to `backoff_max=60.0` seconds, with jitter.
|
|
298
|
+
- **Ack-after-send**: Redis/RabbitMQ/SQLite entries are removed only after the receiver accepted them.
|
|
299
|
+
A crash mid-replay re-sends (at-least-once, so duplicates are possible).
|
|
300
|
+
- A message that can never be sent (e.g. a datagram over the path MTU limit) is dropped and counted in
|
|
301
|
+
`dropped_permanent` instead of blocking the queue forever.
|
|
302
|
+
- RabbitMQ: the queue is declared with `x-max-length`/`x-overflow`; changing them for an existing queue
|
|
303
|
+
needs the queue deleted first (RabbitMQ rejects redeclaring with different arguments).
|
|
304
|
+
|
|
305
|
+
### Redis as a cache
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from richrelay import Relay, RedisCache
|
|
309
|
+
cache = RedisCache("redis://localhost:6379/0", key="logs:recent", max_items=1000, ttl=3600)
|
|
310
|
+
relay = Relay([...], caches=[cache])
|
|
311
|
+
cache.recent(20) # newest first, JSON strings
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Cache writes are best effort and never retried; failures are counted in `relay.stats()["caches"]`.
|
|
315
|
+
|
|
316
|
+
## Delivery guarantees and limits (read this)
|
|
317
|
+
|
|
318
|
+
- **UDP is fire and forget.** The sender only learns the receiver is gone when an ICMP "port unreachable"
|
|
319
|
+
comes back, which surfaces on a *later* send (Linux: `ECONNREFUSED`, Windows: `WSAECONNRESET`), and
|
|
320
|
+
firewalls often drop ICMP entirely. Buffering works for UDP only on a best-effort basis. For lossless
|
|
321
|
+
forwarding use TCP or TLS.
|
|
322
|
+
- **TCP/syslog has no application-level ack.** richrelay detects a peer that closed the connection
|
|
323
|
+
before sending and reconnects, but bytes written just before an undetected failure may be lost. This is
|
|
324
|
+
inherent to syslog over TCP, not specific to this library.
|
|
325
|
+
- **MemoryBuffer is lost on exit.** Use SQLite/Redis/RabbitMQ if that matters. On exit, `close()` makes a
|
|
326
|
+
last delivery attempt; anything still undeliverable stays in the persistent buffer.
|
|
327
|
+
- **GELF** is sent uncompressed and **without UDP chunking**; keep GELF-over-UDP messages small or use TCP.
|
|
328
|
+
- **SQLite**: use one process per spool file (size counters are kept in memory).
|
|
329
|
+
- **fork()**: worker threads do not survive a fork; create the relay after forking (e.g. in each
|
|
330
|
+
gunicorn/multiprocessing worker).
|
|
331
|
+
- `richrelay` does not log through `logging` internally; internal errors are counted in `stats()` and a
|
|
332
|
+
rate-limited line goes to stderr, so attaching `RelayHandler` to the root logger cannot loop.
|
|
333
|
+
|
|
334
|
+
## Operations
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
relay.flush(5) # everything handed over (sent or buffered)
|
|
338
|
+
relay.wait_drained(30) # retry now, wait until buffers are empty
|
|
339
|
+
relay.stats() # sent / buffered / replayed / dropped / state / last_error per destination
|
|
340
|
+
relay.close() # also registered with atexit
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
CLI:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
richrelay send -d "tcp://host:514?buffer=sqlite%3A%2F%2F%2Ftmp%2Fs.db" -l error "disk full"
|
|
347
|
+
richrelay drain -d "tcp://host:514?buffer=sqlite%3A%2F%2F%2Ftmp%2Fs.db" --timeout 60 # exit 1 if not empty
|
|
348
|
+
richrelay listen --udp 5514 --tcp 5514 [--tls-cert c.pem --tls-key k.pem] [--json | --raw]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Development
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
pip install -e ".[dev]"
|
|
355
|
+
ruff check . && pytest
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
CI runs on Linux/Windows/macOS for Python 3.9 to 3.13 and against real Redis and RabbitMQ services.
|
|
359
|
+
Releases: bump `src/richrelay/_version.py`, tag `vX.Y.Z`, and the **Publish to PyPI** workflow uploads
|
|
360
|
+
using PyPI Trusted Publishing (one-time setup is described at the top of `.github/workflows/publish.yml`).
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
(MIT)[LICENSE]
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 👤 Author
|
|
369
|
+
|
|
370
|
+
[Hadi Cahyadi](mailto:cumulus13@gmail.com)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
[](https://www.buymeacoffee.com/cumulus13)
|
|
374
|
+
|
|
375
|
+
[](https://ko-fi.com/cumulus13)
|
|
376
|
+
|
|
377
|
+
[Support me on Patreon](https://www.patreon.com/cumulus13)
|