Source code for log_redaction

"""Logging filter that redacts API keys in log output.

Attaches to the root logger so all loggers (including httpx/httpcore)
have keys partially censored: first 4 chars + ``...`` + last 4 chars.
"""

from __future__ import annotations

import logging
import re

_KEY_RE = re.compile(r"(key=)([A-Za-z0-9_-]{12,})")


def _redact_match(m: re.Match) -> str:
    """Censor a single ``key=<value>`` regex match captured by ``_KEY_RE``.

    Rebuilds the matched substring keeping the literal ``key=`` prefix
    (capture group 1) but replacing the secret value (capture group 2) with
    its first four characters, a ``...`` ellipsis, and its last four
    characters, so the value is recognizable for correlation while never being
    fully exposed in a log line. Pure string transformation with no side
    effects, network, or I/O.

    This is passed as the replacement callable to ``_KEY_RE.sub`` inside
    :func:`redact_api_keys`; it has no other internal callers and is not part
    of the public API.

    Args:
        m: A regex match produced by ``_KEY_RE`` where group 1 is the ``key=``
            prefix and group 2 is the 12+ character secret value.

    Returns:
        str: The reconstructed ``key=`` text with the value partially masked
        (e.g. ``key=abcd...wxyz``).
    """
    prefix, value = m.group(1), m.group(2)
    return f"{prefix}{value[:4]}...{value[-4:]}"


[docs] def redact_api_keys(text: str) -> str: """Replace API key values in *text* with a partially-masked form. Scans the input for ``key=<secret>`` patterns via ``_KEY_RE`` and rewrites each one through :func:`_redact_match` so the secret survives in logs only as its first four and last four characters joined by an ellipsis. This keeps log lines correlatable without ever exposing a full credential. Pure string transformation with no I/O or side effects. Called by :meth:`ApiKeyRedactionFilter.filter` for every log record routed through the handler the filter is attached to, and exercised directly by ``tests/core/test_enhanced_logging.py``. Args: text: The fully-formatted log message that may embed one or more ``key=`` secrets. Returns: str: The same text with every matched key value partially masked; the original string is returned unchanged when no key pattern is present. """ return _KEY_RE.sub(_redact_match, text)
[docs] class ApiKeyRedactionFilter(logging.Filter): """Logging filter that censors API keys before they reach the handler. A :class:`logging.Filter` subclass that sanitizes rather than suppresses: every record passing through is allowed onward, but any embedded ``key=`` secret is partially masked first via :func:`redact_api_keys`. Installing it on the root/console handler means downstream-library noise (httpx, httpcore) is scrubbed alongside the bot's own log lines. Instantiated and attached to a handler by ``core/log_config.py`` (which calls ``addFilter`` on the console handler), and asserted to be present by ``tests/core/test_enhanced_logging.py``. The actual per-record work lives in :meth:`filter`. """
[docs] def filter(self, record: logging.LogRecord) -> bool: """Redact API keys in a log record in place before it is emitted. Implements the :class:`logging.Filter` contract. Renders the record's fully-formatted message via ``record.getMessage()`` (which interpolates ``record.args``), passes it through :func:`redact_api_keys`, and — only when redaction actually changed the text — overwrites ``record.msg`` with the censored string and clears ``record.args`` to ``None`` so the handler does not attempt a second ``%``-format pass against the already-substituted message. The record is always allowed through (returns ``True``); this filter sanitizes rather than suppresses. Registered on a handler (typically the root/console handler) by ``core/log_config.py``, which constructs an :class:`ApiKeyRedactionFilter` and calls ``ch.addFilter(...)``; the standard logging machinery invokes this method for every record routed through that handler. It is also asserted to be present by ``tests/core/test_enhanced_logging.py``. Args: record: The log record about to be handled; ``record.msg`` and ``record.args`` may be mutated in place when a key is found. Returns: bool: Always ``True`` so the (possibly redacted) record continues to the handler. """ original = record.getMessage() redacted = redact_api_keys(original) if redacted != original: record.msg = redacted record.args = None return True