59 lines
2.5 KiB
Python
59 lines
2.5 KiB
Python
import logging
|
|
from contextlib import contextmanager
|
|
import inspect
|
|
|
|
def _fmt_ctx(ctx: dict) -> str:
|
|
# Build a single bracketed context block for the formatter: "[k=v a=b] "
|
|
if not ctx:
|
|
return ""
|
|
kv = " ".join(f"{k}={v}" for k, v in ctx.items() if v is not None)
|
|
return f"[{kv}] "
|
|
|
|
|
|
class HasLogger:
|
|
"""Mixin with explicit logger initialization. No __init__, no MRO dependency."""
|
|
|
|
def __init__(self):
|
|
self.log = None
|
|
self._log_ctx = None
|
|
self._base_logger = None
|
|
|
|
def init_logger(self, *, context: dict | None = None, name: str | None = None) -> None:
|
|
"""Initialize the logger explicitly; call from your class __init__."""
|
|
base_name = name or self.__class__.__name__
|
|
self._base_logger = logging.getLogger(base_name)
|
|
self._log_ctx: dict = dict(context or {})
|
|
self.log = logging.LoggerAdapter(self._base_logger, {"context": _fmt_ctx(self._log_ctx)})
|
|
|
|
def set_logger_context(self, **context) -> None:
|
|
"""Persistently merge new context into this instance and refresh adapter."""
|
|
if not hasattr(self, "_base_logger"): # safety if init_logger was forgotten
|
|
self.init_logger()
|
|
self._log_ctx.update({k: v for k, v in context.items() if v is not None})
|
|
self.log = logging.LoggerAdapter(self._base_logger, {"context": _fmt_ctx(self._log_ctx)})
|
|
|
|
def child_logger(self, **extra) -> logging.LoggerAdapter:
|
|
"""Temporary adapter with additional context (does not mutate instance context)."""
|
|
if not hasattr(self, "_base_logger"):
|
|
self.init_logger()
|
|
merged = self._log_ctx.copy()
|
|
merged.update({k: v for k, v in extra.items() if v is not None})
|
|
return logging.LoggerAdapter(self._base_logger, {"context": _fmt_ctx(merged)})
|
|
|
|
@contextmanager
|
|
def scoped_log(self, **extra):
|
|
"""Context manager yielding a temporary adapter with extra context."""
|
|
yield self.child_logger(**extra)
|
|
|
|
|
|
def function_logger(name: str | None = None, **context):
|
|
"""Return a LoggerAdapter that follows the HasLogger format for free functions."""
|
|
# Derive a readable default name: "<module>.<function>"
|
|
if name is None:
|
|
frame = inspect.currentframe().f_back # caller
|
|
func = frame.f_code.co_name
|
|
mod = frame.f_globals.get("__name__", "app")
|
|
name = f"{mod}.{func}"
|
|
h = HasLogger()
|
|
h.init_logger(name=name, context=context)
|
|
return h.log |