如何使用Python的TimedRotatingFileHandler强制进行轮换命名?

8

我正在尝试使用TimedRotatingFileHandler将日志记录在单独的日志文件中。 旋转按预期完美进行,但我不喜欢它处理文件命名的方式。

如果我将日志文件设置为my_log_file.log,则这将是“今天”的日志文件,在午夜更改日期时,它将被重命名为my_log_file.log.2014-07-08,而末尾没有.log扩展名,并且会创建新的my_log_file.log用于新的一天。

我想得到的是旧文件重命名为my_log_file.2014-07-08.log,甚至是my_log_file-2014-07-08.log,主要是带有.log作为结尾而不是放在中间。 此外,我希望“今天”的日志文件已经以当天的日期命名,就像旧文件一样。

有什么办法可以这样做吗?

我发现可以使用以下内容个性化后缀:

handler.suffix = "%Y-%m-%d"

但我不知道如何去除内部的 .log 部分,并强制当前日志文件添加后缀。

5个回答

6

我创建了一个名为ParallelTimedRotatingFileHandler的类,主要旨在允许多个进程并行写入日志文件。

这个类解决的与并行进程有关的问题包括:

  • 所有进程在同时尝试复制或重命名同一文件时的滚动瞬间会出现错误。
  • 解决此问题的方法正是您建议的命名约定。因此,对于您在处理程序中提供的文件名Service,日志记录不会进入例如Service.log,而是今天进入Service.2014-08-18.log,明天进入Service.2014-08-19.log
  • 另一种解决方案是以a(追加)模式打开文件,而不是w,以允许并行写入。
  • 删除备份文件也需要小心,因为多个并行进程正在同时删除同一文件。
  • 此实现不考虑闰秒(这对Unix不是问题)。在其他操作系统中,可能仍然是2018年6月30日23:59:60,在滚动瞬间,日期未更改,因此我们采用昨天相同的文件名。
  • 我知道标准Python建议不使用logging模块进行并行处理,而应使用SocketHandler,但在我的环境中,这个方法可行。

代码只是标准Python handlers.py模块中代码的微小变化。当然,版权属于版权持有人。

以下是代码:

import logging
import logging.handlers
import os
import time
import re

class ParallelTimedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler):
    def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, postfix = ".log"):

        self.origFileName = filename
        self.when = when.upper()
        self.interval = interval
        self.backupCount = backupCount
        self.utc = utc
        self.postfix = postfix

        if self.when == 'S':
            self.interval = 1 # one second
            self.suffix = "%Y-%m-%d_%H-%M-%S"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$"
        elif self.when == 'M':
            self.interval = 60 # one minute
            self.suffix = "%Y-%m-%d_%H-%M"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$"
        elif self.when == 'H':
            self.interval = 60 * 60 # one hour
            self.suffix = "%Y-%m-%d_%H"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}$"
        elif self.when == 'D' or self.when == 'MIDNIGHT':
            self.interval = 60 * 60 * 24 # one day
            self.suffix = "%Y-%m-%d"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
        elif self.when.startswith('W'):
            self.interval = 60 * 60 * 24 * 7 # one week
            if len(self.when) != 2:
                raise ValueError("You must specify a day for weekly rollover from 0 to 6 (0 is Monday): %s" % self.when)
            if self.when[1] < '0' or self.when[1] > '6':
                 raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
            self.dayOfWeek = int(self.when[1])
            self.suffix = "%Y-%m-%d"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
        else:
            raise ValueError("Invalid rollover interval specified: %s" % self.when)

        currenttime = int(time.time())
        logging.handlers.BaseRotatingHandler.__init__(self, self.calculateFileName(currenttime), 'a', encoding, delay)

        self.extMatch = re.compile(self.extMatch)
        self.interval = self.interval * interval # multiply by units requested

        self.rolloverAt = self.computeRollover(currenttime)

    def calculateFileName(self, currenttime):
        if self.utc:
             timeTuple = time.gmtime(currenttime)
        else:
             timeTuple = time.localtime(currenttime)

        return self.origFileName + "." + time.strftime(self.suffix, timeTuple) + self.postfix

    def getFilesToDelete(self, newFileName):
        dirName, fName = os.path.split(self.origFileName)
        dName, newFileName = os.path.split(newFileName)

        fileNames = os.listdir(dirName)
        result = []
        prefix = fName + "."
        postfix = self.postfix
        prelen = len(prefix)
        postlen = len(postfix)
        for fileName in fileNames:
            if fileName[:prelen] == prefix and fileName[-postlen:] == postfix and len(fileName)-postlen > prelen and fileName != newFileName:
                 suffix = fileName[prelen:len(fileName)-postlen]
                 if self.extMatch.match(suffix):
                     result.append(os.path.join(dirName, fileName))
        result.sort()
        if len(result) < self.backupCount:
            result = []
        else:
            result = result[:len(result) - self.backupCount]
        return result

     def doRollover(self):
         if self.stream:
            self.stream.close()
            self.stream = None

         currentTime = self.rolloverAt
         newFileName = self.calculateFileName(currentTime)
         newBaseFileName = os.path.abspath(newFileName)
         self.baseFilename = newBaseFileName
         self.mode = 'a'
         self.stream = self._open()

         if self.backupCount > 0:
             for s in self.getFilesToDelete(newFileName):
                 try:
                     os.remove(s)
                 except:
                     pass

         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:
             dstNow = time.localtime(currentTime)[-1]
             dstAtRollover = time.localtime(newRolloverAt)[-1]
             if dstNow != dstAtRollover:
                 if not dstNow:  # DST kicks in before next rollover, so we need to deduct an hour
                     newRolloverAt = newRolloverAt - 3600
                 else:           # DST bows out before next rollover, so we need to add an hour
                     newRolloverAt = newRolloverAt + 3600
         self.rolloverAt = newRolloverAt

我曾使用过这个方案,感觉它可以很好地解决我的问题,直到最终需要按照预期的方式运行。当我自己启动Python并让它跑过午夜时,文件能正确交换,并出现了一个新的文件。但是,我真正需要的是通过crontab启动Python,然后再启动另外几个由第一个Python启动的程序。当我不手动启动Python时,只有第一个文件被创建,在午夜时停止写日志。这是为什么呢?也许是Linux文件权限的问题吗? - Jester
在我的情况下,如果没有在__init__的第一行添加对基类构造函数的调用,则您的类无法正常工作:super().__init__(filename=filename, when=when, interval=interval, backupCount=backupCount, encoding=encoding, delay=delay, utc=utc) - Erwin Mayer

5
这里有一个简单的解决方案:向处理程序添加自定义名称函数。日志工具将调用您的名称函数来创建滚动文件的名称,如Jester在6年前指出的那样,格式为filename.log.YYYYMMDD,因此我们需要将“.log”部分移到末尾。
def namer(name):
    return name.replace(".log", "") + ".log"

在设置好处理程序后,只需将函数分配给其名称属性:

handler.namer = namer

这是我完整的日志初始化脚本,我刚接触Python,欢迎批评/建议:

import os
import logging
from logging.handlers import TimedRotatingFileHandler
from config import constants

def namer(name):
    return name.replace(".log", "") + ".log"

def init(baseFilename):
    logPath = constants.LOGGING_DIR
    envSuffix = '-prod' if constants.ENV == 'prod' else '-dev'
    logFilename = os.path.join(logPath, baseFilename + envSuffix + '.log')
    print(f"Logging to {logFilename}")

    handler = TimedRotatingFileHandler(logFilename,
    when = "midnight", 
    backupCount = 30,
    encoding = 'utf8')
    handler.setLevel(logging.DEBUG)
    handler.suffix = "%Y%m%d"
    handler.namer = namer # <-- Here's where I assign the custom namer.

    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s [%(module)s:%(lineno)d]')
    handler.setFormatter(formatter)

    logging.basicConfig(
        handlers = [handler],
        format = '%(asctime)s %(levelname)s %(message)s [%(module)s:%(lineno)d]',
        level = logging.DEBUG,
        datefmt = '%Y-%m-%d %H:%M:%S')


if __name__ == '__main__':
    init('testing')
    logging.error("ohai")
    logging.debug("ohai debug")
    logging.getLogger().handlers[0].doRollover()
    logging.error("ohai next day")
    logging.debug("ohai debug next day")

4
据我所知,目前没有直接实现这个的方法。
您可以尝试一种解决方案,即覆盖默认行为。
具体步骤如下:
1. 创建自己的TimedRotatingFileHandler类,并覆盖doRollover()函数。
2. 检查您Python安装目录下的源代码<PythonInstallDir>/Lib/logging/handlers.py。
代码示例:
class MyTimedRotatingFileHandler(TimedRotatingFileHandler):
    def __init__(self, **kwargs):
        TimedRotatingFileHandler.__init__(self, **kwargs)

    def doRollover(self):
        # Do your stuff, rename the file as you want 

我已经考虑过这个选项,但我希望有更简单的方法来完成它。如果在几周内没有更好的答复,我将把这个设为被接受的答案,但我不会使用它。 - Jester

0

我曾经有同样/类似的问题。最终我自己创建了一个类。我尽可能地利用了父类的方法,使代码保持简洁。但是我没有实现将当前日志文件名包含日期的命名。

from logging.handlers import TimedRotatingFileHandler


class ExtensionManagingTRFHandler(TimedRotatingFileHandler):
    def __init__(self, filename, extension='.log', **kwargs):
        if extension:
            if not extension.startswith('.'):
                # ensure extension starts with '.'
                extension = '.' + extension
            if not filename.endswith(extension):
                # make sure not to double the extension
                filename += extension
        super(ExtensionManagingTRFHandler, self).__init__(filename, **kwargs)
        self.extension = extension

    def rotation_filename(self, default_name):
        # remove the extension from the middle and append to end as the default behaviour adds a
        # date suffix after the extension
        result = default_name.replace(self.extension, '')
        result += self.extension
        # the default method applies the self.namer if namer is callable
        result = super(ExtensionManagingTRFHandler, self).rotation_filename(result)
        return result

    def getFilesToDelete(self):
        # this implementation is a bit of a hack in that it temporarily 
        # renames baseFilename and restores it
        slice_size = 0
        if self.baseFilename.endswith(self.extension):
            slice_size = len(self.extension)
            self.baseFilename = self.baseFilename[:-slice_size]
        # the default method still does the heavy lifting
        # this works because it already accounts for the possibility 
        # of a file extension after the dates
        result = super(ExtensionManagingTRFHandler, self).getFilesToDelete()
        if slice_size:
            self.baseFilename += self.extension
        return result

0

我使用了Python 3.7的https://dev59.com/I4Hba4cB1Zd3GeqPQFtQ#25387192解决方案,它是一个很好的解决方案。

但是对于'midnight' when参数和以'W'开头的when参数,它不起作用,因为在TimedRotatingFileHandler类中引入并使用了atTime参数。

要使用此解决方案,请使用以下__init__行:

def __init__(self, filename, when='h', interval=1, backupCount=0,
             encoding=None, delay=False, utc=False, atTime=None, postfix = ".log"):

同时将以下内容添加到__init__声明的内容中:

self.postfix = postfix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接