import logging from logging.config import dictConfig import os import re from logging.handlers import RotatingFileHandler, HTTPHandler class StripAnsiFormatter(logging.Formatter): """Formatter that strips ANSI escape sequences from formatted log text.""" ANSI_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") def format(self, record): text = super().format(record) return self.ANSI_RE.sub("", text) def setup_logging(app): """Configure logging: console, optional file and external HTTP handler. Controlled by env vars: - LOG_FILE: path to write rotating logs - LOG_MAX_BYTES: max bytes per file (default 10MB) - LOG_BACKUP_COUNT: number of backups (default 5) - EXTERNAL_LOG_URL: if set, an HTTPHandler will POST logs to this URL """ level = logging.DEBUG if app.config.get("DEBUG") else logging.INFO handlers = { "console": { "class": "logging.StreamHandler", "formatter": "default", "level": level, } } root_handlers = ["console"] log_file = os.getenv("LOG_FILE") if log_file: # ensure parent dirs exist dirname = os.path.dirname(log_file) if dirname: os.makedirs(dirname, exist_ok=True) handlers["file"] = { "class": "logging.handlers.RotatingFileHandler", "formatter": "file", "level": level, "filename": log_file, "maxBytes": int(os.getenv("LOG_MAX_BYTES", 10 * 1024 * 1024)), "backupCount": int(os.getenv("LOG_BACKUP_COUNT", 5)), } root_handlers.append("file") external = os.getenv("EXTERNAL_LOG_URL") if external: handlers["external"] = { "class": "logging.handlers.HTTPHandler", "formatter": "default", "level": level, "host": external, "url": "/", "method": "POST", } root_handlers.append("external") config = { "version": 1, "disable_existing_loggers": False, "formatters": { "default": { "format": "%(asctime)s %(levelname)s %(name)s: %(message)s", }, "file": { "()": "app.utils.logger.StripAnsiFormatter", "format": "%(asctime)s %(levelname)s %(name)s: %(message)s", }, }, "handlers": handlers, "root": { "handlers": root_handlers, "level": level, }, } dictConfig(config)