110 lines
3.2 KiB
Python
110 lines
3.2 KiB
Python
"""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}")
|