"""Logging setup: stdout (always) plus a rotating file locally, with secret redaction.""" from __future__ import annotations import logging import logging.handlers from pathlib import Path FORMAT = "%(asctime)s %(levelname)-7s %(name)s: %(message)s" class RedactSecrets(logging.Filter): """Replace known secret values in every log record.""" def __init__(self, secrets: list[str]): super().__init__() self._secrets = sorted({s for s in secrets if s}, key=len, reverse=True) def filter(self, record: logging.LogRecord) -> bool: if not self._secrets: return True message = record.getMessage() redacted = message for secret in self._secrets: redacted = redacted.replace(secret, "***") if redacted != message: record.msg, record.args = redacted, None return True def setup_logging(level: str = "INFO", *, log_dir: Path | None = None, secrets: list[str] | None = None) -> None: """``log_dir=None`` logs to stdout only (Heroku collects stdout via `heroku logs`).""" root = logging.getLogger() root.setLevel(level) for handler in list(root.handlers): root.removeHandler(handler) redact = RedactSecrets(secrets or []) handlers: list[logging.Handler] = [logging.StreamHandler()] if log_dir is not None: log_dir.mkdir(parents=True, exist_ok=True) handlers.append( logging.handlers.RotatingFileHandler(log_dir / "bot.log", maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8") ) for handler in handlers: handler.setFormatter(logging.Formatter(FORMAT)) handler.addFilter(redact) root.addHandler(handler) for noisy in ("telethon", "googleapiclient.discovery_cache", "urllib3", "google_auth_httplib2"): logging.getLogger(noisy).setLevel(logging.WARNING)