Coverage for /usr/local/lib/python3.10/site-packages/opal_common-0.0.0-py3.10.egg/opal_common/logging_utils/scrubbing.py: 93%
12 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 11:54 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 11:54 +0000
1from opal_common.http_utils import redact_url_in_text
4class CredentialScrubbingStream:
5 """A writable-stream wrapper that scrubs embedded URL credentials from
6 every fully-formatted log record before it reaches the underlying stream.
8 This is the catch-all that the model-layer redaction (``RedactedReprMixin``)
9 cannot provide: third-party exceptions - most notably aiohttp
10 ``ClientResponseError`` - render the full request URL
11 (``https://user:pw@host/path?token=...``) inside the traceback that loguru
12 formats into the ``{exception}`` slot. The leaking object is not an OPAL
13 model, so masking ``repr()`` does nothing for it.
15 Because loguru hands a stream sink the *final* formatted text (message +
16 traceback, and the JSON blob when ``serialize=True``), scrubbing on
17 ``write`` covers every record shape on this sink. ``redact_url_in_text`` is
18 idempotent, so the explicit call-site redaction upstream is unaffected.
20 Note: this wraps a *stream* sink. loguru's path-based file sink (with
21 rotation/retention) opens the file itself and cannot be wrapped this way -
22 message-level scrubbing for that sink is handled in ``Formatter.format``.
23 """
25 def __init__(self, stream):
26 self._stream = stream
28 def write(self, message: str):
29 return self._stream.write(redact_url_in_text(message))
31 def flush(self):
32 # loguru flushes the sink after each record when possible.
33 flush = getattr(self._stream, "flush", None)
34 if callable(flush): 34 ↛ exitline 34 didn't return from function 'flush' because the condition on line 34 was always true
35 flush()
37 def __getattr__(self, name):
38 # Delegate everything else (isatty/fileno/encoding/...) to the wrapped
39 # stream so loguru treats this exactly like the underlying stream.
40 return getattr(self._stream, name)