94 lines
2.9 KiB
Python
94 lines
2.9 KiB
Python
|
|
"""自定义日志 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
|