ranczo-energy-usage-scrapers/logging_utils.py
Bartosz Wieczorek 166d64d51e init
2025-09-02 18:14:05 +02:00

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