Coverage for src/local_deep_research/security/secure_logging.py: 100%

21 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-22 21:20 +0000

1"""Shared logging helpers for controlling exception detail. 

2 

3``log_exception_type()`` logs a fixed action and exception class at ERROR 

4without exception text or tracebacks, including in diagnose mode. 

5 

6Provider and search-engine exception handlers must stay observable at 

7ERROR level, but plain ``loguru.logger.exception()`` always attaches the 

8active exception to the record — every sink (stderr, database, frontend) 

9then renders or persists the traceback, including the ``__cause__`` / 

10``__context__`` chain, which has leaked Authorization headers and full 

11URLs embedded in wrapped exceptions (#4183). 

12 

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

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

15 

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

17 

18so in normal operation the record carries *no* exception object — no sink 

19can render a traceback — while the event itself still lands at ERROR. 

20Developers opt into full tracebacks by setting **both** ``LDR_APP_DEBUG`` 

21and ``LDR_LOGURU_DIAGNOSE`` to a truthy value ("1", "true", "yes"). 

22 

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

24 

25 from ..security.secure_logging import logger 

26 

27 try: 

28 ... 

29 except Exception as e: 

30 safe_msg = scrub_error(e) # from ..security.log_sanitizer 

31 logger.exception(f"Engine request failed: {safe_msg}") 

32 

33Caveats: 

34 

35* **Messages are production-visible at ERROR.** The wrapper only strips 

36 the traceback; the message string itself reaches every sink. Call sites 

37 must still pass scrubbed text (``scrub_error`` — or in engine 

38 subclasses ``self._scrub_error``), never raw ``str(e)``. 

39* **Known bypass routes.** ``logger.opt(exception=True).error(...)`` and 

40 ``@logger.catch`` delegate to plain loguru and are NOT gated. No call 

41 site under ``llm/providers/``, ``embeddings/providers/`` or 

42 ``web_search_engines/`` uses them today; the #4183 step-2 pre-commit 

43 hook should detect them. 

44* **Env-gated only.** ``config_logger()`` in ``utilities/log_utils.py`` 

45 gates its per-sink ``diagnose`` flag on its ``debug`` argument, which 

46 may come from the ``app.debug`` DB setting — this wrapper reads only 

47 the environment, so a DB-enabled debug mode does not enable wrapper 

48 tracebacks. See the cross-reference comment in ``config_logger``. 

49""" 

50 

51import os 

52 

53from loguru import logger as _loguru_logger 

54 

55 

56def log_exception_type(action: str, exc: Exception) -> None: 

57 """Log a fixed action and exception class without exception details. 

58 

59 ``action`` must be developer-written text, with no request data or 

60 exception text interpolated into it. The exception is never stringified 

61 or attached to the record, even in diagnose mode. ``depth=1`` preserves 

62 the caller's location and module-based logging filters. 

63 """ 

64 _loguru_logger.opt(exception=False, depth=1).error( 

65 "{action} ({error_type})", 

66 action=action, 

67 error_type=type(exc).__name__, 

68 ) 

69 

70 

71def env_truthy(name: str) -> bool: 

72 """Return True if env var *name* is set to a truthy value ("1", "true", "yes").""" 

73 return os.environ.get(name, "").strip().lower() in ("1", "true", "yes") 

74 

75 

76def is_diagnose_mode() -> bool: 

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

78 

79 Requires BOTH ``LDR_APP_DEBUG`` and ``LDR_LOGURU_DIAGNOSE`` to be 

80 truthy, mirroring the two-flag opt-in that ``config_logger()`` uses 

81 for loguru's frame-locals ``diagnose`` rendering. 

82 """ 

83 return env_truthy("LDR_APP_DEBUG") and env_truthy("LDR_LOGURU_DIAGNOSE") 

84 

85 

86class SecureLogger: 

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

88 

89 Everything except ``exception()``, ``bind()`` and ``patch()`` 

90 delegates straight to loguru. Not a ``loguru._logger.Logger`` 

91 subclass: its ``__init__`` takes ~10 positional private arguments 

92 that shift between releases, so a proxy is the version-stable shape. 

93 """ 

94 

95 __slots__ = ("_logger",) 

96 

97 def __init__(self, wrapped=None): 

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

99 

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

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

102 

103 Positional-only ``__self`` / ``__message`` mirror loguru's own 

104 signature so ``str.format`` keyword fields (e.g. ``msg=``) cannot 

105 collide with parameter names. ``depth=1`` attributes the record 

106 to the caller's module/function/line, keeping 

107 ``logger.enable()`` / ``logger.disable()`` namespace semantics. 

108 """ 

109 __self._logger.opt(exception=is_diagnose_mode(), depth=1).error( 

110 __message, *args, **kwargs 

111 ) 

112 

113 def bind(self, *args, **kwargs): 

114 """Like loguru's ``bind()`` but the result keeps the gated ``exception()``.""" 

115 return SecureLogger(self._logger.bind(*args, **kwargs)) 

116 

117 def patch(self, *args, **kwargs): 

118 """Like loguru's ``patch()`` but the result keeps the gated ``exception()``.""" 

119 return SecureLogger(self._logger.patch(*args, **kwargs)) 

120 

121 def __getattr__(self, name): 

122 return getattr(self._logger, name) 

123 

124 

125logger = SecureLogger()