Coverage for src/local_deep_research/security/secure_logging.py: 100%
19 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
1"""Loguru wrapper whose ``.exception()`` gates tracebacks behind diagnose mode.
3Provider and search-engine exception handlers must stay observable at
4ERROR level, but plain ``loguru.logger.exception()`` always attaches the
5active exception to the record — every sink (stderr, database, frontend)
6then renders or persists the traceback, including the ``__cause__`` /
7``__context__`` chain, which has leaked Authorization headers and full
8URLs embedded in wrapped exceptions (#4183).
10:data:`logger` exported here is a :class:`SecureLogger` proxy around the
11real loguru logger. Its ``.exception()`` is equivalent to::
13 logger.opt(exception=is_diagnose_mode()).error(...)
15so in normal operation the record carries *no* exception object — no sink
16can render a traceback — while the event itself still lands at ERROR.
17Developers opt into full tracebacks by setting **both** ``LDR_APP_DEBUG``
18and ``LDR_LOGURU_DIAGNOSE`` to a truthy value ("1", "true", "yes").
20Usage — a drop-in replacement for the loguru import::
22 from ..security.secure_logging import logger
24 try:
25 ...
26 except Exception as e:
27 safe_msg = scrub_error(e) # from ..security.log_sanitizer
28 logger.exception(f"Engine request failed: {safe_msg}")
30Caveats:
32* **Messages are production-visible at ERROR.** The wrapper only strips
33 the traceback; the message string itself reaches every sink. Call sites
34 must still pass scrubbed text (``scrub_error`` — or in engine
35 subclasses ``self._scrub_error``), never raw ``str(e)``.
36* **Known bypass routes.** ``logger.opt(exception=True).error(...)`` and
37 ``@logger.catch`` delegate to plain loguru and are NOT gated. No call
38 site under ``llm/providers/``, ``embeddings/providers/`` or
39 ``web_search_engines/`` uses them today; the #4183 step-2 pre-commit
40 hook should detect them.
41* **Env-gated only.** ``config_logger()`` in ``utilities/log_utils.py``
42 gates its per-sink ``diagnose`` flag on its ``debug`` argument, which
43 may come from the ``app.debug`` DB setting — this wrapper reads only
44 the environment, so a DB-enabled debug mode does not enable wrapper
45 tracebacks. See the cross-reference comment in ``config_logger``.
46"""
48import os
50from loguru import logger as _loguru_logger
53def env_truthy(name: str) -> bool:
54 """Return True if env var *name* is set to a truthy value ("1", "true", "yes")."""
55 return os.environ.get(name, "").strip().lower() in ("1", "true", "yes")
58def is_diagnose_mode() -> bool:
59 """Return True when the operator explicitly opted into traceback output.
61 Requires BOTH ``LDR_APP_DEBUG`` and ``LDR_LOGURU_DIAGNOSE`` to be
62 truthy, mirroring the two-flag opt-in that ``config_logger()`` uses
63 for loguru's frame-locals ``diagnose`` rendering.
64 """
65 return env_truthy("LDR_APP_DEBUG") and env_truthy("LDR_LOGURU_DIAGNOSE")
68class SecureLogger:
69 """Thin proxy over the loguru logger with a diagnose-gated ``exception()``.
71 Everything except ``exception()``, ``bind()`` and ``patch()``
72 delegates straight to loguru. Not a ``loguru._logger.Logger``
73 subclass: its ``__init__`` takes ~10 positional private arguments
74 that shift between releases, so a proxy is the version-stable shape.
75 """
77 __slots__ = ("_logger",)
79 def __init__(self, wrapped=None):
80 self._logger = wrapped if wrapped is not None else _loguru_logger
82 def exception(__self, __message, *args, **kwargs): # noqa: N805
83 """Log at ERROR; attach the active exception only in diagnose mode.
85 Positional-only ``__self`` / ``__message`` mirror loguru's own
86 signature so ``str.format`` keyword fields (e.g. ``msg=``) cannot
87 collide with parameter names. ``depth=1`` attributes the record
88 to the caller's module/function/line, keeping
89 ``logger.enable()`` / ``logger.disable()`` namespace semantics.
90 """
91 __self._logger.opt(exception=is_diagnose_mode(), depth=1).error(
92 __message, *args, **kwargs
93 )
95 def bind(self, *args, **kwargs):
96 """Like loguru's ``bind()`` but the result keeps the gated ``exception()``."""
97 return SecureLogger(self._logger.bind(*args, **kwargs))
99 def patch(self, *args, **kwargs):
100 """Like loguru's ``patch()`` but the result keeps the gated ``exception()``."""
101 return SecureLogger(self._logger.patch(*args, **kwargs))
103 def __getattr__(self, name):
104 return getattr(self._logger, name)
107logger = SecureLogger()