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

1"""Loguru wrapper whose ``.exception()`` gates tracebacks behind diagnose mode. 

2 

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

9 

10:data:`logger` exported here is a :class:`SecureLogger` proxy around the 

11real loguru logger. Its ``.exception()`` is equivalent to:: 

12 

13 logger.opt(exception=is_diagnose_mode()).error(...) 

14 

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"). 

19 

20Usage — a drop-in replacement for the loguru import:: 

21 

22 from ..security.secure_logging import logger 

23 

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}") 

29 

30Caveats: 

31 

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""" 

47 

48import os 

49 

50from loguru import logger as _loguru_logger 

51 

52 

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") 

56 

57 

58def is_diagnose_mode() -> bool: 

59 """Return True when the operator explicitly opted into traceback output. 

60 

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") 

66 

67 

68class SecureLogger: 

69 """Thin proxy over the loguru logger with a diagnose-gated ``exception()``. 

70 

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 """ 

76 

77 __slots__ = ("_logger",) 

78 

79 def __init__(self, wrapped=None): 

80 self._logger = wrapped if wrapped is not None else _loguru_logger 

81 

82 def exception(__self, __message, *args, **kwargs): # noqa: N805 

83 """Log at ERROR; attach the active exception only in diagnose mode. 

84 

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 ) 

94 

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)) 

98 

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)) 

102 

103 def __getattr__(self, name): 

104 return getattr(self._logger, name) 

105 

106 

107logger = SecureLogger()