为什么 TimedRotatingFileHandler 不删除旧文件?

Myk*_*ych 4 python logging python-3.x

我正在使用TimedRotatingFileHandler创建我的日志。我希望我的日志文件每分钟创建一次,最多保留 2 个日志文件并删除旧的。这是示例代码:

import logging
import logging.handlers
import datetime

logger = logging.getLogger('MyLogger')
logger.setLevel(logging.DEBUG)

handler = logging.handlers.TimedRotatingFileHandler(
    "logs/{:%H-%M}.log".format(datetime.datetime.now()), 
    when="M", 
    backupCount=2)

logger.addHandler(handler)
logger.debug("PLEASE DELETE PREVIOUS FILES")
Run Code Online (Sandbox Code Playgroud)

如果我多次运行此代码(间隔一分钟),我会在我的日志目录中得到多个文件,如下所示:

21-01.log
21-02.log
21-03.log
...
Run Code Online (Sandbox Code Playgroud)

这对我来说似乎很奇怪,因为我设置了backupCount=2它表示最多应该保存 2 个文件,并且应该删除旧文件。但是,当我在日志文件夹中使用 2 个或更多文件启动我的应用程序时,不会删除旧文件。

为什么 TimedRotatingFileHandler 不删除旧文件?有什么办法可以设置 TimedRotatingFileHandler 来删除旧文件?

Mar*_*ers 6

您不能TimedRotatingFileHandler按照设计为您的用例使用 。处理程序期望“当前”日志文件名保持稳定,并将轮换定义为通过重命名. 这些是保留或删除的备份。轮换备份是根据基本文件名加上带有轮换时间戳的后缀创建的。因此,该实现区分了日志文件(存储在 中baseFilename)和轮换文件(在方法 中生成doRotate()) 。请注意,只有在轮换发生时才会删除备份,因此在处理程序至少使用了一个完整的时间间隔之后。

相反,您希望基本文件名本身携带时间信息,因此需要改变日志文件名本身。在这种情况下没有“备份”,您只需在轮换时刻打开一个新文件即可。此外,您似乎正在运行短暂的Python 代码,因此您希望立即删除较旧的文件,而不仅仅是在显式轮换时删除,而这可能永远无法达到。

这就是为什么TimedRotatingFileHandler不会删除任何文件,因为它永远无法创建备份文件。没有备份意味着没有要删除的备份。为了轮换文件,处理程序的当前实现期望负责文件名生成,并且不能期望知道它本身不会生成的文件名。"M"当您使用每分钟轮换频率配置它时,它被配置为将文件轮换为具有模式的备份文件{baseFileame}.{now:%Y-%m-%d_%H_%M},因此只会删除与该模式匹配的轮换备份文件。请参阅文档

系统将通过在文件名后附加扩展名来保存旧日志文件。扩展是基于日期和时间的,使用 strftime 格式%Y-%m-%d_%H-%M-%S或其前导部分,具体取决于翻转间隔。

相反,您想要的是一个本身带有时间戳的基本文件名,并且在打开具有不同名称的新日志文件时,旧的日志文件(而不是备份文件)将被删除。为此,您必须创建一个自定义处理程序。

幸运的是,类层次结构是专门为轻松定制而设计的。您可以BaseRotatingHandler在此处进行子类化,并提供您自己的删除逻辑:

import os
import time
from itertools import islice
from logging.handlers import BaseRotatingHandler, TimedRotatingFileHandler

# rotation intervals in seconds
_intervals = {
    "S": 1,
    "M": 60,
    "H": 60 * 60,
    "D": 60 * 60 * 24,
    "MIDNIGHT": 60 * 60 * 24,
    "W": 60 * 60 * 24 * 7,
}

class TimedPatternFileHandler(BaseRotatingHandler):
    """File handler that uses the current time in the log filename.

    The time is quantisized to a configured interval. See
    TimedRotatingFileHandler for the meaning of the when, interval, utc and
    atTime arguments.

    If backupCount is non-zero, then older filenames that match the base
    filename are deleted to only leave the backupCount most recent copies,
    whenever opening a new log file with a different name.

    """

    def __init__(
        self,
        filenamePattern,
        when="h",
        interval=1,
        backupCount=0,
        encoding=None,
        delay=False,
        utc=False,
        atTime=None,
    ):
        self.when = when.upper()
        self.backupCount = backupCount
        self.utc = utc
        self.atTime = atTime
        try:
            key = "W" if self.when.startswith("W") else self.when
            self.interval = _intervals[key]
        except KeyError:
            raise ValueError(
                f"Invalid rollover interval specified: {self.when}"
            ) from None
        if self.when.startswith("W"):
            if len(self.when) != 2:
                raise ValueError(
                    "You must specify a day for weekly rollover from 0 to 6 "
                    f"(0 is Monday): {self.when}"
                )
            if not "0" <= self.when[1] <= "6":
                raise ValueError(
                    f"Invalid day specified for weekly rollover: {self.when}"
                )
            self.dayOfWeek = int(self.when[1])

        self.interval = self.interval * interval
        self.pattern = os.path.abspath(os.fspath(filenamePattern))

        # determine best time to base our rollover times on
        # prefer the creation time of the most recently created log file.
        t = now = time.time()
        entry = next(self._matching_files(), None)
        if entry is not None:
            t = entry.stat().st_ctime
            while t + self.interval < now:
                t += self.interval

        self.rolloverAt = self.computeRollover(t)

        # delete older files on startup and not delaying
        if not delay and backupCount > 0:
            keep = backupCount
            if os.path.exists(self.baseFilename):
                keep += 1
                delete = islice(self._matching_files(), keep, None)
                for entry in delete:
                    os.remove(entry.path)

        # Will set self.baseFilename indirectly, and then may use
        # self.baseFilename to open. So by this point self.rolloverAt and
        # self.interval must be known.
        super().__init__(filenamePattern, "a", encoding, delay)

    @property
    def baseFilename(self):
        """Generate the 'current' filename to open"""
        # use the start of *this* interval, not the next
        t = self.rolloverAt - self.interval
        if self.utc:
            time_tuple = time.gmtime(t)
        else:
            time_tuple = time.localtime(t)
            dst = time.localtime(self.rolloverAt)[-1]
            if dst != time_tuple[-1] and self.interval > 3600:
                # DST switches between t and self.rolloverAt, adjust
                addend = 3600 if dst else -3600
                time_tuple = time.localtime(t + addend)
        return time.strftime(self.pattern, time_tuple)

    @baseFilename.setter
    def baseFilename(self, _):
        # assigned to by FileHandler, just ignore this as we use self.pattern
        # instead
        pass

    def _matching_files(self):
        """Generate DirEntry entries that match the filename pattern.

        The files are ordered by their last modification time, most recent
        files first.

        """
        matches = []
        pattern = self.pattern
        for entry in os.scandir(os.path.dirname(pattern)):
            if not entry.is_file():
                continue
            try:
                time.strptime(entry.path, pattern)
                matches.append(entry)
            except ValueError:
                continue
        matches.sort(key=lambda e: e.stat().st_mtime, reverse=True)
        return iter(matches)

    def doRollover(self):
        """Do a roll-over. This basically needs to open a new generated filename.
        """
        if self.stream:
            self.stream.close()
            self.stream = None

        if self.backupCount > 0:
            delete = islice(self._matching_files(), self.backupCount, None)
            for entry in delete:
                os.remove(entry.path)

        now = int(time.time())
        rollover = self.computeRollover(now)
        while rollover <= now:
            rollover += self.interval
        if not self.utc:
            # If DST changes and midnight or weekly rollover, adjust for this.
            if self.when == "MIDNIGHT" or self.when.startswith("W"):
                dst = time.localtime(now)[-1]
                if dst != time.localtime(rollover)[-1]:
                    rollover += 3600 if dst else -3600
        self.rolloverAt = rollover

        if not self.delay:
            self.stream = self._open()

    # borrow *some* TimedRotatingFileHandler methods
    computeRollover = TimedRotatingFileHandler.computeRollover
    shouldRollover = TimedRotatingFileHandler.shouldRollover
Run Code Online (Sandbox Code Playgroud)

将其与日志文件名中的time.strftime()占位符一起使用,这些将为您填充:

handler = TimedPatternFileHandler("logs/%H-%M.log", when="M", backupCount=2)
Run Code Online (Sandbox Code Playgroud)

请注意,这会在您创建实例时清除旧文件。


Bsq*_* ℬℬ 5

就像您在TimedRotatingFileHandler文档中看到的那样,您的日志文件名应该相同才能正确获得旋转系统。

在您的情况下,因为您自己附加了 dateTime 信息,所以每次的日志文件名都不同,因此您可以观察结果。

因此,在您的源代码中,您只需要修改日志文件名:

handler = logging.handlers.TimedRotatingFileHandler(
    "logs/MyLog", 
    when="M", 
    backupCount=2)
Run Code Online (Sandbox Code Playgroud)

如果你想挑战它,你可以把它改成when“S”(秒),然后检查旋转是否OK。

例如,它会自动生成这样的文件:

> MyLog
> MyLog.2019-07-08_11-36-53
> MyLog.2019-07-08_11-36-58
Run Code Online (Sandbox Code Playgroud)

如果您需要更多信息,请不要犹豫。