重写TimedRotatingFileHandler 以兼容多进程

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)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值