import logging from logging.handlers import RotatingFileHandler from pathlib import Path LOG_DIR = Path(__file__).resolve().parent.parent / "logs" LOG_FILE = LOG_DIR / "app.log" _FORMAT = "%(asctime)s %(levelname)s [%(name)s] %(message)s" def setup_logging() -> None: """Route all app + uvicorn logging to logs/app.log so errors are traceable without depending on however the process happens to be launched.""" LOG_DIR.mkdir(exist_ok=True) handler = RotatingFileHandler(LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5) handler.setFormatter(logging.Formatter(_FORMAT)) root = logging.getLogger() root.setLevel(logging.INFO) root.addHandler(handler) # "uvicorn.error"/"uvicorn.access" have propagate=False in uvicorn's default # logging config, so they never reach the root handler above and need their # own. The parent "uvicorn" logger is deliberately skipped: uvicorn.error # already bubbles into it, so attaching there too would double every line. for logger_name in ("uvicorn.error", "uvicorn.access"): logging.getLogger(logger_name).addHandler(handler)