""" logging - Centralized logging configuration for Vacuum Wall. Call :func:`setup_logging` once at application startup. All other modules obtain a logger via ``logging.getLogger(__name__)``. Output: * **stderr** (StreamHandler) - captured by systemd journald * **data/logs/vacuum-wall.log** (RotatingFileHandler) - persisted for viewing via the WebUI ``/logs`` page. """ import logging import os import sys from logging.handlers import RotatingFileHandler from pathlib import Path PROJECT_DIR = Path(__file__).resolve().parent.parent _LOG_DIR = PROJECT_DIR / "data" / "logs" _LOG_FILE = _LOG_DIR / "vacuum-wall.log" _MAX_BYTES = 5 * 1024 * 1024 # 5 MB _BACKUP_COUNT = 3 _LOG_FMT = "[%(asctime)s] %(levelname)-8s %(name)s %(message)s" _DATE_FMT = "%Y-%m-%d %H:%M:%S" _initialized = False def setup_logging(level: str | None = None) -> None: """Configure and enable root-level logging for the application. Safe to call multiple times; subsequent calls are no-ops. Args: level: Override log level string (e.g. ``"DEBUG"``). If ``None``, reads ``VACUUM_WALL_LOG_LEVEL`` from the environment, defaulting to ``"INFO"``. """ global _initialized if _initialized: return _initialized = True if level is None: level = os.environ.get("VACUUM_WALL_LOG_LEVEL", "INFO").upper() valid_levels = { "DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL, } numeric = valid_levels.get(level, logging.INFO) root = logging.getLogger() root.setLevel(numeric) fmt = logging.Formatter(_LOG_FMT, datefmt=_DATE_FMT) # stderr handler — feeds systemd journal sh = logging.StreamHandler(sys.stderr) sh.setFormatter(fmt) root.addHandler(sh) # rotating file handler _LOG_DIR.mkdir(parents=True, exist_ok=True) fh = RotatingFileHandler( str(_LOG_FILE), maxBytes=_MAX_BYTES, backupCount=_BACKUP_COUNT, ) fh.setFormatter(fmt) root.addHandler(fh) # Silence noisy third-party loggers in production for name in ("werkzeug", "urllib3"): logging.getLogger(name).setLevel(logging.WARNING)