lib-log-rich 6.3.3
Rich-powered logging runtime with contextual metadata, multi-sink fan-out, and ring-buffer dumps
License: MIT
Requires Python: >=3.10
Keywords: ansi, color, colored, coulored, dump, eventlog, journal, journald, logdump, logging, rich, terminal

# lib_log_rich

<!-- Badges -->
[![CI](https://github.com/bitranox/lib_log_rich/actions/workflows/default_cicd_public.yml/badge.svg)](https://github.com/bitranox/lib_log_rich/actions/workflows/default_cicd_public.yml)
[![CodeQL](https://github.com/bitranox/lib_log_rich/actions/workflows/codeql.yml/badge.svg)](https://github.com/bitranox/lib_log_rich/actions/workflows/codeql.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Open in Codespaces](https://img.shields.io/badge/Codespaces-Open-blue?logo=github&logoColor=white&style=flat-square)](https://codespaces.new/bitranox/lib_log_rich?quickstart=1)
[![PyPI](https://img.shields.io/pypi/v/lib_log_rich.svg)](https://pypi.org/project/lib_log_rich/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/lib_log_rich.svg)](https://pypi.org/project/lib_log_rich/)
[![Code Style: Ruff](https://img.shields.io/badge/Code%20Style-Ruff-46A3FF?logo=ruff&labelColor=000)](https://docs.astral.sh/ruff/)
[![codecov](https://codecov.io/gh/bitranox/lib_log_rich/graph/badge.svg?token=UFBaUDIgRk)](https://codecov.io/gh/bitranox/lib_log_rich)
[![Maintainability](https://qlty.sh/badges/041ba2c1-37d6-40bb-85a0-ec5a8a0aca0c/maintainability.svg)](https://qlty.sh/gh/bitranox/projects/lib_log_rich)
[![Known Vulnerabilities](https://snyk.io/test/github/bitranox/lib_log_rich/badge.svg)](https://snyk.io/test/github/bitranox/lib_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**


```python
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](#integration-decision-guide) – Quick guidance on choosing LoggerProxy vs stdlib handler.
- [Getting Started](#getting-started)
  - [Overview](#overview) – High-level introduction, key features, and quick-start pointers.
  - [Installation](#installation) – Supported Python versions, installation commands, editable mode, and journald prerequisites.
    - [Journald adapter dependency](#journald-adapter-dependency) – Extra host prerequisites for systemd-journald emission.
  - [Logging with LoggerProxy](#logging-with-loggerproxy) – Minimal example covering configuration, logging, shutdown, and submodule patterns.
    - [How to use in submodules](#how-to-use-in-submodules) – Dependency-injection friendly usage.
    - [Domain helpers](#domain-helpers) – Reusable value objects and helper utilities.
    - [Custom pipeline wiring](#custom-pipeline-wiring) – Extending the runtime composition root.
    - [Context vs. per-event metadata](#context-vs-per-event-metadata) – Binding semantics and best practices.
    - [exceptions logging](#exceptions-logging) – Capturing tracebacks and stack information.
    - [Opt-in `.env` loading](#opt-in-env-loading) – Environment discovery and precedence.
- [Integration Options](#integration-options)
  - [Stdlib compatibility & deliberate differences](#stdlib-compatibility--deliberate-differences) – Behavioural notes compared to `logging`.
  - [Integrating stdlib logging](#integrating-stdlib-logging) – Step-by-step handler wiring and dump inspection.
- [Runtime Reference](#runtime-reference)
  - [Public API](#public-api) – Table summarising exported helpers (`init`, `getLogger`, `dump`, etc.).
  - [Runtime configuration](#runtime-configuration) – Runtime knobs, environment flags, and presets.
  - [Payload Limits](#payload-limits) – Message/extra/context sizing and truncation diagnostics.
  - [Environment-only overrides](#environment-only-overrides) – Override runtime settings without code changes.
- [Operations & Observability](#operations--observability)
  - [Log dump](#log-dump) – Dump formats, filtering options, and customisation templates.
  - [Inspecting severity and drop metrics](#inspecting-severity-and-drop-metrics) – Using `max_level_seen`, `severity_snapshot`, and stress-test counters.
  - [Terminal compatibility](#terminal-compatibility) – Behaviour across terminals and colour systems.
- [Console & Terminal Experience](#console--terminal-experience)
  - [Customising per-level colours](#customising-per-level-colours) – Theme overrides and presets.
- [CLI and Automation](#cli-and-automation)
  - [CLI entry point](#cli-entry-point) – Usage of `python -m lib_log_rich` and available subcommands.
    - [Streaming console output to other consumers](#streaming-console-output-to-other-consumers) – Queue-backed adapters for GUIs/async consumers.
    - [Quick smoke-test helpers ship with the package:](#quick-smoke-test-helpers-ship-with-the-package) – Self-test commands and diagnostics.
- [Further documentation](#further-documentation) – Links to CLI, dump, style, and architecture guides.
- [Development](#development) – Contribution guidelines, testing strategy, and project structure.
- [License](#license) – Project licensing information.

---

## Integration Decision Guide

- Prefer **`lib_log_rich.getLogger(...)` and `LoggerProxy`** when 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 **`StdlibLoggingHandler`** via `attach_std_logging()` when you must support existing `logging` hierarchies (framework defaults, third-party libraries, legacy modules). The handler normalises `LogRecord` objects into the same runtime, preserves `extra` payloads, copies call-site metadata (`pathname`, `lineno`, `funcName`), honours `exc_info`/`stack_info`, and skips recursion for events originating inside `lib_log_rich`.
- Mixing both approaches is common: initialise the runtime once, use `LoggerProxy` in 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

<a id="section-overview"></a>
### 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.
> Core dependencies: `lib_cli_exit_tools>=2.1.0`, `pydantic>=2.12.0`, `rich>=14.2.0`, `rich-click>=1.9.3`, and `python-dotenv>=1.1.1` (as mirrored in `pyproject.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`, and `graylog_level` are completely independent — each gates events to its respective adapter (console, journald/EventLog, Graylog) without affecting the others. Use `get_minimum_log_level()` to find the lowest threshold for stdlib integration.
- opt-in `.env` loading (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_hook` callback 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) — `QueueConsoleAdapter` for threads and `AsyncQueueConsoleAdapter` for asyncio — exposed via `console_adapter_factory`, handy for Textual panes, SSE/WebSocket streams, or tests.
- [EXAMPLES.md](EXAMPLES.md) — runnable snippets from Hello World to multi-backend wiring.

---


### Installation

For a quick start from PyPI:

```bash
pip install lib_log_rich
```

Detailed installation options (venv, pipx, uv, Poetry/PDM, and Git installs) live in [INSTALL.md](INSTALL.md). If you plan to enable systemd journald logging, follow the extra host prerequisites in [INSTALL_JOURNAL.md](INSTALL_JOURNAL.md).

#### Journald adapter dependency

The Journald adapter requires the `systemd-python` bindings. Install via the optional extra:

```bash
# 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.

When you initialise the runtime with `enable_journald=True` without the bindings, a `RuntimeError` is raised immediately so you can fix the dependency before emitting events. Once installed the adapter can emit to journald regardless of queue settings. See [INSTALL_JOURNAL.md](INSTALL_JOURNAL.md) for a deeper walkthrough covering Linux service managers and verification steps.

---

<a id="section-usage"></a>
### Logging with LoggerProxy

```python
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.

```python
# 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.charge_order("ord-42")
```

```python
# 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(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.

```python
# 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 as exc:
            # 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(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:

```python
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", extra={"tenant": "acme"}),
    extra={"tenant": "acme", "order_id": "ORD-7"},
)
should_emit = filters.matches(event)
```

- `LogLevel` keeps conversions idempotent (`from_name`, `from_python_level`, `to_python_level`), so threading a standard `logging.LogRecord` level 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 a `DumpFilter` you can reuse in unit tests, notebook exploration, or dump pipelines by invoking `matches(...)` or handing its field tuples to the runtime façade.
- `LoggerProxy.log(level, msg, *args, exc_info=None, stack_info=None, stacklevel=1, extra=None)` mirrors the stdlib `logging.Logger` signature while still normalising enum/string/integer levels. Messages are formatted lazily inside the process pipeline, `exc_info` can be `True`, an exception instance, or a full tuple, and optional `stack_info` strings are threaded through to every adapter. The `stacklevel` keyword is accepted for API parity and currently ignored.
- `LoggerProxy.exception(msg, *args, exc_info=True, stack_info=None, stacklevel=1, extra=None)` matches `logging.Logger.exception`: it logs at `LogLevel.ERROR`, defaults `exc_info` to `True` (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:

```python
from datetime import datetime, 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=1.0),
    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

    # 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}

---

#### Exceptions logging 

When logging exceptions, add the formatted traceback to `extra["exc_info"]`.
The runtime keeps only the top/bottom `stacktrace_max_frames` frames (default 10)::

    import traceback

    try:
        raise RuntimeError("upstream failed")
    except RuntimeError:
        logger.error(
            "job crashed",
            extra={"exc_info": traceback.format_exc()},
        )

Oversized traces are collapsed with `... truncated N frame(s) ...`, and the diagnostic hook
receives `exc_info_truncated`.

---

#### Opt-in `.env` loading

`lib_log_rich` has always honoured real environment variables over function arguments (`LOG_SERVICE`, `LOG_CONSOLE_LEVEL`, and friends). The `.env` helpers let you keep that precedence while sourcing defaults from a project-local file:

```python
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:

- `.env` loading is explicit – nothing is read unless you call `enable_dotenv()` (or `load_dotenv()`).
- Precedence stays intact: CLI flag ➝ real `os.environ` ➝ discovered `.env` ➝ defaults.
- Search uses `python-dotenv.find_dotenv(usecwd=True)` and stops once `.env` appears or the filesystem root is reached.
- Pass `dotenv_override=True` when you intentionally want `.env` values to win over real environment variables.

See [DOTENV.md](DOTENV.md) for more detail, examples, and CLI usage.

---

## 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(...)` (defaults `exc_info=True` just like the stdlib)
- `.log(level, ...)`
- `.setLevel(level)`

Key differences from the stdlib logging framework:

- Calls return structured dictionaries describing emission outcomes instead of `None`.
- No hierarchical logger tree: we do not expose the root logger, `getChild()`, propagation, or handler management. Each `getLogger(name)` call yields an independent proxy whose name is metadata only.
- Aliases such as `.warn(...)`, `.fatal(...)`, `isEnabledFor(...)`, `addHandler(...)`, and handler lists are intentionally unsupported.
- `stacklevel` is accepted but currently ignored. Stack trace capture is explicit: provide `exc_info` or `stack_info` to record traces, and the runtime stores sanitised text in the event payload rather than rendering the default stdlib traceback formatting.
- Console output defaults to stderr (matching the standard library logger), but you can steer it via `RuntimeConfig.console_stream` to `"stdout"`, `"stderr"`, `"both"`, `"custom"` (supply `console_stream_target`), or `"none"` to suppress console output entirely. You can also pass a no-op `console_adapter_factory` for bespoke mute behaviour. Without a console sink nothing touches stdio; stack traces continue to flow only 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 now ships with a bridge so those callers can continue to emit without refactoring.

```python
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 single `StdlibLoggingHandler` on the chosen logger (root by default). By default it sets the logger level to `get_minimum_log_level()` so events aren't pre-filtered before reaching lib_log_rich, and sets `propagate=False` to prevent duplicate emission. Pass `logger_level=None` to leave the level unchanged.
- Every `logging.LogRecord` is translated into the runtime pipeline: message/arguments flow through the same sanitiser as `LoggerProxy`, `extra` metadata travels unchanged, and `exc_info`/`stack_info` remain available to adapters.
- Location metadata (`pathname`, `lineno`, `funcName`, plus any custom `extra` fields) is copied into the event so dumps, Graylog, and Rich console output can display the original call site.
- Records originating from `lib_log_rich` are ignored automatically to avoid recursion; set `extra={"lib_log_rich_skip": True}` if you need to suppress specific third-party records.

#### 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.

```python
import logging
import lib_log_rich as log

# 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,
)
log.init(config)
log.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`, and `graylog_level` are 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. When Graylog is disabled, its level is ignored.
- **Automatic integration**: `attach_std_logging()` uses this by default; pass `logger_level=None` to leave the stdlib logger level unchanged.

---


## Runtime Reference

<a id="section-public-api"></a>
### 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.                                                                                                                                                                                                                                   |
| `bind`                     | `bind(**fields)` (context manager)                                                                                                                                                                                                                                  | Binds contextual metadata. Requires `service`, `environment`, and `job_id` when no parent context exists; nested scopes merge overrides. Yields the active `LogContext`.                                                                                                                                                                                                                                         |
| `getLogger`                | `getLogger(name: str) -> LoggerProxy`                                                                                                                                                                                                                               | Returns a `LoggerProxy` exposing `.debug/.info/.warning/.error/.critical/.exception`. Each call returns a dict (e.g. `{"ok": True, "event_id": "..."}` or `{ "ok": False, "reason": "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` payloads are preserved for adapters, 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 the diagnostic dict (unlike `logging.Logger`, which returns `None`). |
| `dump`                     | `dump(*, dump_format="text", path=None, level=None, console_format_preset=None, console_format_template=None, theme=None, console_styles=None, context_filters=None, context_extra_filters=None, extra_filters=None, color=False) -> str`                           | Serialises the ring buffer (text/json/html_table/html_txt). `level` filters events by severity, presets/templates customise text rendering (template wins), `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 (`console_level`, `backend_level`, and `graylog_level` 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. 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/stops the queue, and clears global state. Safe to call repeatedly after initialisation.                                                                                                                                                                                                                                                                                                 |
| `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: 5.0s). Set `flush_ring_buffer=True` to append buffer events to checkpoint file and clear the buffer (no-op if no checkpoint path configured; buffer 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.                                                                                                                                                                                                                                   |
| `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.