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.
Files changed (38) hide show
  1. richrelay-1.0.2/.gitignore +212 -0
  2. richrelay-1.0.2/CHANGELOG.md +12 -0
  3. richrelay-1.0.2/LICENSE +21 -0
  4. richrelay-1.0.2/PKG-INFO +377 -0
  5. richrelay-1.0.2/README.md +333 -0
  6. richrelay-1.0.2/examples/basic.py +27 -0
  7. richrelay-1.0.2/examples/redis_rabbitmq.py +19 -0
  8. richrelay-1.0.2/examples/rich_handler.py +10 -0
  9. richrelay-1.0.2/pyproject.toml +132 -0
  10. richrelay-1.0.2/src/richrelay/__init__.py +33 -0
  11. richrelay-1.0.2/src/richrelay/__main__.py +5 -0
  12. richrelay-1.0.2/src/richrelay/_version.py +1 -0
  13. richrelay-1.0.2/src/richrelay/buffers/__init__.py +10 -0
  14. richrelay-1.0.2/src/richrelay/buffers/base.py +45 -0
  15. richrelay-1.0.2/src/richrelay/buffers/memory.py +62 -0
  16. richrelay-1.0.2/src/richrelay/buffers/rabbitmq_buffer.py +135 -0
  17. richrelay-1.0.2/src/richrelay/buffers/redis_buffer.py +138 -0
  18. richrelay-1.0.2/src/richrelay/buffers/sqlite.py +126 -0
  19. richrelay-1.0.2/src/richrelay/channel.py +351 -0
  20. richrelay-1.0.2/src/richrelay/cli.py +65 -0
  21. richrelay-1.0.2/src/richrelay/event.py +26 -0
  22. richrelay-1.0.2/src/richrelay/formats.py +231 -0
  23. richrelay-1.0.2/src/richrelay/integrations.py +259 -0
  24. richrelay-1.0.2/src/richrelay/listener.py +205 -0
  25. richrelay-1.0.2/src/richrelay/parsers.py +97 -0
  26. richrelay-1.0.2/src/richrelay/py.typed +0 -0
  27. richrelay-1.0.2/src/richrelay/relay.py +215 -0
  28. richrelay-1.0.2/src/richrelay/syslog.py +83 -0
  29. richrelay-1.0.2/src/richrelay/transports.py +253 -0
  30. richrelay-1.0.2/src/richrelay/urls.py +83 -0
  31. richrelay-1.0.2/tests/conftest.py +62 -0
  32. richrelay-1.0.2/tests/test_buffers.py +63 -0
  33. richrelay-1.0.2/tests/test_delivery.py +154 -0
  34. richrelay-1.0.2/tests/test_formats.py +65 -0
  35. richrelay-1.0.2/tests/test_integrations.py +140 -0
  36. richrelay-1.0.2/tests/test_rabbitmq.py +64 -0
  37. richrelay-1.0.2/tests/test_redis.py +100 -0
  38. 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`).
@@ -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.
@@ -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
+ [![CI](https://github.com/cumulus13/richrelay/actions/workflows/ci.yml/badge.svg)](https://github.com/cumulus13/richrelay/actions/workflows/ci.yml)
48
+ [![PyPI](https://img.shields.io/pypi/v/richrelay.svg)](https://pypi.org/project/richrelay/)
49
+ [![Python](https://img.shields.io/pypi/pyversions/richrelay.svg)](https://pypi.org/project/richrelay/)
50
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](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
+ [![Architecture](https://files.catbox.moe/1cemxp.jpg)](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
+ [![Store-and-forward](https://files.catbox.moe/uaprl2.jpg)](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
+ [![Channel state machine](https://files.catbox.moe/seo27a.jpg)](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
+ [![Buy Me a Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/cumulus13)
374
+
375
+ [![Donate via Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/cumulus13)
376
+
377
+ [Support me on Patreon](https://www.patreon.com/cumulus13)