Files
2026-06-20 17:07:48 +03:30

110 lines
3.3 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 (default: logs/seed.log)
"""
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 = os.getenv("LOG_FILE", "logs/seed.log")
# ── 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)
# Rotating file handler (always on)
os.makedirs(os.path.dirname(_LOG_FILE), exist_ok=True)
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}")