Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/bitranox/lib_log_rich itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 25, 2026lib_log_rich
- Enjoy vibrant Rich-style colored console output with clean formatting.
- Tailor colored (or monochrome) logs with flexible templates and themes, while your OS takes care of persistence.
- Forget about manual logfile rotation.
- Export logs as JSON, HTML or plain text whenever you need.
- Send events to journald, Windows Event Log, or Graylog with ease.
- Get optional OpenTelemetry integration for deeper observability.
- Configure logging per app/user/host via configfile or environment
import logging
import lib_log_rich
from lib_log_rich.runtime import attach_std_logging
lib_log_rich.init(lib_log_rich.RuntimeConfig(service="my-app", environment="dev"))
attach_std_logging()
logging.info("Ready to go!") # existing logging.* calls to the root logger just work
logger = logging.getLogger(__name__) # log to any custom logger
logger.info("Also works fine!") # logging in third party libraries just work
lib_log_rich.shutdown() # shutdown to make sure all records are written to backends like graylog
Table of Contents
- Integration Decision Guide - Quick guidance on choosing LoggerProxy vs stdlib handler.
- Getting Started
- Overview - High-level introduction, key features, and quick-start pointers.
- Installation - Supported Python versions, installation commands (more options in INSTALL.md), and journald prerequisites.
- Journald adapter dependency - Extra host prerequisites for systemd-journald emission.
- Windows Event Log adapter dependency - The
pywin32extra for the Event Log sink. - JSON encoder and free-threaded Python - orjson where it can run, stdlib
jsonon a free-threaded build.
- Logging with LoggerProxy - Minimal example covering configuration, logging, shutdown, and submodule patterns.
- How to use in submodules - Dependency-injection friendly usage.
- Domain helpers - Reusable value objects and helper utilities.
- Custom pipeline wiring - Extending the runtime composition root.
- Context vs. per-event metadata - Binding semantics and best practices.
- exceptions logging - Capturing tracebacks and stack information.
- Opt-in
.envloading - Environment discovery and precedence. - Checking a configuration without starting a runtime -
validate_config()givesinit()'s verdict while logging keeps running.
- Integration Options
- Stdlib compatibility & deliberate differences - Behavioural notes compared to
logging. - Integrating stdlib logging - Step-by-step handler wiring and dump inspection.
- Stdlib compatibility & deliberate differences - Behavioural notes compared to
- Runtime Reference
- Public API - Table summarising exported helpers (
init,getLogger,dump, etc.). - LoggerProxy methods and logdemo - Level helpers,
ProcessResultreturn values, and thelogdemo()preview. - Runtime configuration - Runtime knobs, environment flags, and presets.
- Payload Limits - Message/extra/context sizing and truncation diagnostics.
- Environment-only overrides - Override runtime settings without code changes.
- Public API - Table summarising exported helpers (
- Operations & Observability
- Log dump - Dump formats, filtering options, and customisation templates.
- Inspecting severity and drop metrics - Using
max_level_seen,severity_snapshot, and stress-test counters. - Terminal compatibility - Behaviour across terminals and colour systems.
- Console & Terminal Experience
- Customising per-level colours - Theme overrides and presets.
- CLI and Automation
- CLI entry point - Usage of
python -m lib_log_richand available subcommands.- Streaming console output to other consumers - Queue-backed adapters for GUIs/async consumers.
- Quick smoke-test helpers ship with the package: - Self-test commands and diagnostics.
- CLI entry point - Usage of
- Claude Code skill - Install the
python-loggingskill that teaches a coding agent to use and deploy lib_log_rich. - Further documentation - Links to CLI, dump, style, and architecture guides.
- Development - Contribution guidelines, testing strategy, and project structure.
- License - Project licensing information.
Integration Decision Guide
- Prefer
lib_log_rich.getLogger(...)andLoggerProxywhen you own the emitting code. You gain the full structured pipeline: Rich console output, journald/Windows Event Log/Graylog fan-out, payload sanitisation, context binding, queue-based delivery, and diagnostic return values without touching stdlib internals. - Attach the
StdlibLoggingHandlerviaattach_std_logging()when you must support existinglogginghierarchies (framework defaults, third-party libraries, legacy modules). The handler normalisesLogRecordobjects into the same runtime, preservesextrapayloads, copies call-site metadata (pathname,lineno,funcName), honoursexc_info/stack_info, and skips recursion for events originating insidelib_log_rich. - Mixing both approaches is common: initialise the runtime once, use
LoggerProxyin new code, and bridge whatever still relies on the standard library. All routes share the same ring buffer, payload limits, diagnostics, CLI tooling, and dump facilities.
Getting Started
Overview
Rich-powered logging backbone with contextual metadata, multi-target fan-out (console, journald, Windows Event Log, Graylog), ring-buffer dumps, and
queue-based decoupling for multi-threaded workloads.
Rich renders multi-colour output tuned to each terminal, while adapters and dump exporters support configurable formats and templates.
Each runtime captures the active user, short hostname, process id, and PID chain automatically, so every sink receives consistent system identity fields.
The public API stays intentionally small: initialise once, bind context, emit logs (with per-event extra payloads), dump history in text/JSON/HTML, and shut down cleanly.
Python requirement: lib_log_rich targets Python 3.10 and newer. Free-threaded builds: 3.14t and 3.15t are supported, 3.13t is not (see JSON encoder and free-threaded Python). Core dependencies:
lib_cli_exit_tools,orjson(below Python 3.14; from 3.14 the optionalorjsonextra),pydantic,rich,rich-click, andpython-dotenv(the version floors live inpyproject.toml).
- colored terminal logs via rich, with UTC or local timestamps
- supports journald
- supports Windows Event Logs
- supports Graylog via Gelf (and gRPC after adding Open Telemetry Support)
- supports quick log-dump with filtering from the ringbuffer without leaving the application
- runtime configuration validated via Pydantic models, yielding structured errors and JSON schemas
- per-event payload guards (4KB messages, 8KB extras, depth limits) configurable via
payload_limits - independent log levels:
console_level,backend_level, andgraylog_levelare completely independent - each gates events to its respective adapter (console, journald/EventLog, Graylog) without affecting the others. Useget_minimum_log_level()to find the lowest threshold for stdlib integration. - opt-in
.envloading (same precedence for CLI and programmatic use) - Open Telemetry Support on user (Your) request - not implemented yet (because I do not need it myself). If You need it, let me know.
- optional
diagnostic_hookcallback that observes the runtime without modifying it. The hook reports queue activity (drops, worker failures, drop-handler errors), rate limiting, and payload clamps so you can surface metrics, alerts, or dashboards while keeping the logging pipeline decoupled from any monitoring stack. - queue-backed console adapters (ANSI or HTML) -
QueueConsoleAdapterfor threads andAsyncQueueConsoleAdapterfor asyncio - exposed viaconsole_adapter_factory, handy for Textual panes, SSE/WebSocket streams, or tests. - EXAMPLES.md - runnable snippets from Hello World to multi-backend wiring.
Installation
For a quick start from PyPI:
pip install lib_log_rich
Detailed installation options (venv, pipx, uv, Poetry/PDM, and Git installs) live in INSTALL.md. If you plan to enable systemd journald logging, follow the extra host prerequisites in INSTALL_JOURNAL.md.
Journald adapter dependency
The Journald adapter requires the systemd-python bindings. Install via the optional extra:
# Requires libsystemd-dev system package to build
sudo apt-get install libsystemd-dev # Debian/Ubuntu
pip install lib_log_rich[journald]
Alternatively, use your distro's pre-built package (sudo apt-get install python3-systemd on Debian/Ubuntu) which avoids the build step.
Without the bindings, enable_journald=True still works on a systemd host: the adapter falls back to writing the journal's native datagram format to /run/systemd/journal/socket (then /dev/log). The bindings remain the supported path. A write that no journal socket accepts fails that one sink, which the runtime reports as an adapter_error diagnostic while the other sinks still receive the event. The adapter sets no SYSLOG_IDENTIFIER, so journalctl -t <name> finds nothing from it; query by field instead: journalctl SERVICE=<service> -o verbose. See INSTALL_JOURNAL.md for a deeper walkthrough covering Linux service managers and verification steps.
Windows Event Log adapter dependency
The Windows Event Log adapter calls win32evtlogutil.ReportEvent from pywin32. Install it via the optional extra (the marker makes it a no-op off Windows):
pip install lib_log_rich[eventlog]
Without pywin32, init(RuntimeConfig(..., enable_eventlog=True)) still succeeds, but every Event Log write fails with RuntimeError("pywin32 is required for Windows Event Log support"), reported as an adapter_error diagnostic.
JSON encoder and free-threaded Python
Free-threaded (no-GIL) Python builds:
| Build | Supported | Notes |
|---|---|---|
3.15t | yes | tested in CI on Linux and Windows |
3.14t | yes | tested in CI on Linux and Windows |
3.13t | no | does not install: pydantic-core and orjson ship no 3.13t build, and pydantic-core's build tooling (PyO3) dropped 3.13t |
Events, checkpoints and JSON dumps are encoded with orjson where it can run, and with the standard library's json otherwise. orjson has no free-threaded build, so on a free-threaded interpreter lib_log_rich never imports it, and the GIL stays off. Below Python 3.14 orjson is installed with the package. From 3.14, the first release with supported free-threading, it is the optional orjson extra, because no installer marker can tell a free-threaded build from a normal one; on a normal 3.14 or later add it for the faster encoder:
pip install "lib_log_rich[orjson]" # normal (GIL) Python 3.14+; never on a free-threaded build
Both encoders write the same JSON, with orjson's choices for the values the standard library treats differently: null for NaN and infinity, ISO text for datetimes, a UUID's string form, an Enum's value. Two differences remain, valid JSON either way: a float exponent is spelled 1e-07 rather than 1e-7, and an int beyond 64 bits is written out where orjson refuses it.
Why we recommend uv: supply-chain protection
This repository commits a uv.lock. It pins every package the project needs, transitive
dependencies included, to an exact version and to the hashes of that version's files. The
exclude-newer setting under [tool.uv] in pyproject.toml adds a cooldown: uv leaves any
release younger than that out of the lock. CI installs from the lock, so a hijacked release that
gets pulled again within the cooldown never reaches our builds, and a file whose hash does not
match is refused.
That protection stays in this repository. The installable package declares only lower bounds
(>=) for its dependencies, so a plain pip install takes the newest release of every transitive
dependency at the moment you run it. If one of them was hijacked an hour earlier, you get the
hijacked version. pipx install and uv tool install without options do the same.
To install the command line tool with a cooldown of your own (uv then skips any release younger than 7 days, transitive ones included), or to run the exact tree our CI tests:
uv tool install --exclude-newer "7 days" lib_log_rich
git clone https://github.com/bitranox/lib_log_rich.git && cd lib_log_rich && uv sync --locked && uv run lib_log_rich --help
If you depend on lib_log_rich in your own project, give that project the same cooldown (7 days here, as
an example) and commit the uv.lock that uv add lib_log_rich writes:
[tool.uv]
exclude-newer = "7 days"
Logging with LoggerProxy
import lib_log_rich as log
config = log.RuntimeConfig(
service="my-service",
environment="dev",
queue_enabled=False,
enable_graylog=False,
)
log.init(config)
with log.bind(job_id="startup", request_id="req-001"):
logger = log.getLogger("app.http")
logger.info("ready", extra={"port": 8080})
# Inspect the recent history (text/json/html_table/html_txt)
print(log.dump(dump_format="json"))
log.shutdown()
How to use in submodules
Initialise the runtime once near your process entrypoint, then let every
module fetch its own LoggerProxy via lib_log_rich.getLogger. The runtime keeps the
Rich-backed pipeline global, so submodules only need the name they wish to log
under.
# entrypoint.py
import lib_log_rich as log
log.init(log.RuntimeConfig(service="billing", environment="prod"))
from billing import payments # import after init so the runtime exists
payments.PaymentProcessor().charge_order("ord-42")
log.shutdown() # drain the queue so every record reaches its sinks
# billing/payments.py
from __future__ import annotations
from collections.abc import Callable
from lib_log_rich import bind, getLogger
from lib_log_rich.runtime import LoggerProxy
LoggerFactory = Callable[[str], LoggerProxy]
class PaymentProcessor:
def __init__(self, get_logger: LoggerFactory | None = None) -> None:
self._get_logger = get_logger or getLogger
def charge_order(self, order_id: str) -> None:
logger = self._get_logger(__name__)
with bind(extra={"order_id": order_id}):
logger.info("Submitting charge", extra={"provider": "stripe"})
# Composition root
processor = PaymentProcessor()
processor.charge_order("ord-123")
- Key points:
- No module-level mutable state; PaymentProcessor takes a LoggerFactory, so tests can supply a stub while production code uses lib_log_rich.getLogger.
- The binding is scoped inside the method, which is explicit and test-friendly.
- You can register a singleton instance if desired (processor = PaymentProcessor()), yet the module stays stateless and aligns with the repo's dependency-injection guidelines.
If you just want a module-level logger but can not guarantee lib_log_rich.init(...) ran before the module is imported, add a tiny helper that initialises on demand once and then hands back the cached proxy. The helper keeps the module stateless and makes reuse in notebooks or ad-hoc scripts painless while still respecting the single-runtime rule.
# billing/payments.py
from __future__ import annotations
from lib_log_rich import RuntimeConfig, bind, getLogger, init
from lib_log_rich.runtime import LoggerProxy, is_initialised
def ensure_logging() -> LoggerProxy:
if not is_initialised():
try:
init(RuntimeConfig(service="billing", environment="prod"))
except RuntimeError:
# Another thread/process section won the race; re-check before propagating.
if not is_initialised():
raise
return getLogger(__name__)
logger: LoggerProxy = ensure_logging()
def charge_order(order_id: str) -> None:
with bind(extra={"order_id": order_id}):
logger.info("Submitting charge", extra={"provider": "stripe"})
If you are certain lib_log_rich.init(...) already executed (for example in your CLI entrypoint), you can replace ensure_logging() with a direct getLogger(__name__) assignment and keep the rest identical.
Domain helpers
The domain package (lib_log_rich.domain) exposes reusable value objects so you can keep adapters and feature modules decoupled from implementation modules. A few shortcuts you might want immediately:
import logging
from datetime import datetime, timezone
from lib_log_rich.domain import DumpFormat, LogContext, LogEvent, LogLevel, build_dump_filter
# Translate stdlib levels into the richer domain enum (icons, display metadata, etc.).
domain_level = LogLevel.from_python_level(logging.WARNING)
# Convert back when bridging to the stdlib logging module.
python_level = domain_level.to_python_level()
# Parse dump format values coming from CLI flags or environment variables.
dump_format = DumpFormat.from_name("json")
# Build reusable filters and exercise them against recorded events.
filters = build_dump_filter(context={"service": "billing"}, extra={"tenant": "acme"})
event = LogEvent(
event_id="evt-1",
timestamp=datetime.now(tz=timezone.utc),
logger_name="billing.worker",
level=LogLevel.INFO,
message="ready",
context=LogContext(service="billing", environment="prod", job_id="worker-1", extra={"tenant": "acme"}),
extra={"tenant": "acme", "order_id": "ORD-7"},
)
should_emit = filters.matches(event)
LogLevelkeeps conversions idempotent (from_name,from_python_level,to_python_level, andcoerce, which accepts a member, a name or a stdlib integer), so threading a standardlogging.LogRecordlevel through Rich adapters only needs a single call.DumpFormat.from_name(...)parses human-friendly inputs ("json","html_table", etc.) and keeps the call site self-documenting.build_dump_filter(...)returns aDumpFilteryou can reuse in unit tests, notebook exploration, or dump pipelines by invokingmatches(...)or handing its field tuples to the runtime façade.LoggerProxy.log(level, msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None)mirrors the stdliblogging.Loggersignature while still normalising enum/string/integer levels. Messages are formatted lazily inside the process pipeline.exc_infocan beTrue, an exception instance or a(type, value, traceback)tuple (ready-made traceback text goes inextra={"exc_info": text}; a string given as theexc_infoargument is ignored), andstack_info=Truecaptures the current stack; both are stored as text on the event (see Exceptions logging). Thestacklevelkeyword is accepted for API parity and currently ignored.LoggerProxy.exception(msg, *args, exc_info=True, stack_info=False, stacklevel=1, extra=None)matcheslogging.Logger.exception: it logs atLogLevel.ERROR, defaultsexc_infotoTrue(capturing the active exception), and otherwise delegates to the same structured pipeline as.error(...).LoggerProxy.setLevel(level)updates the proxy's own threshold. Events below that level are discarded before entering the processing pipeline, so a message must pass the proxy level and each handler's level (console/backend/Graylog) to be emitted.
Custom pipeline wiring
Need to instrument specific collaborators (for example, injecting fakes in a benchmark or swapping adapters without reinitialising the runtime)? Assemble the process pipeline directly with the public dependency bundle:
from datetime import datetime, timedelta, timezone
from rich.console import Console
from lib_log_rich import application
from lib_log_rich.adapters import RegexScrubber, RichConsoleAdapter, SlidingWindowRateLimiter
from lib_log_rich.application import ProcessPipelineDependencies
from lib_log_rich.domain import ContextBinder, LogLevel, RingBuffer, SeverityMonitor
from lib_log_rich.domain.identity import SystemIdentity
from lib_log_rich.runtime import PayloadLimits
class StubClock:
def now(self) -> datetime:
return datetime.now(tz=timezone.utc)
class StubIdProvider:
def __call__(self) -> str:
return "evt-stub"
class StubIdentity:
def resolve_identity(self) -> SystemIdentity:
return SystemIdentity(user_name="demo", hostname="example", process_id=1234)
binder = ContextBinder()
dependencies = ProcessPipelineDependencies(
context_binder=binder,
ring_buffer=RingBuffer(max_events=64),
severity_monitor=SeverityMonitor(),
console=RichConsoleAdapter(console=Console(record=True), no_color=True),
console_level=LogLevel.INFO,
structured_backends=(),
backend_level=LogLevel.INFO,
graylog=None,
graylog_level=LogLevel.ERROR,
scrubber=RegexScrubber(patterns={}),
rate_limiter=SlidingWindowRateLimiter(max_events=10, interval=timedelta(seconds=1)),
clock=StubClock(),
id_provider=StubIdProvider(),
limits=PayloadLimits(),
identity=StubIdentity(),
)
process = application.create_process_log_event(dependencies)
with binder.bind(service="custom", environment="demo", job_id="example"):
payload = process(
logger_name="example.pipeline",
level=LogLevel.INFO,
message="hello",
extra={"scope": "demo"},
)
assert payload.ok is True
# The stub classes above satisfy the relevant ports (`ClockPort`, `IdProvider`,
# `SystemIdentityPort`) so you can compose the pipeline without touching
# runtime internals. In production you would rely on the factories exported via
# ``lib_log_rich.application`` instead.
Context vs. per-event metadata
lib_log_rich.bind(...) establishes the context for subsequent log calls: service, environment, job/request identifiers, trace/span IDs, user, hostname, PID, and optional LogContext.extra. The extra argument on bind is a stable mapping you want attached to every event in that scope (for example, deployment labels or tenant metadata). Every event emitted inside the bound scope inherits the entire context automatically.
The extra= argument on logger.debug/info/... supplements a single event with ad-hoc details (order IDs, feature flags, timing data). The runtime merges the per-event extra with the bound context to form the structured payload that adapters see.
Payload limits apply to both buckets: context extras are capped at 20 keys/256 characters, while per-event extras default to 25 keys/512 characters with depth and aggregate guards. Oversized values are truncated (with a …[truncated] suffix) and the optional diagnostic hook receives events such as extra_keys_dropped, extra_value_truncated_depth_collapsed, or context_extra_keys_dropped when clamping occurs. Nested mappings deeper than extra_max_depth are collapsed into JSON strings so adapters (and downstream storage) are spared from unbounded recursion.
For example:
import lib_log_rich as log
log.init(log.RuntimeConfig(service="billing", environment="prod", queue_enabled=False))
# Context-wide extras travel with every event in the scope
with log.bind(
service="billing",
environment="prod",
job_id="invoice-processor",
extra={"deployment": "blue", "team": "finops"},
):
logger = log.getLogger("billing.worker")
# Per-event extras describe this specific message
logger.info(
"processed invoice",
extra={"invoice_id": "INV-42", "duration_ms": 183},
)
# The emitted event includes:
# context.extra -> {"deployment": "blue", "team": "finops"}
# event.extra -> {"invoice_id": "INV-42", "duration_ms": 183}
log.shutdown()
Exceptions logging
.exception(...), or exc_info=True, an exception instance or a (type, value, traceback) tuple on any level helper, records the formatted traceback text that traceback.format_exception produces, as stdlib logging does. Stdlib records arriving through attach_std_logging() are recorded the same way. Ready-made traceback text passed as extra["exc_info"] moves out of extra into the event's exc_info field without being re-formatted (a string given as the exc_info argument itself is silently ignored), and stack_info=True records the formatted stack:
import json
import lib_log_rich as log
log.init(log.RuntimeConfig(service="svc", environment="dev", queue_enabled=False))
logger = log.getLogger("billing.worker")
try:
raise RuntimeError("upstream failed")
except RuntimeError:
logger.exception("job crashed") # records the formatted traceback
logger.warning("slow call", stack_info=True) # records the formatted stack
events = json.loads(log.dump(dump_format=log.DumpFormat.JSON))
print(events[0]["exc_info"])
log.shutdown()
The text is stored on LogEvent.exc_info / LogEvent.stack_info, and every sink shows it, exc_info first, as stdlib logging does: the Rich console and the text and HTML text dumps print it indented under the event line, the HTML table puts it in the message cell, Graylog sends it as GELF full_message, journald appends it to MESSAGE and also sends EXCEPTION_TEXT / STACK_TEXT, and the Windows Event Log adds exc_info=... / stack_info=... strings. JSON dumps and the ring-buffer checkpoint carry both fields. The scrubber does not touch this text, just as it leaves the message alone, so an exception message reaches every sink as written.
Long traces are compacted in two steps, and the diagnostic hook receives exc_info_truncated / stack_info_truncated for each:
stacktrace_max_frames(default 10) counts frames: aFile "...", line N, in fline plus the indented source and caret lines under it. When the text holds more than twice that many frames, the first and laststacktrace_max_framesframes are kept and the frames between are removed: each run of removed frames becomes one... truncated K frame(s) ...line at that run's indentation, and the diagnostic carriesframes_removed, the sum of K over all runs. Only frames are removed: everyTraceback (most recent call last):header, exception line and separator of a chained exception or an exception group is kept, and so is a[Previous line repeated N more times]line whose frame stays. Text with no frame lines is not frame-compacted.- Text still longer than
extra_max_value_chars(default 512) is cut by keeping its end, which for most tracebacks is the innermost frames and the exception line; an exception message longer than the limit fills the kept text on its own. The kept text starts with[truncated]…, and the diagnostic carriesreason,original_length,new_lengthandkey. Raiseextra_max_value_charsinpayload_limitsto keep longer traces.
Opt-in .env loading
lib_log_rich honours real environment variables over RuntimeConfig fields (LOG_SERVICE, LOG_CONSOLE_LEVEL, and friends). The .env helpers let you keep that precedence while sourcing defaults from a project-local file:
import lib_log_rich as log
import lib_log_rich.config as log_config
log_config.enable_dotenv() # walk upwards from cwd, load the first .env found
config = log.RuntimeConfig(service="svc", environment="dev", queue_enabled=False)
log.init(config)
...
log.shutdown()
Key points:
.envloading is explicit - nothing is read unless you callenable_dotenv().- Precedence stays intact: CLI flag -> real
os.environ-> discovered.env-> defaults. - Search walks up from the cwd (or
search_from) and stops at the first.env, at a directory holding apyproject.toml/.gitmarker, or at a directory it cannot look into. A.envthat is not UTF-8 raisesDotenvDecodeError, naming the file and byte offset but none of its content. - Pass
dotenv_override=Truewhen you intentionally want.envvalues to win over real environment variables.
See DOTENV.md for more detail, examples, and CLI usage.
Checking a configuration without starting a runtime
validate_config(config) raises the ValueError that init(config) would raise, with the same message, but never starts a runtime and never touches one that is already running. Use it to judge a candidate configuration (a reloaded config file, a --profile switch, an edited .env) while logging keeps working, instead of shutting down and initialising again to find out.
import lib_log_rich as log
candidate = log.RuntimeConfig(service="svc", environment="dev", console_level="loud")
try:
log.validate_config(candidate)
except ValueError as exc:
print(exc) # Invalid runtime settings: console_level: Unknown log level: 'loud'
Key points:
init()andvalidate_config()resolve settings through the same code, so they cannot disagree: a config one accepts, the other accepts.- Environment overrides (
LOG_*, including a loaded.env) apply exactly as they would forinit(), so the verdict holds for aninit()made in the same environment. - Refused before any adapter is built: unknown level names (in the config or in
LOG_CONSOLE_LEVEL/LOG_BACKEND_LEVEL/LOG_GRAYLOG_LEVEL), scrub patterns that do not compile (config orLOG_SCRUB_PATTERNS), an unknown name inLOG_CONSOLE_THEME,LOG_CONSOLE_STREAM,LOG_CONSOLE_FORMAT_PRESET,LOG_DUMP_FORMAT_PRESETorLOG_GRAYLOG_PROTOCOL, the console-onlyshort_loc_iconas dump preset, aring_buffer_sizethat is not positive (or aLOG_RING_BUFFER_SIZEthat is not an integer), a malformedLOG_GRAYLOG_ENDPOINTorLOG_RATE_LIMIT, Graylog enabled without an endpoint, UDP combined with TLS while Graylog is enabled, aring_buffer_checkpointthat is a directory, an empty service or environment (including an emptyLOG_SERVICE/LOG_ENVIRONMENT), and, while the built-in console is used, a console style key that is not a level name. A customconsole_adapter_factorydecides for itself what style keys mean, so those are not checked then. - An unparsable
LOG_QUEUE_MAXSIZE,LOG_QUEUE_FULL_POLICY,LOG_QUEUE_PUT_TIMEOUTorLOG_QUEUE_STOP_TIMEOUTis not refused: it is ignored and the configured value stays. - An unknown theme, Graylog protocol, queue policy, console stream or format preset, and an invalid
payload_limitsvalue, never reachvalidate_config():RuntimeConfig(...)itself refuses them with a pydanticValidationError(aValueErrorsubclass) naming the field. RuntimeConfig(...)also refuses a keyword that is not one of its fields, so a misspelled setting (ring_bufer_size=5) raises aValidationErrornaming it instead of leaving the intended value at its default.- Two refusals are outside its reach, because it builds no adapters and reads no files: an adapter from
console_adapter_factorythat lacks aConsolePortmethod (emitorflush), whichinit()refuses with aTypeErrornaming the missing method, and aring_buffer_checkpointfile holding a line it cannot load, whichinit()refuses with aValueErrornaming the file and line. - A refusal reads
Invalid runtime settings: <reason>, with the field orLOG_variable in front of the reason when a single one is to blame (console_level: Unknown log level: 'loud'); several problems in the settings models are joined with;.
Integration Options
Stdlib compatibility & deliberate differences
LoggerProxy implements the core logging.Logger surface so existing call sites rarely need adjustment. Supported helpers are:
.debug(...),.info(...),.warning(...),.error(...),.critical(...).exception(...)(defaultsexc_info=Truejust like the stdlib).log(level, ...).setLevel(level)
Key differences from the stdlib logging framework:
- Calls return a
ProcessResultdescribing the emission outcome instead ofNone. - No hierarchical logger tree: we do not expose the root logger,
getChild(), propagation, or handler management. EachgetLogger(name)call yields an independent proxy whose name is metadata only. - Aliases such as
.warn(...),.fatal(...),isEnabledFor(...),addHandler(...), and handler lists are intentionally unsupported. stacklevelis accepted but currently ignored. Stack trace capture is explicit: provideexc_infoorstack_infoto record traces. The runtime stores the formatted, compacted text on the event, and every sink shows it (see exceptions logging).- Console output defaults to stderr (matching the standard library logger), but you can steer it via
RuntimeConfig.console_streamtoConsoleStream.STDOUT,ConsoleStream.STDERR,ConsoleStream.BOTH,ConsoleStream.CUSTOM(supplyconsole_stream_target), orConsoleStream.NONEto suppress console output entirely (RuntimeConfigalso parses the value strings"stdout","stderr","both","custom"and"none";ConsoleStreamcomes fromlib_log_rich.domain). You can also pass a no-opconsole_adapter_factoryfor bespoke mute behaviour. Without a console sink nothing touches stdio; events continue to flow through the configured backends.
When migrating code that relies on root/sub-logger behaviour or handler mutation, refactor to request proxies explicitly via lib_log_rich.getLogger(name) and configure sinks through RuntimeConfig instead of the stdlib globals.
Integrating stdlib logging
Existing projects often expose module-level loggers via logging.getLogger(). The runtime ships with a bridge so those callers can continue to emit without refactoring.
import logging
from lib_log_rich.runtime import RuntimeConfig, attach_std_logging, init
init(RuntimeConfig(service="legacy-app", environment="prod", queue_enabled=False, enable_graylog=False))
attach_std_logging() # logger_level defaults to get_minimum_log_level()
logging.getLogger(__name__).info("legacy path", extra={"tenant": 42})
if __name__ == "__main__":
from lib_log_rich.runtime import dump
print(dump())
attach_std_logging(...)registers a singleStdlibLoggingHandleron the chosen logger (root by default). By default it sets the logger level toget_minimum_log_level()so events aren't pre-filtered before reaching lib_log_rich, and setspropagate=Falseto prevent duplicate emission. Passlogger_level=Noneto leave the level unchanged.- Every
logging.LogRecordis translated into the runtime pipeline: message/arguments flow through the same sanitiser asLoggerProxy,extrametadata travels unchanged, and a record'sexc_info/stack_infois stored as formatted text exactly as forLoggerProxy(see Exceptions logging). - Location metadata (
pathname,lineno,funcName, plus any customextrafields) is copied into the event so dumps, Graylog, and Rich console output can display the original call site. - Records originating from
lib_log_richare kept out of the pipeline to avoid recursion. When the bridge is the only handler such a record would reach, warnings and errors (an adapter that failed, for example) go to stderr throughlogging.lastResort, exactly as stdlib prints them when no handler is configured; with a handler of your own on the chain, that handler prints them instead. Setextra={"lib_log_rich_skip": True}if you need to suppress specific third-party records; those are dropped silently.
Understanding get_minimum_log_level()
attach_std_logging() automatically sets the logger level to get_minimum_log_level(), which returns the lowest (most permissive) threshold among active adapters. This ensures stdlib doesn't pre-filter events before they reach lib_log_rich.
import logging
import lib_log_rich as log
from lib_log_rich.runtime import attach_std_logging
# Initialize with independent levels for different adapters
config = log.RuntimeConfig(
service="my-service",
environment="prod",
console_level="INFO", # Console shows INFO and above
backend_level="WARNING", # Journald/EventLog shows WARNING and above
graylog_level="ERROR", # Graylog shows ERROR and above
enable_graylog=True,
graylog_endpoint=("graylog.example.com", 12201),
)
log.init(config)
attach_std_logging() # Automatically uses get_minimum_log_level() -> INFO
# Now stdlib loggers won't pre-filter
logging.getLogger("app.module").info("This reaches lib_log_rich console")
logging.getLogger("app.module").debug("This is filtered by lib_log_rich, not stdlib")
log.shutdown()
Key points:
- Independent levels:
console_level,backend_level, andgraylog_levelare completely independent. Each gates events to its respective adapter without affecting the others. get_minimum_log_level(): Returns the lowest (most permissive) threshold among active adapters.backend_levelcounts only when journald or the Windows Event Log is enabled, andgraylog_levelonly when Graylog is enabled.- Automatic integration:
attach_std_logging()uses this by default; passlogger_level=Noneto leave the stdlib logger level unchanged.
Runtime Reference
Public API
All runtime configuration flows through RuntimeConfig. Create an instance with the desired fields and pass it to lib_log_rich.init(config). The table below summarises the fields and related helpers.
| Symbol | Signature (abridged) | Description |
|---|---|---|
init | init(config: RuntimeConfig) | Composition root. Wires adapters, queue, scrubber, and rate limiter. Pass a RuntimeConfig instance; environment variables listed below continue to override matching fields. |
validate_config | validate_config(config: RuntimeConfig) -> None | Raises the ValueError init() would raise for config, with the same message, without starting a runtime or touching a running one. Environment overrides apply exactly as for init(). Use it to check a candidate configuration while logging is running instead of shutting down and initialising again. |
bind | bind(*, service=None, environment=None, job_id=None, request_id=None, user_id=None, user_name=None, hostname=None, process_id=None, process_id_chain=None, trace_id=None, span_id=None, extra=None) (context manager) | Binds contextual metadata for the scope. The fields are keyword-only; anything else goes in extra (for example bind(extra={"order_id": order_id})), and an unknown field raises TypeError. None inherits the parent frame's value; init() primes a base frame from the config's service and environment with job_id="bootstrap", so a scope names only what it adds or overrides. Yields the active LogContext. |
getLogger | getLogger(name: str) -> LoggerProxy | Returns a LoggerProxy exposing .debug/.info/.warning/.error/.critical/.exception. Each call returns a ProcessResult (e.g. ProcessResult(ok=True, event_id="...") or ProcessResult(ok=False, reason=DropReason.RATE_LIMITED)). |
LoggerProxy | created via getLogger(name) | Lightweight facade around the process use case. Level helpers mirror the stdlib signature: .debug(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None) (and similarly for info/warning/error/critical) plus .exception(msg, *args, exc_info=True, ...), .log(level, msg, *args, ...), and .setLevel(level). Messages are formatted in the pipeline, exc_info/stack_info are stored on the event as formatted traceback/stack text (see Exceptions logging), and stacklevel is accepted but ignored. .setLevel(...) updates the proxy's own threshold so events must satisfy it and the handler levels; calls still return a ProcessResult (unlike logging.Logger, which returns None). |
dump | dump(*, dump_format: str | DumpFormat = "text", path=None, level: str | LogLevel | None = None, console_format_preset: FormatPreset | None = None, console_format_template=None, theme: ConsoleTheme | None = None, console_styles=None, context_filters=None, context_extra_filters=None, extra_filters=None, color=False) -> str | Serialises the ring buffer (DumpFormat.TEXT, JSON, HTML_TABLE, HTML_TXT, or their value strings). theme and console_format_preset take enum members only; a plain string raises TypeError. level filters events by severity, presets/templates customise text rendering (template wins; FormatPreset.SHORT_LOC_ICON is console-only and a text dump refuses it with ValueError), theme/console_styles reuse or override the runtime palette, the context_*/extra_* filter mappings narrow results by metadata, and color toggles ANSI output for text dumps. Payloads are always returned and optionally written to path. |
get_minimum_log_level | get_minimum_log_level() -> LogLevel | Returns the minimum (most permissive) log level among all active adapters: the lowest of console_level, backend_level (only when journald or the Windows Event Log is enabled) and graylog_level (only when Graylog is enabled). Useful for setting the stdlib logging.Logger root level to match the lowest threshold, ensuring no events are pre-filtered before reaching lib_log_rich. Raises RuntimeError if called before init(). |
max_level_seen | max_level_seen() -> LogLevel | None | Returns the highest severity observed since the runtime was initialised or metrics were reset, or None before any event. Handy for "only dump logs when something >= ERROR happened" checks. |
severity_snapshot | severity_snapshot() -> SeveritySnapshot | Captures totals, per-level counts, threshold buckets, and drop statistics (by reason and severity). Safe to call from any thread. |
reset_severity_metrics | reset_severity_metrics() -> None | Clears severity and drop counters without touching the ring buffer or adapters. Invoke after you've handed dumps to operators. |
shutdown | shutdown() -> None | Flushes adapters, drains and stops the queue, persists the ring buffer to its checkpoint (when ring_buffer_checkpoint is set), and clears global state. Calling it again before another init() raises RuntimeError, as does calling it inside a running event loop (await shutdown_async() there). |
shutdown_async | async shutdown_async() -> None | Async variant of shutdown() for code running in an event loop (shutdown() raises RuntimeError there). It waits for the queue worker on a worker thread, so a diagnostic hook that hands work back to the loop (call_from_thread, run_coroutine_threadsafe(...).result()) cannot deadlock it. |
flush | flush(timeout: float | None = None, *, flush_ring_buffer: bool = False) -> None | Drains queues and flushes all adapters (console, Graylog) without terminating the runtime. Unlike shutdown(), logging remains active after this call. Raises TimeoutError if the queue doesn't drain within timeout (default: the configured queue_stop_timeout, or 5.0 s when that is None). flush_ring_buffer=True appends the buffer's events to its checkpoint and clears the buffer, when the runtime has one: init() configures it from ring_buffer_checkpoint (env LOG_RING_BUFFER_CHECKPOINT); without it flush_ring_buffer=True does nothing and the buffer is preserved. Raises RuntimeError if called from within an active event loop. |
flush_async | flush_async(timeout: float | None = None, *, flush_ring_buffer: bool = False) -> None | Async variant of flush(). Awaitable from async contexts. Same behaviour: drains queue, flushes adapters, keeps runtime active. Raises TimeoutError on queue drain timeout. It waits for the queue worker on a worker thread, so a diagnostic hook that hands work back to the loop (call_from_thread, run_coroutine_threadsafe(...).result()) cannot deadlock it. |
hello_world | hello_world() -> None | Prints the canonical "Hello World" message for smoke tests. |
i_should_fail | i_should_fail() -> None | Raises RuntimeError("I should fail") to exercise failure handling paths. |
summary_info | summary_info() -> str | Returns the CLI metadata banner as a string without printing it. |
logdemo | logdemo(*, theme=ConsoleTheme.CLASSIC, service=None, environment=None, dump_format: DumpFormat | None = None, dump_path=None, color=None, console_format_preset: FormatPreset | None = None, console_format_template=None, dump_format_preset: FormatPreset | None = None, dump_format_template=None, context_filters=None, context_extra_filters=None, extra_filters=None, enable_graylog=False, graylog_endpoint=None, graylog_protocol=GraylogProtocol.TCP, graylog_tls=False, enable_journald=False, enable_eventlog=False) -> LogDemoResult | Spins up a temporary runtime, emits one sample event per level, optionally renders a dump, and records which backends were requested in LogDemoResult.backends (a BackendStatus with graylog, journald and eventlog flags). The result also carries theme, styles, events (one ProcessResult per level), dump, service and environment. Use the boolean flags to exercise Graylog, journald, or Windows Event Log sinks from the CLI or API. |
SeveritySnapshot | frozen dataclass returned by severity_snapshot() | Fields: highest, total_events, counts, thresholds, dropped_total, drops_by_reason (keyed by DropReason), drops_by_level, drops_by_reason_and_level (keyed by (DropReason, LogLevel)). All mappings are read-only copies so you can serialise them directly. |
QueueConsoleAdapter | QueueConsoleAdapter(queue, *, export_style=ExportStyle.ANSI, force_color=False, no_color=False, styles=None, format_preset: FormatPreset | None = None, format_template=None, console_width=None) | Threaded console adapter (import from lib_log_rich.runtime or lib_log_rich.adapters.console) that renders Rich output into a queue.Queue. It reuses the Rich formatter so console level thresholds and styling stay consistent. export_style and format_preset take enum members; a plain string raises TypeError. Pass via console_adapter_factory to stream ANSI or HTML lines to GUIs, SSE/WebSocket feeds, or background workers without touching global state. |
AsyncQueueConsoleAdapter | AsyncQueueConsoleAdapter(queue, *, export_style=ExportStyle.ANSI, force_color=False, no_color=False, styles=None, format_preset: FormatPreset | None = None, format_template=None, console_width=None, on_drop=None, loop=None) | Asyncio variant (import from lib_log_rich.runtime or lib_log_rich.adapters.console) targeting asyncio.Queue producers/consumers. It shares the Rich formatter and level gate with the default console adapter. Ideal for Textual apps, async servers, or tests that await console output; wire it with console_adapter_factory alongside the threaded adapter. Chunks are enqueued with put_nowait, so full queues drop latest segments; on_drop receives each dropped segment (without it the adapter logs a warning). An emit from another thread (the runtime queue worker) is handed to the queue's loop with call_soon_threadsafe; that loop is the one running at construction, or loop=. |
ExportStyle | ExportStyle.ANSI, ExportStyle.HTML | String enum (from lib_log_rich.domain or lib_log_rich.runtime) selecting the payload format returned by queue-backed console adapters (ANSI for terminal passthrough, HTML for rich web panes). |
attach_std_logging | attach_std_logging(*, logger=None, handler_level=None, logger_level=<minimum>, propagate=False) -> StdlibLoggingHandler | Import from lib_log_rich.runtime. Registers one StdlibLoggingHandler on logger (the root logger by default) and returns it. handler_level sets the handler's own level; logger_level defaults to get_minimum_log_level() and None leaves the logger level unchanged; propagate sets the logger's propagate flag. See Integrating stdlib logging. |
ConsoleTheme, DumpFormat, FormatPreset, GraylogProtocol | enums | Importable from lib_log_rich as well as from lib_log_rich.domain. dump() and logdemo() take theme and the preset parameters as enum members only. |
LoggerProxy methods and logdemo
LoggerProxy instances returned by getLogger() support the standard logging-level methods, and every call that passes the proxy's own level feeds the runtime's severity monitor (see Inspecting severity and drop metrics).
import lib_log_rich as log
config = log.RuntimeConfig(service="svc", environment="metrics", queue_enabled=False)
log.init(config)
with log.bind(job_id="run-42"):
log.getLogger("svc.worker").info("started")
log.getLogger("svc.worker").error("boom")
# `LoggerProxy` instances returned by `getLogger()` support the standard logging-level methods:
logger = log.getLogger("app.component")
logger.info("payload %s", "alice", extra={"user": "alice"})
logger.error("boom", exc_info=True)
logger.warning("slow call", stack_info=True)
try:
raise RuntimeError("bad input")
except RuntimeError:
logger.exception("captured runtime failure")
# Drop INFO/DEBUG at the proxy before they hit any handlers
logger.setLevel("ERROR")
Each call returns a ProcessResult describing the outcome (ok plus event_id, queued=True, or ok=False with a reason such as DropReason.RATE_LIMITED). Standard logging APIs return None; this diagnostic payload is the deliberate divergence we keep for caller observability.
The public .log(...) helper normalises level inputs from strings ("warning"), integers (logging.WARNING), or the domain enum so advanced callers can apply dynamic thresholds without reimplementing conversions. Format strings are interpolated inside the process pipeline, so %-style placeholders behave exactly like logging.Logger. When exc_info or stack_info are provided they are stored on the event as formatted text, compacted according to the configured payload limits (see Exceptions logging). The stacklevel keyword is accepted for drop-in compatibility but currently ignored. .exception(...) is a thin convenience wrapper over .error(...) that sets level=LogLevel.ERROR and defaults exc_info to True. .setLevel(...) gates events at the proxy, leaving console/backend/Graylog thresholds untouched so both conditions must be satisfied for emission.
The optional extra mapping is copied into the structured event and travels end-to-end: it is scrubbed, persisted in the ring buffer, and forwarded to every adapter (Rich console, journald, Windows Event Log, Graylog, dump exporters). Use it to attach contextual fields such as port numbers, tenant IDs, or feature flags. journald names each field after its key, uppercased and mapped to the characters journald accepts: A-Z, 0-9 and _, at most 64 long, not starting with a digit or _ (order-id becomes ORDER_ID, _x becomes EXTRA__X); a name that clashes with a reserved or already-set field gets the EXTRA_ prefix.
Need a quick preview of console colours? Call:
import lib_log_rich as log
result = log.logdemo(theme=log.ConsoleTheme.NEON, dump_format=log.DumpFormat.JSON)
print(result.events) # one ProcessResult per level
print(result.dump) # rendered dump string (or None when not requested)
print(result.backends) # BackendStatus(graylog=False, journald=False, eventlog=False)
The helper initialises a throwaway runtime, emits one message per level using the selected theme, optionally renders a text/JSON/HTML dump via the dump_format argument, and then shuts itself down. Themes are the ConsoleTheme members CLASSIC, DARK, NEON, and PASTEL, described in CONSOLESTYLES.md. ConsoleTheme, DumpFormat, FormatPreset and GraylogProtocol are importable from lib_log_rich itself as well as from lib_log_rich.domain. logdemo() and dump() take theme and the preset parameters as enum members only; a plain string such as "neon" raises TypeError. console_styles overrides individual level styles on top of a theme.
The optional backend flags (enable_graylog, enable_journald, enable_eventlog) let you route the demo events to real adapters during manual testing - the result exposes the chosen targets via result.backends.
Runtime configuration
RuntimeConfig holds every runtime setting (keyword arguments only; an unknown keyword is refused); pass it to lib_log_rich.init(config). Environment variables in the last column override the matching field.
| Parameter | Type | Default | Expected values | Purpose | Environment variable |
|---|---|---|---|---|---|
service | str | (required) | Non-empty identifier such as checkout, worker, billing. | Logical service name recorded in each event and used by adapters. | LOG_SERVICE |
environment | str | (required) | Deployment label (dev, stage, prod, local, ...) | Deployment environment recorded in each event and used by adapters. | LOG_ENVIRONMENT |
console_level | str | LogLevel | LogLevel.INFO | Case-insensitive debug, info, warning, error, critical or a LogLevel enum. | Lowest level emitted to the Rich console adapter. Independent of backend/Graylog levels. | LOG_CONSOLE_LEVEL |
backend_level | str | LogLevel | LogLevel.WARNING | Same set as console_level. | Threshold shared by journald and Windows Event Log adapters. Independent of console/Graylog levels. | LOG_BACKEND_LEVEL |
graylog_endpoint | tuple[str, int] | None | None | (host, port) tuple (port > 0); only LOG_GRAYLOG_ENDPOINT takes a HOST:PORT string. | Host/port for GELF; combine with enable_graylog=True. | LOG_GRAYLOG_ENDPOINT (host:port) |
graylog_protocol | GraylogProtocol | GraylogProtocol.TCP | GraylogProtocol.TCP or GraylogProtocol.UDP; the value strings tcp/udp parse in any case. | Transport to reach Graylog. | LOG_GRAYLOG_PROTOCOL |
graylog_tls | bool | False | True only with GraylogProtocol.TCP. | Wrap the Graylog connection in TLS. | LOG_GRAYLOG_TLS |
graylog_level | str | LogLevel | LogLevel.WARNING | Same set as console_level. | Severity threshold for Graylog fan-out. Independent of console/backend levels. Use get_minimum_log_level() to find the lowest among all three. | LOG_GRAYLOG_LEVEL |
enable_ring_buffer | bool | True | True/False. | Toggles the in-memory ring buffer (falls back to 1024 events when disabled). | LOG_RING_BUFFER_ENABLED |
ring_buffer_size | int | 25_000 | Integer > 0 (events). | Max events retained in the ring buffer. | LOG_RING_BUFFER_SIZE |
ring_buffer_checkpoint | Path | None | None | A file path; an empty string means none; an existing directory is refused at init(). | JSONL file the ring buffer is loaded from at init() and appended to by flush(flush_ring_buffer=True), shutdown() and the flush after a dump. It only grows: rotate it yourself. A corrupt line makes init() raise ValueError. | LOG_RING_BUFFER_CHECKPOINT |
enable_journald | bool | False | True/False; effective on Linux/systemd only. | Adds the journald adapter. | LOG_ENABLE_JOURNALD |
enable_eventlog | bool | False | True/False; effective on Windows only. | Adds the Windows Event Log adapter. | LOG_ENABLE_EVENTLOG |
enable_graylog | bool | False | True/False; True without an endpoint is refused. | Enables the Graylog adapter. | LOG_ENABLE_GRAYLOG |
queue_enabled | bool | True | True/False. | Routes events through the background queue worker. | LOG_QUEUE_ENABLED |
queue_maxsize | int | 2048 | Integer > 0 (slots). | Maximum pending events before the full-policy applies. | LOG_QUEUE_MAXSIZE |
queue_full_policy | QueuePolicy | QueuePolicy.BLOCK | QueuePolicy.BLOCK or QueuePolicy.DROP; the value strings block/drop parse. | Choose whether producers wait or drop when the queue is full. | LOG_QUEUE_FULL_POLICY |
queue_put_timeout | float | None | 1.0 | Seconds > 0, or None/<=0 for indefinite wait. | Timeout applied to blocking queue puts before falling back to the caller. | LOG_QUEUE_PUT_TIMEOUT |
queue_stop_timeout | float | None | 5.0 | Seconds > 0, or None/<=0 to wait forever. | Deadline for draining the queue during shutdown(). | LOG_QUEUE_STOP_TIMEOUT |
force_color | bool | False | True/False. | Forces Rich colour output even when stderr is not a TTY. | LOG_FORCE_COLOR |
no_color | bool | False | True/False. | Disables colour output regardless of terminal support. | LOG_NO_COLOR |
console_styles | Mapping[str | LogLevel, str] | None | None | Mapping keyed by DEBUG, INFO, WARNING, ERROR, CRITICAL (or LogLevel members) with Rich style strings. | Rich style overrides per level. | LOG_CONSOLE_STYLES (comma-separated LEVEL=style) |
console_theme | ConsoleTheme | None | ConsoleTheme.DARK | ConsoleTheme.CLASSIC, DARK, NEON or PASTEL; value strings parse in any case, a blank string is None, and an unknown name is refused when RuntimeConfig is built. | Palette applied to the console (and inherited by dumps without overrides). | LOG_CONSOLE_THEME |
console_format_preset | FormatPreset | None | platform-specific | FormatPreset.FULL, SHORT, FULL_LOC, SHORT_LOC, SHORT_LOC_ICON; value strings parse in any case. None means SHORT_LOC_ICON on Windows, SHORT_LOC on Linux/Mac. | Console line layout when no custom template is supplied. | LOG_CONSOLE_FORMAT_PRESET |
console_format_template | str | None | None | Python str.format template using fields listed in Template fields. | Custom console template overriding the preset. | LOG_CONSOLE_FORMAT_TEMPLATE |
console_stream | ConsoleStream | ConsoleStream.STDERR | ConsoleStream.STDOUT, STDERR, BOTH, CUSTOM or NONE (value strings parse): stdout, stderr, both, a custom stream, or mute entirely. | Default mirrors stdlib logging (stderr); tweak to match host expectations. | LOG_CONSOLE_STREAM |
console_stream_target | object | None | None | File-like object implementing write(); required with ConsoleStream.CUSTOM. | Inject in-memory buffers or other sinks without a custom adapter factory. | (none) |
console_adapter_factory | Callable[[ConsoleAppearance], ConsolePort] | None | None | Callable returning a custom ConsolePort adapter. | Plug-in point for queue-backed or test adapters; see STREAMINGCONSOLE.md. | (none) |
dump_format_preset | FormatPreset | None | None (FormatPreset.FULL) | FormatPreset.FULL, SHORT, FULL_LOC, SHORT_LOC; the console-only SHORT_LOC_ICON is refused at init(). | Default text dump layout used when callers omit one. | LOG_DUMP_FORMAT_PRESET |
dump_format_template | str | None | None | Python str.format template using Template fields. | Default text dump template overriding the preset. | LOG_DUMP_FORMAT_TEMPLATE |
scrub_patterns | dict[str, str] | None | {"password": ".+", "secret": ".+", "token": ".+"} | Mapping of field=regex pairs merged with built-in scrub rules. | Removes sensitive values before fan-out. | LOG_SCRUB_PATTERNS |
rate_limit | tuple[int, float] | None | None | (max_events, window_seconds) with both positives; env string MAX:WINDOW_SECONDS. | Throttles emissions before fan-out. | LOG_RATE_LIMIT |
payload_limits | PayloadLimits | None | See below | A PayloadLimits, or a mapping of its fields (message_max_chars, extra_max_keys, etc.) validated into one when RuntimeConfig is built, so an invalid limit is refused there. | Guards message/extra size; see Payload Limits. | (none) |
diagnostic_hook | Callable[[str, Mapping[str, object]], None] | None | None | Callable receiving (event_name, diagnostics) or None. | Observes queue drops, rate limiting, payload clamps, etc. | (none) |
config.enable_dotenv() helper | (call before init()) | (opt-in) | Optional search_from, markers, dotenv_override; see DOTENV.md. | Loads .env files while preserving env precedence. | LOG_USE_DOTENV (CLI/module entry points) |
Console template fields
console_format_template and dump_format_template accept the same Python str.format placeholders. Common fields include:
| Placeholder | Description |
|---|---|
timestamp | ISO 8601 UTC timestamp with microseconds. |
timestamp_trimmed_naive | UTC timestamp trimmed to whole seconds without timezone information. |
timestamp_loc | Host-local ISO 8601 timestamp with offset. |
LEVEL | Upper-case level name (DEBUG, INFO, WARNING, ERROR, CRITICAL). |
level_icon | Rich glyph representing the level (bug, info, warning, cross, skull). |
level_code | Four-character abbreviation (DEBG, INFO, WARN, ERRO, CRIT). |
logger_name | Name passed to getLogger(...) when the event was emitted. |
message | Log message string. |
context_fields | Space-prefixed key=value pairs merged from LogContext and extra. |
user_name, hostname, process_id | System identity fields captured during initialisation. |
process_id_chain | Parent/child process lineage formatted as pid1>pid2>pid3. |
extra | Shallow copy of the extra mapping supplied to the logger. |
See LOGDUMP.md for the exhaustive placeholder list (including individual date/time components and dotted aliases).
See
STREAMINGCONSOLE.mdfor advanced queue-backed console wiring examples that build onconsole_adapter_factory.
Payload Limits
These guards exist to keep a single buggy or malicious caller from flooding the ring buffer, queue, or downstream sinks with giant payloads. The defaults clamp events to dimensions that safely fit journald, GELF, and log-shipper expectations while still leaving room for rich context.
Pass a PayloadLimits instance (from lib_log_rich.runtime) as payload_limits, for example:
import lib_log_rich as log
from lib_log_rich.runtime import PayloadLimits
config = log.RuntimeConfig(
service="svc",
environment="prod",
payload_limits=PayloadLimits(message_max_chars=2048, extra_max_keys=10),
)
log.init(config)
RuntimeConfig also accepts a mapping of the same fields (from a config file, say) and validates it into a PayloadLimits when it is built, so an invalid limit, or a key that is not one of these fields, is refused there with a ValidationError.
Default limits guard the pipeline and can be tuned per environment. Every field is optional; unspecified values fall back to the defaults below.
PayloadLimits fields
truncate_message(bool, defaultTrue) - whenTruelong messages are truncated tomessage_max_chars; whenFalseoversized messages raiseValueError.message_max_chars(int, default4096) - maximum characters for the primary log message.extra_max_keys(int, default25) - maximum number of keys accepted in theextramapping attached to the event. Additional keys are dropped with a diagnostic hook notice.extra_max_value_chars(int, default512) - per-key character cap after values are stringified; excess content is truncated with a…[truncated]suffix.exc_infoandstack_infotext is capped at the same length but keeps its end instead, starting with[truncated]…(see Exceptions logging).extra_max_depth(int, default3) - nesting depth allowed before nested structures are stringified.extra_max_total_bytes(int | None, default8192) - total UTF-8 encoded size allowed for the sanitizedextrapayload. Set toNoneto disable the aggregate clamp.context_max_keys(int, default20) - maximum keys stored inLogContext.extra.context_max_value_chars(int, default256) - per-value limit for context metadata once stringified.stacktrace_max_frames(int, default10) - number of leading and trailing frames preserved inexc_infoandstack_infotext once it holds more than twice that many. A frame is aFile "...", line N, in fline plus the indented lines under it; the frames between are removed, each run of them replaced with one... truncated N frame(s) ...line, and the result is then subject toextra_max_value_chars.
Whenever a limit is enforced, the optional diagnostic_hook receives an event (for example message_truncated, extra_keys_dropped, exc_info_truncated) so operators can monitor clamping in production. Queue resilience signals use the same channel: queue_worker_error fires when the background worker raises (and the adapter flips its worker_failed flag for health checks, clearing automatically after the cooldown or a clean restart), and queue_drop_callback_error captures drop-handler failures without tearing the worker down.
Graylog fan-out uses the configured graylog_level (default WARNING when enabled, automatically tightened to CRITICAL when Graylog is disabled). Text dumps do not inherit the console preset or template: they use dump_format_preset / dump_format_template (default FormatPreset.FULL) unless a dump() call passes its own.
The initializer also honours LOG_BACKEND_LEVEL, LOG_FORCE_COLOR, and LOG_NO_COLOR simultaneously: a set, valid environment variable wins over the matching RuntimeConfig field. When enable_journald is requested on Windows hosts or enable_eventlog on non-Windows hosts the runtime silently disables those adapters so cross-platform deployments never fail during initialisation.
Note: TLS is only supported with the TCP transport. With Graylog enabled, combining
GraylogProtocol.UDPwith TLS (or settingLOG_GRAYLOG_PROTOCOL=udpalongsideLOG_GRAYLOG_TLS=1) raises aValueErrorduring initialisation.
Environment-only overrides
Set these, restart your process, and the runtime will merge them with the RuntimeConfig you pass to init(...).
| Variable | Default | Expected values | Effect |
|---|---|---|---|
LOG_SERVICE | RuntimeConfig.service | Non-empty string (e.g., checkout). | Override the advertised service name. |
LOG_ENVIRONMENT | RuntimeConfig.environment | Non-empty string (e.g., dev, stage, prod). | Override the deployment/stage label. |
LOG_CONSOLE_LEVEL | info | debug, info, warning, error, critical (case-insensitive). | Minimum level emitted to the console adapter. |
LOG_BACKEND_LEVEL | warning | Same as LOG_CONSOLE_LEVEL. | Threshold for journald/Event Log adapters. |
LOG_GRAYLOG_LEVEL | warning | Same as LOG_CONSOLE_LEVEL. | Threshold for Graylog emission. |
LOG_RING_BUFFER_ENABLED | true | Boolean toggle (1/true/yes/on or 0/false/no/off). | Disable to skip ring-buffer retention. |
LOG_RING_BUFFER_SIZE | 25000 | Integer > 0 (events). | Resize the in-memory ring buffer. |
LOG_RING_BUFFER_CHECKPOINT | unset | A file path; empty means unset. | Persist the ring buffer to this JSONL file (beats ring_buffer_checkpoint). |
LOG_ENABLE_JOURNALD | false | Boolean toggle (ignored on Windows). | Enable/disable the journald adapter. |
LOG_ENABLE_EVENTLOG | false | Boolean toggle (ignored on non-Windows). | Enable/disable the Windows Event Log adapter. |
LOG_ENABLE_GRAYLOG | false | Boolean toggle; requires LOG_GRAYLOG_ENDPOINT. | Enable the Graylog adapter. |
LOG_GRAYLOG_ENDPOINT | none | HOST:PORT string with port > 0. | Target host and port for GELF. |
LOG_GRAYLOG_PROTOCOL | tcp | tcp or udp; anything else is refused at init(). | Choose Graylog transport. |
LOG_GRAYLOG_TLS | false | Boolean toggle; only valid with tcp. | Wrap the Graylog connection in TLS. |
LOG_QUEUE_ENABLED | true | Boolean toggle. | Disable to process fan-out inline without a queue. |
LOG_QUEUE_MAXSIZE | 2048 | Integer > 0 (slots). | Queue capacity before the full-policy applies. |
LOG_QUEUE_FULL_POLICY | block | block or drop; an unknown value falls back to the configured policy. | Decide whether producers wait or drop when the queue is full. |
LOG_QUEUE_PUT_TIMEOUT | 1.0 | Seconds (float). <=0 clears the timeout. | Timeout for blocking puts before the caller handles overflow. |
LOG_QUEUE_STOP_TIMEOUT | 5.0 | Seconds (float). <=0 waits indefinitely. | Drain deadline during shutdown. |
LOG_FORCE_COLOR | false | Boolean toggle. | Force ANSI colour even when stderr is not a TTY. |
LOG_NO_COLOR | false | Boolean toggle. | Strip colour output entirely. |
LOG_CONSOLE_THEME | dark | classic, dark, neon, pastel (case-insensitive); an unknown name is refused at init(), an empty value keeps the configured theme. | Apply a palette to the console/dumps. |
LOG_CONSOLE_STYLES | none | Comma-separated LEVEL=style pairs (e.g., INFO=green). | Override Rich styles per level. |
LOG_CONSOLE_FORMAT_PRESET | platform-specific | full, short, full_loc, short_loc, short_loc_icon; an unknown name is refused at init(). Default: short_loc_icon on Windows, short_loc on Linux/Mac. | Default preset for console lines. |
LOG_CONSOLE_FORMAT_TEMPLATE | none | str.format template using Template fields. | Override the preset with a custom layout. |
LOG_CONSOLE_STREAM | stderr | stdout, stderr, both, custom, none; anything else is refused at init(). | Redirect console output; custom requires console_stream_target, none mutes the console. |
LOG_DUMP_FORMAT_PRESET | full | full, short, full_loc, short_loc; an unknown name, or the console-only short_loc_icon, is refused at init(). | Default preset when dumping with dump_format="text". |
LOG_DUMP_FORMAT_TEMPLATE | none | str.format template using Template fields. | Custom text-dump template. |
LOG_SCRUB_PATTERNS | password=.+,secret=.+,token=.+ | Comma-separated field=regex pairs. | Merge additional scrub patterns with defaults. |
LOG_RATE_LIMIT | none | MAX:WINDOW_SECONDS with positive numbers (e.g., 500:60). | Rate limit emissions before fan-out. |
LOG_USE_DOTENV | false | Boolean toggle. | Allow the CLI/module entry point to load a nearby .env. |
lib_log_rich stresstest also pre-fills its form from LOG_DUMP_FORMAT and LOG_DUMP_LEVEL; the runtime itself ignores them.
Queue safety defaults
See also QUEUE.md for a complete guide to queue policies and diagnostics.
A blocking put waits up to one second by default (queue_put_timeout=1.0); None, or LOG_QUEUE_PUT_TIMEOUT set to <=0, waits indefinitely. When the timeout is positive and the worker remains in a failed state, blocking puts fall back to drop mode once the timeout elapses and the runtime emits a queue_degraded_drop_mode diagnostic. After recovery (cooldown expiry, clean stop(drain=True), or a fresh start()), the adapter restores the configured policy. Tune queue_put_timeout or LOG_QUEUE_PUT_TIMEOUT if you need bounded waits.
Boolean variables accept 1, true, yes, on as true and 0, false, no, off as false (case-insensitive); an empty or unrecognised value keeps the configured value.
Operations & Observability
Log dump
log.dump(...) bridges the in-memory ring buffer to structured exports. See LOGDUMP.md for parameter tables, text placeholder references, and usage notes covering text/JSON/HTML dumps.
JSON dumps expose enriched metadata (level_name, numeric level_value, the four-character level_code, and the console level_icon) plus a normalised process_id_chain.
When you need to isolate specific events, provide mapping-based filters such as context_filters={"job_id": "batch-42"} or extra_filters={"request": {"icontains": "api"}}. Entries accept exact values, substring predicates (contains/icontains), or regex dictionaries ({"pattern": r"^prefix", "regex": True}), and multiple keys combine with logical AND while repeated keys OR together.
Inspecting severity and drop metrics
The runtime keeps a thread-safe severity monitor so you can make dump decisions without scanning the ring buffer.
import lib_log_rich as log
from lib_log_rich.domain import DropReason, LogLevel
log.init(log.RuntimeConfig(service="svc", environment="metrics", queue_enabled=False))
with log.bind(job_id="run-42"):
log.getLogger("svc.worker").info("started")
log.getLogger("svc.worker").error("boom")
snapshot = log.severity_snapshot()
assert snapshot.highest is LogLevel.ERROR
assert snapshot.total_events == 2
assert snapshot.dropped_total == 0
assert snapshot.drops_by_reason[DropReason.RATE_LIMITED] == 0 # counters are pre-seeded
warning_or_higher = snapshot.thresholds[LogLevel.WARNING]
assert warning_or_higher == 1
# Drops caused by rate limiting, queue saturation, or adapter failures
# surface by reason and severity:
rate_limited_errors = snapshot.drops_by_reason.get(DropReason.RATE_LIMITED, 0)
log.reset_severity_metrics() # start a fresh window without reinitialising the runtime
if log.max_level_seen() is None:
print("No high-severity events since the reset")
log.shutdown()
drops_by_reason is pre-seeded with the DropReason members RATE_LIMITED, QUEUE_FULL and ADAPTER_ERROR (from lib_log_rich.domain), and a QUEUE_FAILURE drop (the queue refused the event) adds that key. ProcessResult.reason can also be LOGGER_LEVEL (below the proxy's setLevel threshold); those events never enter the pipeline and are not counted. Use drops_by_reason_and_level when you need per-severity breakdowns for dashboards or alerts.
Threshold buckets complement the per-level counts: each threshold represents an
"at least" view (for example the built-in LogLevel.WARNING bucket includes
WARNING, ERROR, and CRITICAL events). The runtime seeds WARNING and ERROR by
default so operators can monitor actionable activity without summing individual
levels. When wiring a monitor manually (e.g., for custom tooling) you can pass
additional LogLevel values to SeverityMonitor(thresholds=...) to track
different cut-offs.
-
By default the
SeverityMonitortracks two cumulative thresholds:LogLevel.WARNING: counts every event logged at WARNING, ERROR, or CRITICAL.LogLevel.ERROR: counts every event logged at ERROR or CRITICAL.
Those buckets are included automatically so you can see "actionable" volume without summing individual levels. If you need different cut-offs (e.g., include INFO or add a CRITICAL-only bucket) you can pass your own iterable when constructing the monitor -
SeverityMonitor(thresholds=[LogLevel.INFO, LogLevel.ERROR])- and the snapshot will expose exactly those you request.
Terminal compatibility
Rich automatically detects whether the target is 16-colour, 256-colour, or truecolor, and adjusts the style to the nearest supported palette. For truly minimal environments (plain logs, CI artefacts), set no_color=True (or LOG_NO_COLOR=1) and Rich suppresses ANSI escapes entirely. Conversely, force_color=True (or LOG_FORCE_COLOR=1) forces colouring even if stderr isn't a tty (useful in some container setups).
Console & Terminal Experience
Customising per-level colours
Override the default Rich styles by passing a dictionary as RuntimeConfig(console_styles=...) or by exporting LOG_CONSOLE_STYLES as a comma-separated list, for example:
export LOG_CONSOLE_STYLES="DEBUG=dim,INFO=bright_green,WARNING=bold yellow,ERROR=bold white on red,CRITICAL=bold magenta"
Values use Rich's style grammar (named colours, modifiers like bold/dim, or hex RGB). Omitted keys fall back to the built-in theme. logdemo cycles through all preset x theme combinations (5 presets x 4 themes = 20 examples by default) so you can preview styles before committing to overrides. Use --preset and --theme options to filter specific combinations.
CLI and Automation
CLI entry point
lib_log_rich ships with a rich-click interface for quick diagnostics, demos, and automation. See CLI.md for the full command breakdown, option tables, and usage examples. Quick highlight: run python -m lib_log_rich for the metadata banner, use lib_log_rich logdemo to preview console themes and generate text/JSON/HTML dumps (with optional Graylog, journald, or Event Log fan-out), or launch lib_log_rich stresstest to drive a Textual TUI that stress-tests the runtime while streaming live diagnostics. (The stress tester requires Textual; install dev extras with pip install -e .[dev] before running it.)
Filtering options such as --context-exact job_id=batch and --extra-regex request=^api flow through logdemo so CLI dumps can focus on specific workloads without post-processing. Regex-based filters validate patterns eagerly and raise a friendly click.BadParameter when a pattern is invalid (exit code 2), so a typo never surfaces as a raw re.error traceback.
Streaming console output to other consumers
RuntimeConfig.console_adapter_factory lets you swap the Rich console for a custom adapter without touching internals. Ship a queue-backed adapter (see lib_log_rich.adapters.console.QueueConsoleAdapter) to feed GUIs, SSE/WebSocket endpoints, or tests. Each adapter honours the same format presets/templates and level filtering as the Rich console and can emit ANSI strings or HTML snippets. A detailed walk-through with threaded, asyncio, and composite patterns lives in STREAMINGCONSOLE.md.
A minimal Flask example lives in examples/flask_console_stream.py; it pushes rendered HTML lines to an async EventSource stream while regular CLI/TUI usage keeps the Textual stress tester responsive - no monkey patching required.
Quick smoke-test helpers ship with the package:
import lib_log_rich as log
log.hello_world()
try:
log.i_should_fail()
except RuntimeError as exc:
print(exc)
Claude Code skill
This repo is itself a Claude Code plugin marketplace and ships a skill (skills/python-logging/) that teaches a coding agent to install, configure, deploy and operate lib_log_rich: RuntimeConfig and its LOG_* environment overrides, the console, journald, Windows Event Log and Graylog sinks, systemd, container and Windows service deployment, secret scrubbing, the stdlib bridge, queues and subprocesses, validate_config, ring-buffer dumps and the CLI. Install it in any project:
/plugin marketplace add bitranox/lib_log_rich
/plugin install lib_log_rich
The same skill is also available in the central bitranox-skills marketplace as coding-python-logging.
Further documentation
- skills/python-logging/SKILL.md - the agent guide: usage, configuration and deployment in one place.
- docs/systemdesign/concept.md - product concept and goals.
- docs/systemdesign/concept_architecture.md - layered architecture guide.
- docs/systemdesign/concept_architecture_plan.md - TDD implementation roadmap.
- docs/systemdesign/module_reference.md - authoritative design reference.
- INSTALL.md - detailed installation paths.
- README.md - quick overview and parameters.
- CLI.md - command reference, options, and CLI usage examples.
- LOGDUMP.md - dump API parameters, placeholders, and usage guidance.
- CONSOLESTYLES.md - palette syntax, themes, and overrides.
- STREAMINGCONSOLE.md - queue-backed console adapters and
console_adapter_factorypatterns. - DOTENV.md - opt-in
.envloading flow, CLI flags, and precedence rules. - INSTALL_JOURNAL.md - journald-specific installation checks, socket permissions, and smoke tests.
- SUBPROCESSES.md - multi-process logging guidance.
- EXAMPLES.md - runnable snippets from Hello World to multi-backend wiring.
- DEVELOPMENT.md - contributor workflow.
- CONTRIBUTING.md - contribution expectations, coding standards, and review process.
- CHANGELOG.md - release history and noteworthy changes.
- DIAGNOSTIC.md - diagnostic hook semantics, event catalogue, and instrumentation patterns.
Development
Contributor workflows, make targets, CI automation, and release guidance are documented in DEVELOPMENT.md.