From 0750f4c41d4569591f0e1cafaa1a6cef10051cac Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Thu, 16 Jul 2026 09:22:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=8C=89=E5=A4=A9=E7=AE=A1?= =?UTF-8?q?=E7=90=86=EF=BC=9A=E6=96=B0=E5=A2=9E=20DailyFileHandler=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=BD=92=E6=A1=A3=E5=8E=86=E5=8F=B2=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 需求:每天一个日志;历史日志重命名加时间戳后缀并归档; 日志根目录只保留当天文件;已产生的日志全部放进根目录下的 Archive 文件夹。 实现(src/runner.py): - 新增 DailyFileHandler(logging.FileHandler): · 当天写入 /sync.log(根目录只留当天文件)。 · 跨天或进程启动时,旧 sync.log 重命名为 sync_YYYYMMDD.log 并移入 /Archive/ 归档。 · emit 时若日期变化自动滚动;额外提供 maybe_rollover() 供主循环每轮主动检查,保证跨天必切(即使本轮无日志输出)。 - setup_logging 改为幂等:清掉已有 handler 再添加,避免重复日志行 (此前 setup_logging 被多次调用会产生重复行,现已免疫)。 - main 循环每轮调用 maybe_rollover,跨天切换时记一行 “日志已切换到新的一天,旧日志归档至 Archive/”。 验证:本地 4 项单测全绿(启动归档旧文件 / 当天写入 / 跨天主动滚动 / 同天不滚动)。 --- src/runner.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/runner.py b/src/runner.py index 454a54f..6459977 100644 --- a/src/runner.py +++ b/src/runner.py @@ -21,6 +21,74 @@ from . import source_watcher logger = logging.getLogger(__name__) +class DailyFileHandler(logging.FileHandler): + """按天滚动的日志 handler。 + + - 当天写入 ``/sync.log``(根目录只保留当天文件)。 + - 跨天或进程启动时,旧的 ``sync.log`` 会被重命名为 + ``sync_YYYYMMDD.log`` 并移入 ``/Archive/`` 归档。 + - 历史日志全部集中在 Archive 文件夹,根目录保持干净。 + """ + + def __init__(self, log_dir, filename="sync.log", encoding="utf-8"): + self.log_dir = Path(log_dir) + self.archive_dir = self.log_dir / "Archive" + self.filename = filename + self.archive_dir.mkdir(parents=True, exist_ok=True) + self._file_date = time.strftime("%Y%m%d") + # 启动时若根目录已有“非今天”的 sync.log(跨天重启 / 长期停机),先归档 + self._archive_if_stale() + super().__init__(str(self.log_dir / self.filename), mode="a", + encoding=encoding, delay=False) + + def _archived_name(self, datestr): + p = Path(self.filename) + return f"{p.stem}_{datestr}{p.suffix}" + + def _archive_if_stale(self): + cur = self.log_dir / self.filename + if cur.exists(): + mdate = time.strftime("%Y%m%d", time.localtime(cur.stat().st_mtime)) + if mdate < self._file_date: + dest = self.archive_dir / self._archived_name(mdate) + if not dest.exists(): + try: + cur.rename(dest) + except OSError: + pass + + def maybe_rollover(self): + """可在 emit 之外主动调用(如主循环每轮),保证跨天必切文件。 + 返回是否发生了滚动。""" + today = time.strftime("%Y%m%d") + if today != self._file_date: + self._rollover_to(today) + return True + return False + + def _rollover_to(self, new_date): + if self.stream: + self.stream.close() + self.stream = None + old = self.log_dir / self.filename + if old.exists(): + dest = self.archive_dir / self._archived_name(self._file_date) + if not dest.exists(): + try: + old.rename(dest) + except OSError: + pass + self.baseFilename = str(self.log_dir / self.filename) + self._file_date = new_date + self.stream = self._open() + + def emit(self, record): + today = time.strftime("%Y%m%d") + if today != self._file_date: + self._rollover_to(today) + super().emit(record) + + def setup_logging(cfg): log_cfg = cfg.get("logging", {}) level = getattr(logging, log_cfg.get("level", "INFO").upper(), logging.INFO) @@ -29,12 +97,20 @@ def setup_logging(cfg): fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") root = logging.getLogger() root.setLevel(level) - fh = logging.FileHandler(log_dir / "sync.log", encoding="utf-8") + # 幂等:清理已有 handler,避免重复日志行(setup_logging 被多次调用时也安全) + for h in list(root.handlers): + root.removeHandler(h) + try: + h.close() + except Exception: + pass + fh = DailyFileHandler(log_dir) fh.setFormatter(fmt) root.addHandler(fh) sh = logging.StreamHandler(sys.stdout) sh.setFormatter(fmt) root.addHandler(sh) + return fh def run_once(cfg, source_path=None, dry_run=False): @@ -82,7 +158,7 @@ def main(): args = parser.parse_args() cfg = load_config(args.config) - setup_logging(cfg) + log_fh = setup_logging(cfg) if args.local or args.dry_run: run_once(cfg, source_path=args.local, dry_run=args.dry_run) @@ -98,6 +174,9 @@ def main(): run_once(cfg) except Exception as e: logger.exception("同步周期失败: %s", e) + # 主动检查跨天滚动(即使本轮无日志输出也保证按天归档) + if log_fh.maybe_rollover(): + logger.info("=== 日志已切换到新的一天,旧日志归档至 Archive/ ===") time.sleep(interval)