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
« 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# ///////////////////////////////////////////////////////////////
6"""
7InterceptHandler: bridge from stdlib logging to loguru.
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).
14Simplest usage via Ezpl (recommended):
16 ezpl = Ezpl(log_file="app.log", hook_logger=True)
18Manual installation (for fine-grained control):
20 import logging
21 from ezpl import InterceptHandler
23 logging.basicConfig(handlers=[InterceptHandler()], level=logging.DEBUG, force=True)
24"""
26from __future__ import annotations
28# ///////////////////////////////////////////////////////////////
29# IMPORTS
30# ///////////////////////////////////////////////////////////////
31# Standard library imports
32import logging
33from typing import TYPE_CHECKING
35if TYPE_CHECKING: 35 ↛ 36line 35 didn't jump to line 36 because the condition on line 35 was never true
36 import types
38# Third-party imports
39from loguru import logger
41# ///////////////////////////////////////////////////////////////
42# CLASSES
43# ///////////////////////////////////////////////////////////////
46class InterceptHandler(logging.Handler):
47 """
48 Redirect stdlib logging records to loguru.
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.
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.
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 """
67 def emit(self, record: logging.LogRecord) -> None:
68 """
69 Forward a stdlib LogRecord to loguru.
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)
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
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())
94# ///////////////////////////////////////////////////////////////
95# PUBLIC API
96# ///////////////////////////////////////////////////////////////
98__all__ = [
99 "InterceptHandler",
100]