"""Central logging configuration for the Seed project. Usage anywhere in the codebase: from logger import get_logger log = get_logger(__name__) log.info("message") log.warning("watch out") log.error("something broke") Environment variables: LOG_LEVEL — DEBUG | INFO | WARNING | ERROR | CRITICAL (default: INFO) LOG_FILE — path to write a rotating log file (optional) """ from __future__ import annotations import logging import logging.handlers import os import sys from typing import Final _LEVEL_MAP: Final = { "DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL, } _LOG_LEVEL: int = _LEVEL_MAP.get(os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO) _LOG_FILE: str | None = os.getenv("LOG_FILE") # ── ANSI colour codes (console only) ───────────────────────────────────────── _RESET = "\033[0m" _BOLD = "\033[1m" _GREY = "\033[90m" _CYAN = "\033[96m" _YELLOW = "\033[93m" _RED = "\033[91m" _BRED = "\033[1;91m" _LEVEL_COLOURS: Final[dict[int, str]] = { logging.DEBUG: _GREY, logging.INFO: _CYAN, logging.WARNING: _YELLOW, logging.ERROR: _RED, logging.CRITICAL: _BRED, } class _ColourFormatter(logging.Formatter): _FMT = "{colour}{level:<8}{reset} {grey}{asctime} {name}{reset} {msg}" def format(self, record: logging.LogRecord) -> str: colour = _LEVEL_COLOURS.get(record.levelno, "") level = record.levelname grey = _GREY if sys.stderr.isatty() else "" reset = _RESET if sys.stderr.isatty() else "" col = colour if sys.stderr.isatty() else "" self.datefmt = "%H:%M:%S" base = super().format(record) return self._FMT.format( colour=col, level=level, reset=reset, grey=grey, asctime=self.formatTime(record, self.datefmt), name=record.name, msg=record.getMessage(), ) + (f"\n{record.exc_text}" if record.exc_info and self.formatException(record.exc_info) else "") _FILE_FMT = logging.Formatter( fmt="%(asctime)s %(levelname)-8s %(name)s %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) def _build_root_logger() -> logging.Logger: root = logging.getLogger("seed") root.setLevel(_LOG_LEVEL) if root.handlers: return root # already configured (e.g. imported twice) # Console handler console = logging.StreamHandler(sys.stderr) console.setLevel(_LOG_LEVEL) console.setFormatter(_ColourFormatter()) root.addHandler(console) # Optional rotating file handler if _LOG_FILE: fh = logging.handlers.RotatingFileHandler( _LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8" ) fh.setLevel(_LOG_LEVEL) fh.setFormatter(_FILE_FMT) root.addHandler(fh) root.propagate = False return root _root = _build_root_logger() def get_logger(name: str) -> logging.Logger: """Return a child logger namespaced under 'seed'.""" if name.startswith("seed.") or name == "seed": return logging.getLogger(name) return logging.getLogger(f"seed.{name}")