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
« 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# ///////////////////////////////////////////////////////////////
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 inspect
33import logging
34from typing import TYPE_CHECKING
36if TYPE_CHECKING: 36 ↛ 37line 36 didn't jump to line 37 because the condition on line 36 was never true
37 import types
39# Third-party imports
40from loguru import logger
42# ///////////////////////////////////////////////////////////////
43# CLASSES
44# ///////////////////////////////////////////////////////////////
47class InterceptHandler(logging.Handler):
48 """
49 Redirect stdlib logging records to loguru.
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.
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.
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 """
68 def emit(self, record: logging.LogRecord) -> None:
69 """
70 Forward a stdlib LogRecord to loguru.
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)
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
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())
105# ///////////////////////////////////////////////////////////////
106# PUBLIC API
107# ///////////////////////////////////////////////////////////////
109__all__ = [
110 "InterceptHandler",
111]