Files
medical_training/config/logging_handlers.py
T

94 lines
2.9 KiB
Python
Raw Normal View History

2026-05-29 15:58:00 +08:00
"""自定义日志 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