"""自定义日志 Handler:按日期分文件,兼容 Windows 文件锁。 替代 TimedRotatingFileHandler,避免 Windows 上因 rename 操作遇到文件锁 (PermissionError: [WinError 32])导致日志丢失的问题。 文件命名规则:{prefix}-YYYY-MM-DD.log 日期切换时自动打开新文件,无需 rename 旧文件。 """ import logging import time from pathlib import Path class DailyFileHandler(logging.Handler): """按日期自动分文件的日志 Handler。 与 TimedRotatingFileHandler 的关键区别: - 文件直接以日期命名,日期切换时打开新文件,**不 rename 旧文件** - 多进程(dev server + 测试 / 管理命令)可同时写入,互不阻塞 - 自动清理超过 backup_count 天的旧文件 dictConfig 用法:: 'audit_file': { 'class': 'config.logging_handlers.DailyFileHandler', 'dir_path': '/path/to/logs', 'prefix': 'audit', 'backup_count': 30, 'formatter': 'verbose', } """ def __init__(self, dir_path, prefix='audit', backup_count=30, encoding='utf-8'): super().__init__() self.dir_path = Path(dir_path) self.dir_path.mkdir(parents=True, exist_ok=True) self.prefix = prefix self.backup_count = backup_count self.encoding = encoding self._current_date = None self._stream = None self._open_today() def _today(self): return time.strftime('%Y-%m-%d') def _open_today(self): """打开当天的日志文件(追加模式)。""" if self._stream: try: self._stream.close() except OSError: pass self._current_date = self._today() filepath = self.dir_path / f'{self.prefix}-{self._current_date}.log' self._stream = open(filepath, 'a', encoding=self.encoding) def emit(self, record): try: today = self._today() if today != self._current_date: self._open_today() self._cleanup_old_files() msg = self.format(record) self._stream.write(msg + '\n') self._stream.flush() except Exception: self.handleError(record) def close(self): self.acquire() try: if self._stream: try: self._stream.close() except OSError: pass self._stream = None finally: self.release() super().close() def _cleanup_old_files(self): """删除超过 backup_count 天的旧日志文件。""" if self.backup_count <= 0: return try: files = sorted(self.dir_path.glob(f'{self.prefix}-*.log')) for f in files[:-self.backup_count]: f.unlink(missing_ok=True) except OSError: pass