feat(compact): dedicated compact.log with same daily-archive semantics as sync.log
Compact audit trail now writes to logs/compact.log via its own RotatingFileHandler. The sync.compact logger does not propagate to root, so compact messages are isolated. Archive/ rotation happens on next setup_logging call (service restart) via the existing mechanism. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import time
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from .config import SyncConfig
|
from .config import SyncConfig
|
||||||
|
from .logging_setup import setup_compact_log
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@ log = logging.getLogger(__name__)
|
|||||||
# it supports both .mdb (Jet) and .accdb (ACE) formats.
|
# it supports both .mdb (Jet) and .accdb (ACE) formats.
|
||||||
_DAO_PROGID = "DAO.DBEngine.120"
|
_DAO_PROGID = "DAO.DBEngine.120"
|
||||||
|
|
||||||
|
# Guard: only set up the compact file handler once per process.
|
||||||
|
_compact_log_ready = False
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ result types
|
# ------------------------------------------------------------------ result types
|
||||||
|
|
||||||
@@ -264,7 +268,15 @@ def compact_files(cfg: SyncConfig,
|
|||||||
When *dry_run* is True, only checks file accessibility and lock status
|
When *dry_run* is True, only checks file accessibility and lock status
|
||||||
without modifying anything. Each file is compacted independently; one
|
without modifying anything. Each file is compacted independently; one
|
||||||
failure does not abort the run.
|
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:
|
if dry_run:
|
||||||
return compact_dry_run(cfg, db_filter=db_filter, file_list=file_list)
|
return compact_dry_run(cfg, db_filter=db_filter, file_list=file_list)
|
||||||
|
|
||||||
|
|||||||
@@ -169,3 +169,42 @@ def setup_logging(cfg_dict: dict | None):
|
|||||||
# Console keeps the short format (fullsync/compare are interactive there).
|
# Console keeps the short format (fullsync/compare are interactive there).
|
||||||
sh.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
|
sh.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
|
||||||
root.addHandler(sh)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user