logurich
Logurich combines standard-library logging with live Rich renderables, structured context, text and JSON output, and an optional multiprocessing queue.
Installation
pip install logurich
pip install "logurich[click]"
Quick start
from rich.panel import Panel
from logurich import get_logger, init_logger
init_logger("INFO", console="plain", file="json", enqueue=False)
logger = get_logger(__name__)
logger.info("Processed %s items", 12, batch="b-42")
request_logger = logger.bind(request_id=logger.ctx("req-42", style="cyan"))
request_logger.info("Request completed", duration_ms=17)
with logger.contextualize(user_id="alice"):
logger.info("Authenticated")
logger.rich(
"INFO",
Panel("Service ready", border_style="green"),
title="Startup",
prefix=True,
width=80,
)
Context values display their key ([batch=b-42]), whether they are raw or
wrapped in ctx(). Use ctx() to apply a style, rename the key with label=,
or hide it with show_key=False. Context precedence is:
contextualize()/global context < bind() < per-call keywords
Every keyword that is not one of stdlib's four (exc_info, stack_info,
stacklevel, extra) is context; rendering options are parameters of
rich(). To log a stdlib name as context, bind it: logger.bind(exc_info=...).
bind(), new(), unbind(), and try_unbind() always return a new
LogurichLogger; the original adapter is never mutated. None is a real
context value. Remove bound values with unbind() or try_unbind(), remove
selected ambient values with global_context_unset(), or clear all ambient
values with global_clear_context().
Standard-library compatibility
get_logger(name) returns an explicit logging.LoggerAdapter around
logging.getLogger(name). It preserves positional %s formatting,
exc_info, stack_info, stacklevel, custom numeric levels, normal extra,
and the usual logger properties and handler methods.
Each call builds a new adapter, so get_logger("app") is get_logger("app") is
False even though both wrap the same stdlib logger. Adapters are cheap and
hold only their own bound context; level, handlers, and propagation live on the
shared stdlib logger. Compare logger.name rather than adapter identity.
Third-party loggers remain ordinary stdlib loggers:
import logging
logging.getLogger("third.party").info(
"Request completed",
extra={"request_id": "req-42"},
)
When Logurich is configured, flat third-party extra fields are displayed as
context. Third-party loggers do not receive .ctx(), .rich(), .bind(), or
.contextualize() methods. A reusable library should not call init_logger();
the application owns handler configuration and shutdown.
Output modes
Console and file formats are independent:
init_logger(
"INFO",
console="rich", # auto | rich | plain | json
file="json", # text | json
log_filename="app.log",
)
The defaults are console="plain" and file="text".
auto answers "is a human reading this?", not "how pretty can output be": it
resolves to plain text on a TTY and to JSON otherwise, and it never selects the
Rich handler. Rich rendering changes how output is laid out, so it stays an
explicit opt-in via console="rich".
When set, LOGURICH_OUTPUT always takes precedence over console= and
--logger-console; it affects only the console mode. Unset the variable to
honour the Python or CLI argument. An unrecognised value emits a UserWarning
and falls back to the configured mode, so a typo in a shared environment cannot
break startup. LOGURICH_EXTRA_* values continue to be included in JSON
record.extra.
JSON output never renders Rich objects as ASCII art. Text-file output renders
them without ANSI escape codes, while JSON serialises them as structured data
under record.renderables, leaving text and record.message free of borders
and box drawing characters. The JSON schema otherwise keeps the public text
and record structure from Logurich 0.9.
{
"type": "table",
"title": "Metrics",
"columns": ["Name", "Value"],
"rows": [["requests", "42"]]
}
Table, Panel, Tree, Syntax, Markdown, Rule, Layout, Group and
Columns have dedicated shapes; Padding, Align and Constrain are
unwrapped. Any other Rich object degrades to {"type": "text", "text": ...} and
any non-renderable value to {"type": "object", "repr": ...}. Strings passed to
logger.rich() are not structured: they stay in text. Nesting is capped at
four levels and tables at 100 rows, with a "truncated": true marker.
The same converter is public, so tools that build reports from Rich objects can reuse it instead of re-rendering:
from logurich import SCHEMA_VERSION, serialize_renderables
payload = serialize_renderables((table,), max_rows=None, styles=True)
max_depth and max_rows relax the caps (max_rows=None keeps every row), and
styles=True switches to fidelity mode: text values become
{"text": ..., "spans": [...]} objects instead of plain strings, where each
span carries start, end, style and an optional link. Fidelity mode also
adds justify, no_wrap and per-column style to tables, border_style and
alignments to panels, and align/style to rules. Log output always uses the
default mode, so enabling styles never changes what handlers emit.
SCHEMA_VERSION identifies the payload contract. New keys may be added within a
version; existing keys are never renamed or removed.
Premarkup
Premarkup tags transform text before Rich parses styling markup. Unknown tags are left untouched, so Rich still handles them:
from logurich import process_premarkup_to_text
process_premarkup_to_text("[defang]http://evil.test/a[/defang]")
# Text: http[:]//evil[.]test/a
Three actions ship built in: defang (neutralise URLs, domains and e-mails),
color-obs (highlight observables) and truncate-url (shorten long URLs).
Tags may combine actions, which then run in priority order:
[truncate-url defang]...[/truncate-url defang].
Register your own with register_premarkup(name, handler, priority=...); lower
priorities run first. unregister_premarkup() removes one and
premarkup_actions() lists them in execution order.
Premarkup is a standalone utility: it is never applied automatically to log
records, so it costs nothing on the logging path. process_premarkup() returns
a markup string that must be handed to Text.from_markup for its escapes to
resolve; process_premarkup_to_text() does that for you and passes non-string
inputs through unchanged. Inputs longer than MAX_PREMARKUP_INPUT are returned
as-is, and untrusted content should go through rich.markup.escape first, since
the output is markup.
Rich objects
logger.rich(level, *renderables, title="", prefix=True, end="\n", width=None, highlight=False) accepts strings and live Rich objects such as Panel and
Table. Objects stay live until the destination handler renders them. The same
method works after bind() and with direct or queued logging.
For multiprocessing, serialisable Rich values reach the listener unchanged. If
a renderable cannot be pickled, Logurich explicitly falls back to a plain,
ANSI-free producer-side rendering; other unpicklable record values produce a
clear logging error. Such a fallback is a plain string, so in JSON output it
lands in text instead of record.renderables.
Multiprocessing
Only the parent process owns console and file handlers. Every worker must attach the shared queue explicitly:
import multiprocessing as mp
from logurich import (
LogLevels,
configure_child_logging,
get_log_levels,
get_log_queue,
get_logger,
init_logger,
)
def worker(log_queue: mp.Queue, log_levels: LogLevels, worker_id: int) -> None:
configure_child_logging(log_queue, levels=log_levels)
get_logger(f"worker.{worker_id}").info("Worker ready", worker=worker_id)
def main() -> None:
init_logger("INFO", enqueue=True)
queue = get_log_queue()
process = mp.Process(target=worker, args=(queue, get_log_levels(), 1))
process.start()
process.join()
levels is optional: without it a worker enqueues every record and the parent
applies the configured levels. Passing it lets the worker drop filtered records
before they are built and sent.
Execution-local context follows ContextVar rules: asyncio tasks inherit it,
but new threads and processes do not inherit it implicitly. Configure worker
context inside each worker.
Click integration
The optional decorator adds --logger-level, --logger-verbose,
--logger-filename, --logger-level-by-module, and --logger-console:
import click
from logurich import get_logger
from logurich.opt_click import click_logger_params
@click.command()
@click_logger_params
def cli() -> None:
get_logger(__name__).info("Ready")
Examples: my-cli --logger-console rich or
my-cli --logger-console auto. The default is plain.
Lifecycle
Repeated init_logger() calls are no-ops unless force=True is supplied.
Logurich registers shutdown hooks for short-lived programs; call
shutdown_logger() when deterministic teardown is needed in tests or before
reconfiguration.
init_logger() takes over the root logger: clear_handlers=True (the default)
drops handlers installed by a host runtime — AWS Lambda, gunicorn, an APM agent
— so Logurich is the single sink. Pass clear_handlers=False to leave them
attached; they then also receive Logurich records, whose context,
renderables and rich_traceback attributes carry Rich objects a foreign
formatter may fail on.
Users upgrading from 0.9 should read the v1 migration guide.
Development
uv sync --all-groups
uv run pytest
uv run ruff check .
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file logurich-1.0.1.tar.gz.
File metadata
- Download URL: logurich-1.0.1.tar.gz
- Upload date:
- Size: 30.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8d51ab1887845f7f753b09253cdf9c7f2e5ba9d3fa508bb9b8babc214fa543b
|
|
| MD5 |
73a13f1075fbbb876ce971d2d72ba8c3
|
|
| BLAKE2b-256 |
0cc3c225ef2cf5d3d85f93326b86f2d73125cca60a7f5c9c5466e5ba49cebed1
|
Provenance
The following attestation bundles were made for logurich-1.0.1.tar.gz:
Publisher:
ci.yml on PakitoSec/logurich
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
logurich-1.0.1.tar.gz -
Subject digest:
a8d51ab1887845f7f753b09253cdf9c7f2e5ba9d3fa508bb9b8babc214fa543b - Sigstore transparency entry: 2713082838
- Sigstore integration time:
-
Permalink:
PakitoSec/logurich@19a11c537ae50ec0e76930ce6720979e76aff3b9 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/PakitoSec
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@19a11c537ae50ec0e76930ce6720979e76aff3b9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file logurich-1.0.1-py3-none-any.whl.
File metadata
- Download URL: logurich-1.0.1-py3-none-any.whl
- Upload date:
- Size: 34.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1c443a69c0cf00b651d4f14dfcdb0e8e957fc811b0642e62d6721135665425c
|
|
| MD5 |
2069379a2405d0984e0b1b7313e2b38a
|
|
| BLAKE2b-256 |
fb1d52a25ea5403972f0cced76c4d54141e5e80976899a2ae5ce9f7842f791aa
|
Provenance
The following attestation bundles were made for logurich-1.0.1-py3-none-any.whl:
Publisher:
ci.yml on PakitoSec/logurich
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
logurich-1.0.1-py3-none-any.whl -
Subject digest:
e1c443a69c0cf00b651d4f14dfcdb0e8e957fc811b0642e62d6721135665425c - Sigstore transparency entry: 2713082906
- Sigstore integration time:
-
Permalink:
PakitoSec/logurich@19a11c537ae50ec0e76930ce6720979e76aff3b9 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/PakitoSec
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@19a11c537ae50ec0e76930ce6720979e76aff3b9 -
Trigger Event:
push
-
Statement type: