TimedRotatingFileHandler有一个缺点就是没有办法支持多进程的日志切换,多进程进行日志切换的时候可能会因为重命名而丢失日志数据
原因在于它的日志并不是直接写在 baseFilename.2022-02-02.log 上,而是先写到 baseFilename上,到了凌晨零点时,把baseFilename 改名为 baseFilename.2022-02-02.log 。然后新建一个baseFilename文件 。
(如果改名时baseFilename.2022-02-02.log已存在,则把原baseFilename.2022-02-02.log删除,再把baseFilename 改名为 baseFilename.2022-02-02.log)
dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
if os.path.exists(dfn):
os.remove(dfn)
if os.path.exists(self.baseFilename):
os.rename(self.baseFilename, dfn)
如果要解决多进程重复删除baseFilename.2022-02-02.log的问题,比较容易想到的两个思路:
方案一:日志一开始就直接写在baseFilename.2022-02-02.log上
方案二:改名时如果baseFilename.2022-02-02.log已存在,则认为其他进程已做了切换操作,当前进程不需重复操作。
其他方案参考:https://blog.csdn.net/ling620/article/details/103862183
# 代码转自:https://blog.csdn.net/dustless927/article/details/122061953
# 另有简易版(无删除过期日志功能) https://my.oschina.net/lionets/blog/796438
import codecs
import os
import re
import sys
import logging
import time
from pathlib import Path
from logging.handlers import BaseRotatingHandler
class DailyRotatingFileHandler(BaseRotatingHandler):
"""
同`logging.TimedRotatingFileHandler`类似,不过这个handler:
- 可以支持多进程
- 只支持自然日分割
- 暂不支持UTC
"""
def __init__(self, filename, backupCount=0, encoding=None, delay=False, utc=False, **kwargs):
self.backup_count = backupCount
self.utc = utc
self.suffix = "%Y-%m-%d"
self.base_log_path = Path(filename)
self.base_filename = self.base_log_path.name
self.current_filename = self._compute_fn()
self.current_log_path = self.base_log_path.with_name(self.current_filename)
BaseRotatingHandler.__init__(self, filename, 'a', encoding, delay)
def shouldRollover(self, record):
"""
判断是否该滚动日志,如果当前时间对应的日志文件名与当前打开的日志文件名不一致,则需要滚动日志
"""
if self.current_filename != self._compute_fn():
return True
return False
def doRollover(self):
"""
滚动日志
"""
# 关闭旧的日志文件
if self.stream:
self.stream.close()
self.stream = None
# 计算新的日志文件
self.current_filename = self._compute_fn()
self.current_log_path = self.base_log_path.with_name(self.current_filename)
# 打开新的日志文件
if not self.delay:
self.stream = self._open()
# 删除过期日志
self.delete_expired_files()
def _compute_fn(self):
"""
计算当前时间对应的日志文件名
"""
return self.base_filename + "." + time.strftime(self.suffix, time.localtime())
def _open(self):
"""
打开新的日志文件,同时更新base_filename指向的软链,修改软链不会对日志记录产生任何影响
"""
if self.encoding is None:
stream = open(str(self.current_log_path), self.mode)
else:
stream = codecs.open(str(self.current_log_path), self.mode, self.encoding)
# 删除旧的软链
if self.base_log_path.exists():
try:
# 如果base_log_path不是软链或者指向的日志文件不对,则先删除该软链
if not self.base_log_path.is_symlink() or os.readlink(self.base_log_path) != self.current_filename:
os.remove(self.base_log_path)
except OSError:
pass
# 建立新的软链
try:
os.symlink(self.current_filename, str(self.base_log_path))
except OSError:
pass
return stream
def delete_expired_files(self):
"""
删除过期的日志
"""
if self.backup_count <= 0:
return
file_names = os.listdir(str(self.base_log_path.parent))
result = []
prefix = self.base_filename + "."
plen = len(prefix)
for file_name in file_names:
if file_name[:plen] == prefix:
suffix = file_name[plen:]
if re.match(r"^\d{4}-\d{2}-\d{2}(\.\w+)?$", suffix):
result.append(file_name)
if len(result) < self.backup_count:
result = []
else:
result.sort()
result = result[:len(result) - self.backup_count]
for file_name in result:
os.remove(str(self.base_log_path.with_name(file_name)))
方案二的实现:(windows要另外处理fcntl)
参考:https://blog.csdn.net/ling620/article/details/103862183
#代码转自https://www.cnblogs.com/piperck/p/9837637.html
class MultiCompatibleTimedRotatingFileHandler(TimedRotatingFileHandler):
def doRollover(self):
if self.stream:
self.stream.close()
self.stream = None
# get the time that this sequence started at and make it a TimeTuple
currentTime = int(time.time())
dstNow = time.localtime(currentTime)[-1]
t = self.rolloverAt - self.interval
if self.utc:
timeTuple = time.gmtime(t)
else:
timeTuple = time.localtime(t)
dstThen = timeTuple[-1]
if dstNow != dstThen:
if dstNow:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
# 兼容多进程并发 LOG_ROTATE
if not os.path.exists(dfn):
f = open(self.baseFilename, 'a')
fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
if os.path.exists(self.baseFilename):
os.rename(self.baseFilename, dfn)
if self.backupCount > 0:
for s in self.getFilesToDelete():
os.remove(s)
if not self.delay:
self.stream = self._open()
newRolloverAt = self.computeRollover(currentTime)
while newRolloverAt <= currentTime:
newRolloverAt = newRolloverAt + self.interval
# If DST changes and midnight or weekly rollover, adjust for this.
if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
self.rolloverAt = newRolloverAt
windows下实现fcntl函数功能
复制以下代码段,保存为fcntlock.py文件,将其放到引用的目录下,通过import fcntlock as fcntl
引入模块即可
# 代码转自https://blog.csdn.net/weixin_42254735/article/details/108531358
import os
import win32con
import pywintypes
import win32file
LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
LOCK_SH = 0 # The default value
LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
__overlapped = pywintypes.OVERLAPPED()
def lock(file, flags):
hfile = win32file._get_osfhandle(file.fileno())
win32file.LockFileEx(hfile, flags, 0, 0xffff0000, __overlapped)
def unlock(file):
hfile = win32file._get_osfhandle(file.fileno())
win32file.UnlockFileEx(hfile, 0, 0xffff0000, __overlapped)