69c685798c
- 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
82 lines
2.6 KiB
Python
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:])
|