Coverage for src/ezplog/app_mode.py: 83.33%

20 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-17 00:16 +0000

1# /////////////////////////////////////////////////////////////// 

2# APP_MODE - Stdlib logging bridge for application-level interception 

3# Project: ezpl 

4# /////////////////////////////////////////////////////////////// 

5 

6""" 

7InterceptHandler: bridge from stdlib logging to loguru. 

8 

9Install this handler on the root stdlib logger to automatically capture 

10log records emitted by any library using logging.getLogger(__name__) — 

11including those using ezpl.lib_mode.get_logger() — and route them through 

12the loguru pipeline (and thus through EzLogger if configured). 

13 

14Simplest usage via Ezpl (recommended): 

15 

16 ezpl = Ezpl(log_file="app.log", hook_logger=True) 

17 

18Manual installation (for fine-grained control): 

19 

20 import logging 

21 from ezpl import InterceptHandler 

22 

23 logging.basicConfig(handlers=[InterceptHandler()], level=logging.DEBUG, force=True) 

24""" 

25 

26from __future__ import annotations 

27 

28# /////////////////////////////////////////////////////////////// 

29# IMPORTS 

30# /////////////////////////////////////////////////////////////// 

31# Standard library imports 

32import inspect 

33import logging 

34from typing import TYPE_CHECKING 

35 

36if TYPE_CHECKING: 36 ↛ 37line 36 didn't jump to line 37 because the condition on line 36 was never true

37 import types 

38 

39# Third-party imports 

40from loguru import logger 

41 

42# /////////////////////////////////////////////////////////////// 

43# CLASSES 

44# /////////////////////////////////////////////////////////////// 

45 

46 

47class InterceptHandler(logging.Handler): 

48 """ 

49 Redirect stdlib logging records to loguru. 

50 

51 This handler bridges the stdlib logging system and loguru, allowing 

52 libraries that use logging.getLogger(__name__) to have their output 

53 captured by the loguru pipeline configured by ezpl. 

54 

55 The caller frame is resolved by walking up the call stack past logging 

56 internals, so the log records appear with the correct source location 

57 in loguru output. 

58 

59 Example: 

60 >>> import logging 

61 >>> from ezpl import Ezpl, InterceptHandler 

62 >>> # Option 1 — automatic via Ezpl 

63 >>> ezpl = Ezpl(log_file="app.log", hook_logger=True) 

64 >>> # Option 2 — manual installation 

65 >>> logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) 

66 """ 

67 

68 def emit(self, record: logging.LogRecord) -> None: 

69 """ 

70 Forward a stdlib LogRecord to loguru. 

71 

72 Args: 

73 record: The log record emitted by a stdlib logger. 

74 """ 

75 # Map stdlib level name to a loguru level; fall back to numeric level 

76 try: 

77 level = logger.level(record.levelname).name 

78 except ValueError: 

79 level = str(record.levelno) 

80 

81 # Walk up the call stack to find the actual caller — skip both 

82 # `emit` itself and all stdlib `logging` machinery (handlers, Logger._log, 

83 # callHandlers, etc.). This is loguru's canonical InterceptHandler recipe: 

84 # start at the current frame (depth=0) and keep walking while we're 

85 # either still in `emit` or inside the stdlib logging module. 

86 frame: types.FrameType | None = inspect.currentframe() 

87 depth = 0 

88 while frame is not None and ( 

89 depth == 0 or frame.f_code.co_filename == logging.__file__ 

90 ): 

91 frame = frame.f_back 

92 depth += 1 

93 

94 # Bind task="logger" so records pass EzLogger's file sink filter. 

95 # NOTE: brace escaping (`{` -> `{{`) is handled centrally by EzLogger's 

96 # custom formatter, which is the single source of truth for all entry 

97 # paths (intercept, direct calls, third-party binds). Do not escape 

98 # here or braces would be doubled in the final output. 

99 logger.bind(task="logger").opt( 

100 depth=depth, 

101 exception=record.exc_info, 

102 ).log(level, record.getMessage()) 

103 

104 

105# /////////////////////////////////////////////////////////////// 

106# PUBLIC API 

107# /////////////////////////////////////////////////////////////// 

108 

109__all__ = [ 

110 "InterceptHandler", 

111]