日志按天管理:新增 DailyFileHandler 自动归档历史日志

需求:每天一个日志;历史日志重命名加时间戳后缀并归档;
日志根目录只保留当天文件;已产生的日志全部放进根目录下的
Archive 文件夹。

实现(src/runner.py):
- 新增 DailyFileHandler(logging.FileHandler):
  · 当天写入 <log_dir>/sync.log(根目录只留当天文件)。
  · 跨天或进程启动时,旧 sync.log 重命名为 sync_YYYYMMDD.log
    并移入 <log_dir>/Archive/ 归档。
  · emit 时若日期变化自动滚动;额外提供 maybe_rollover()
    供主循环每轮主动检查,保证跨天必切(即使本轮无日志输出)。
- setup_logging 改为幂等:清掉已有 handler 再添加,避免重复日志行
  (此前 setup_logging 被多次调用会产生重复行,现已免疫)。
- main 循环每轮调用 maybe_rollover,跨天切换时记一行
  “日志已切换到新的一天,旧日志归档至 Archive/”。

验证:本地 4 项单测全绿(启动归档旧文件 / 当天写入 /
跨天主动滚动 / 同天不滚动)。
This commit is contained in:
Misaka_Company
2026-07-16 09:22:56 +08:00
parent 5e3c5d5b31
commit 0750f4c41d

View File

@@ -21,6 +21,74 @@ from . import source_watcher
logger = logging.getLogger(__name__)
class DailyFileHandler(logging.FileHandler):
"""按天滚动的日志 handler。
- 当天写入 ``<log_dir>/sync.log``(根目录只保留当天文件)。
- 跨天或进程启动时,旧的 ``sync.log`` 会被重命名为
``sync_YYYYMMDD.log`` 并移入 ``<log_dir>/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)