49 lines
1.8 KiB
Python
49 lines
1.8 KiB
Python
"""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)
|