你无法使用
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
_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))
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)
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)
super().__init__(filenamePattern, "a", encoding, delay)
@property
def baseFilename(self):
"""Generate the 'current' filename to open"""
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:
addend = 3600 if dst else -3600
time_tuple = time.localtime(t + addend)
return time.strftime(self.pattern, time_tuple)
@baseFilename.setter
def baseFilename(self, _):
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 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()
computeRollover = TimedRotatingFileHandler.computeRollover
shouldRollover = TimedRotatingFileHandler.shouldRollover
在日志文件名中使用time.strftime()
占位符,系统会自动替换为相应内容:
handler = TimedPatternFileHandler("logs/%H-%M.log", when="M", backupCount=2)
请注意,这将在您创建实例时清理旧文件。
_install_handlers
函数没有读取那个backupCount
部分,因此构造函数始终为TimedRotatingFileHandler
的backupCount=0
。似乎是个 bug。问题是如何解决这个问题,而不必等待主分支上的问题。 - eranTimedRotatingFileHandler('my.log', when='s', backupCount=2)
每秒钟会旋转一次日志,因此您将获得my.log
和最后两个备份(文件名为my.log.YYYY-MM-DD_HH-MM-SS
)。OP 的代码每次都创建具有新名称的日志文件,因此不会发生滚动,并且备份计数永远不会达到。 - hoefling.ini
文件解析器中存在一个错误。它不会加载backupCount
参数,因此TimedRotatingFileHandler
的构造函数得到backupCount=0。当我进行手术旁路时,旋转删除起作用了。我正在寻找一个非原始包补丁解决方案。 - eran