""" utils/logger.py — Centralised logging configuration. Writes simultaneously to stdout (coloured) and a rotating file log. """ import logging import os import sys from datetime import datetime from logging.handlers import RotatingFileHandler import config class _ColourFormatter(logging.Formatter): """ANSI colour codes for terminal output.""" GREY = "\033[38;5;240m" CYAN = "\033[36m" YELLOW = "\033[33m" RED = "\033[31m" BOLD_R = "\033[1;31m" RESET = "\033[0m" LEVEL_COLOURS = { logging.DEBUG : GREY, logging.INFO : CYAN, logging.WARNING : YELLOW, logging.ERROR : RED, logging.CRITICAL: BOLD_R, } def format(self, record: logging.LogRecord) -> str: colour = self.LEVEL_COLOURS.get(record.levelno, self.RESET) fmt = ( f"{self.GREY}%(asctime)s{self.RESET} " f"{colour}%(levelname)-8s{self.RESET} " f"%(name)-20s %(message)s" ) formatter = logging.Formatter(fmt, datefmt="%H:%M:%S") return formatter.format(record) def get_logger(name: str) -> logging.Logger: """ Return a configured logger for *name*. All loggers share the same handlers so duplicate lines never appear. Call once per module: logger = get_logger(__name__) """ logger = logging.getLogger(name) # Avoid adding handlers more than once (module re-imports) if logger.handlers: return logger level = getattr(logging, config.LOG_LEVEL.upper(), logging.INFO) logger.setLevel(level) # ── Console handler ────────────────────────────────────────────────── ch = logging.StreamHandler(sys.stdout) ch.setLevel(level) ch.setFormatter(_ColourFormatter()) logger.addHandler(ch) # ── File handler ───────────────────────────────────────────────────── if config.LOG_TO_FILE: log_filename = os.path.join( config.LOGS_DIR, f"scraper_{datetime.now().strftime('%Y%m%d')}.log", ) fh = RotatingFileHandler( log_filename, maxBytes=5 * 1024 * 1024, # 5 MB backupCount=7, encoding="utf-8", ) fh.setLevel(level) plain_fmt = logging.Formatter( "%(asctime)s %(levelname)-8s %(name)-20s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) fh.setFormatter(plain_fmt) logger.addHandler(fh) # Prevent propagation to root logger (avoids double-printing) logger.propagate = False return logger