Files
MailRelay/modules/logger.py
T
Claude 69c685798c Add --debug flag, dual-mode logging, and auto-run on startup
- logger.py: root-level handlers; normal mode shows only user-facing INFO
  (sync, export, push, MBOX) plus WARNING/ERROR; --debug shows all DEBUG
  with full context (module.func:line). Third-party loggers silenced to WARNING.
- main.py: add --debug CLI flag, call configure_logging() at startup,
  auto-trigger sync on first run or when last sync is overdue by the interval
- database.py: add metadata table with record_sync_time() / get_last_sync_time()
  so startup knows whether a sync is due; sync time recorded on success
- forwarder.py: INFO at push start and push complete with counts
- packager.py: INFO before MBOX conversion begins
- exporter.py: INFO when Proton Mail export starts

https://claude.ai/code/session_01KjaNo9RXevw6x1DjJD8mj6
2026-03-24 21:54:25 +00:00

82 lines
2.6 KiB
Python

"""Rotating log file and console setup for MailRelay.
Two modes:
normal — console shows only user-facing INFO messages (sync, push, MBOX)
plus WARNING/ERROR from any logger
debug — console shows DEBUG from every logger with full context
"""
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
LOG_PATH = Path(__file__).parent.parent / "data" / "mailrelay.log"
MAX_BYTES = 5 * 1024 * 1024 # 5 MB
BACKUP_COUNT = 3
_NORMAL_FORMAT = "%(asctime)s [%(levelname)-8s] %(message)s"
_DEBUG_FORMAT = "%(asctime)s [%(levelname)-8s] %(name)s.%(funcName)s:%(lineno)d%(message)s"
# Loggers whose INFO messages are shown in normal (non-debug) mode
_USER_LOGGERS = frozenset({
"__main__",
"modules.exporter",
"modules.forwarder",
"modules.packager",
})
# Third-party loggers silenced to WARNING in all modes
_SUPPRESS = ("apscheduler", "uvicorn", "fastapi", "asyncio", "multipart", "starlette")
class _UserFacingFilter(logging.Filter):
"""Pass WARNING+ from any logger; INFO only from the user-visible set."""
def filter(self, record: logging.LogRecord) -> bool:
if record.levelno >= logging.WARNING:
return True
return record.name in _USER_LOGGERS
def get_logger(name: str) -> logging.Logger:
"""Return a named logger that propagates to the root handler."""
return logging.getLogger(name)
def configure_logging(debug: bool = False) -> None:
"""Set up root logger with file + console handlers. Call once at startup."""
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.handlers.clear()
# File handler — always full DEBUG detail
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
fh = RotatingFileHandler(
LOG_PATH, maxBytes=MAX_BYTES, backupCount=BACKUP_COUNT, encoding="utf-8"
)
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter(_DEBUG_FORMAT))
root.addHandler(fh)
# Console handler
ch = logging.StreamHandler()
if debug:
ch.setLevel(logging.DEBUG)
ch.setFormatter(logging.Formatter(_DEBUG_FORMAT))
else:
ch.setLevel(logging.INFO)
ch.addFilter(_UserFacingFilter())
ch.setFormatter(logging.Formatter(_NORMAL_FORMAT))
root.addHandler(ch)
# Silence noisy third-party loggers
for lib in _SUPPRESS:
logging.getLogger(lib).setLevel(logging.WARNING)
def tail_log(lines: int = 50) -> str:
"""Return the last N lines of the log file as a string."""
if not LOG_PATH.exists():
return "(no log file yet)"
text = LOG_PATH.read_text(encoding="utf-8")
return "\n".join(text.splitlines()[-lines:])