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
|