diff --git a/src/sync/compact.py b/src/sync/compact.py index bee7aee..bdb0883 100644 --- a/src/sync/compact.py +++ b/src/sync/compact.py @@ -24,6 +24,7 @@ import time from dataclasses import dataclass, field from .config import SyncConfig +from .logging_setup import setup_compact_log log = logging.getLogger(__name__) @@ -31,6 +32,9 @@ log = logging.getLogger(__name__) # it supports both .mdb (Jet) and .accdb (ACE) formats. _DAO_PROGID = "DAO.DBEngine.120" +# Guard: only set up the compact file handler once per process. +_compact_log_ready = False + # ------------------------------------------------------------------ result types @@ -264,7 +268,15 @@ def compact_files(cfg: SyncConfig, When *dry_run* is True, only checks file accessibility and lock status without modifying anything. Each file is compacted independently; one failure does not abort the run. + + Logs are written to a dedicated ``logs/compact.log`` file (separate from + ``sync.log``) with the same rotation and daily-archive semantics. """ + global _compact_log_ready + if not _compact_log_ready: + setup_compact_log(cfg.logging) + _compact_log_ready = True + if dry_run: return compact_dry_run(cfg, db_filter=db_filter, file_list=file_list) diff --git a/src/sync/logging_setup.py b/src/sync/logging_setup.py index d331838..d857742 100644 --- a/src/sync/logging_setup.py +++ b/src/sync/logging_setup.py @@ -169,3 +169,42 @@ def setup_logging(cfg_dict: dict | None): # Console keeps the short format (fullsync/compare are interactive there). sh.setFormatter(logging.Formatter("%(levelname)s %(message)s")) root.addHandler(sh) + + +def setup_compact_log(cfg_dict: dict | None): + """Configure a dedicated file handler for the ``sync.compact`` logger. + + Writes to ``logs/compact.log`` with the same rotation and daily-archive + semantics as ``sync.log``, but kept in a separate file so compact audit + trail is isolated and easy to grep. The logger does NOT propagate to the + root logger — compact messages go to ``compact.log`` only (console output + is handled by ``main.py``'s ``print()`` calls, not by this handler). + """ + level = getattr(logging, (cfg_dict or {}).get("level", "INFO")) if cfg_dict else logging.INFO + sync_path = (cfg_dict or {}).get("path", "sync.log") + log_dir = os.path.dirname(sync_path) or "." + os.makedirs(log_dir, exist_ok=True) + + compact_path = os.path.join(log_dir, "compact.log") + # Archive yesterday's compact.log using the same logic as sync.log. + _archive_completed_logs(log_dir, os.path.basename(compact_path)) + + cl = logging.getLogger("sync.compact") + cl.setLevel(level) + cl.propagate = False # do NOT duplicate into sync.log + + # Idempotent: drop existing handlers before re-adding. + for old in list(cl.handlers): + cl.removeHandler(old) + try: + old.close() + except Exception: + pass + + fh = logging.handlers.RotatingFileHandler( + compact_path, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8" + ) + fh.setFormatter(logging.Formatter( + "%(asctime)s %(levelname)s [%(name)s] %(message)s" + )) + cl.addHandler(fh)