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

19 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 16:27 +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 logging 

33from typing import TYPE_CHECKING 

34 

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

36 import types 

37 

38# Third-party imports 

39from loguru import logger 

40 

41# /////////////////////////////////////////////////////////////// 

42# CLASSES 

43# /////////////////////////////////////////////////////////////// 

44 

45 

46class InterceptHandler(logging.Handler): 

47 """ 

48 Redirect stdlib logging records to loguru. 

49 

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

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

52 captured by the loguru pipeline configured by ezpl. 

53 

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

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

56 in loguru output. 

57 

58 Example: 

59 >>> import logging 

60 >>> from ezpl import Ezpl, InterceptHandler 

61 >>> # Option 1 — automatic via Ezpl 

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

63 >>> # Option 2 — manual installation 

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

65 """ 

66 

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

68 """ 

69 Forward a stdlib LogRecord to loguru. 

70 

71 Args: 

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

73 """ 

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

75 try: 

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

77 except ValueError: 

78 level = str(record.levelno) 

79 

80 # Walk up the call stack to find the actual caller — skip logging internals 

81 frame: types.FrameType | None = logging.currentframe() 

82 depth = 2 

83 while frame is not None and frame.f_code.co_filename == logging.__file__: 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true

84 frame = frame.f_back 

85 depth += 1 

86 

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

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

89 depth=depth, 

90 exception=record.exc_info, 

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

92 

93 

94# /////////////////////////////////////////////////////////////// 

95# PUBLIC API 

96# /////////////////////////////////////////////////////////////// 

97 

98__all__ = [ 

99 "InterceptHandler", 

100]