如何强制使用Python的TimedRotatingFileHandler进行文件轮换?
我正在尝试使用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 个回答
我也有过类似的问题。最后我自己创建了一个类。我尽量让这个类简单一些,尽量使用父类的方法。不过,我没有实现把当前日志文件的名字和日期结合起来的功能。
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
我用了这个解决方案 https://stackoverflow.com/a/25387192/6619512,在Python 3.7上效果很好。
但是对于'午夜'的 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
据我所知,直接实现这个功能的方法是没有的。
你可以尝试一种解决方案,就是覆盖默认的行为。
- 创建你自己的
TimedRotatingFileHandler class
,并重写doRollover() function.
- 查看你电脑上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大约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")
我创建了一个叫做 ParallelTimedRotatingFileHandler 的类,主要是为了让多个进程可以同时写入同一个日志文件。
这个类解决了并行进程的一些问题,包括:
- 当所有进程同时尝试复制或重命名同一个文件时,会出现错误,这就是所谓的“轮换时刻”。
- 解决这个问题的方法正是你提到的命名规则。比如,如果你在处理程序中提供了一个文件名
Service
,那么日志不会写入Service.log
,而是今天写入Service.2014-08-18.log
,明天写入Service.2014-08-19.log
。 - 另一个解决方案是以
a
(追加)模式打开文件,而不是w
(写入)模式,这样可以允许并行写入。 - 删除备份文件时也需要小心,因为多个并行进程可能会同时删除同一个文件。
- 这个实现没有考虑闰秒(对 Unix 系统来说这不是问题)。在其他操作系统中,轮换时刻可能仍然是 2008 年 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