Python logging.handlers模块,RotatingFileHandler、TimedRotatingFileHandler 处理器各参数详细介绍

在python的logging.handlers模块中,提供了很多有用的日志处理程序对象,其中比较常用的有:RotatingFileHandlerTimedRotatingFileHandler。这两个处理程序主要是用来对输出的日志文件进行切割。

一、RotatingFileHandler 介绍

RotatingFileHandler 是 Python 自带的日志处理器之一,用于将日志写入到指定的文件中,并控制文件大小和数量,可实现日志轮转(即备份旧日志并创建新的日志)。

class logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)

RotatingFileHandler 的各参数解释:

  1. filename:文件名,指定日志文件的路径和名称;
  2. mode:模式,可选值为 ‘a’ 或 ‘w’。当为 ‘a’ 时,表示以追加方式写入日志;当为 ‘w’ 时,表示以覆盖方式写入日志。默认值为 ‘a’;
  3. maxBytes:单个日志文件的最大大小,单位为字节。当日志文件达到该大小时,会自动备份旧日志并创建新的日志文件。默认值为 0,表示不限制日志文件大小。;
  4. backupCount:备份文件数目。当生成的日志文件数量超过该数目时,会自动删除旧的备份日志文件。默认值为 0,表示不备份;
  5. encoding:日志文件的编码格式,默认为 None,表示使用系统默认编码;
  6. delay:是否延时打开文件,可选值为 True 和 False。当为 True 时,表示延时打开文件,即在第一次写入日志时才打开日志文件。当为 False 时,表示在初始化时即打开日志文件。默认值为 False

默认情况下,文件无限增长。您可以指定maxBytesbackupCount的特定值,以允许文件以预定的大小滚动。

需要注意的是,如果指定了为maxBytes指定了>0的值,则mode指定为w是没有意义的,RotatingFileHandler初始化时会自动将mode强制改为a

示例1:

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger('mylogger')
logger.setLevel(logging.INFO)

handler = RotatingFileHandler(filename='test.log', maxBytes=6, backupCount=3)

logger.addHandler(handler)

for i in range(10):
    logger.info("Hello%d" % i)

输出4个日志文件,文件名及文件内容分别如下:

# test.log
Hello9

# test.log.1
Hello8

# test.log.2
Hello7

# test.log.3
Hello6

解释:

  • 文件最新内容始终输出在test.log基本日志文件中;
  • test.log大小达到设定的maxBytes时,如果再有日志写入,则新生成一个test.log,并将原来的test.log重命名为test.log.1,将原来的test.log.1(如果有)重命名为test.log.2,将原来的test.log.2(如果有)重命名为test.log.3,将原来的test.log.3(如果有)删除(因为只备份3个)

二、TimedRotatingFileHandler 介绍

TimedRotatingFileHandler 是 Python 自带的日志处理器之一,与 RotatingFileHandler 类似,也用于将日志写入到指定的文件中,并对文件进行切割。不同的是TimedRotatingFileHandler 可以按照时间轮转日志,例如每天或每小时生成一个新的日志文件。

class logging.handlers.TimedRotatingFileHandler(filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None)

TimedRotatingFileHandler 的各参数的详解:

  1. filename:文件名,指定日志文件的路径和名称;
  2. when:日志轮转的时间间隔,可选值为 ‘S’、‘M’、‘H’、‘D’、‘W’ 和 ‘midnight’,分别表示秒、分、时、天、周和每天的午夜;默认值为 ‘midnight’,即每天的午夜轮转,值不区分大小写;
  3. interval:时间间隔的数量,默认为 1;例如,当 when=‘D’ 且 interval=7 时,表示每周轮转一次;
  4. backupCount:备份文件数目;当生成的日志文件数量超过该数目时,会自动删除旧的备份日志文件;默认值为 0,表示不备份;
  5. encoding:日志文件的编码格式,默认为 None,表示使用系统默认编码;
  6. delay:是否延时打开文件,可选值为 True 和 False;当为 True 时,表示延时打开文件,即在第一次写入日志时才打开日志文件;当为 False 时,表示在初始化时即打开日志文件;默认值为 False;
  7. utc:是否使用 UTC 时间,默认为 False,表示使用本地时间
  8. atTime:用来设置轮转时间,格式为 ‘%H:%M:%S’,默认为午夜 12 点;需要注意的是该参数仅在whenW/midnight时有效;

当使用基于星期的轮换时,星期一为 ‘W0’,星期二为 ‘W1’,以此类推直至星期日为 ‘W6’。 在这种情况下,传入的 interval 值不会被使用。

示例1: 日志文件备份规则

import logging
from logging.handlers import TimedRotatingFileHandler
import time

logger = logging.getLogger('mylogger')
logger.setLevel(logging.INFO)

handler = TimedRotatingFileHandler(
    filename="./test.log",
    when="S",
    interval=3,
    backupCount=5,
    encoding="utf8"
)

ffmt = logging.Formatter("%(asctime)s - %(message)s")
handler.setFormatter(ffmt)

logger.addHandler(handler)

for i in range(10):
    logger.info("Hello%d" % i)
    logger.info(handler.suffix)
    logger.info(handler.extMatch)
    time.sleep(3)

输出文件:
示例1生成的日志文件

部分文件内容:

# test.log
2023-08-17 22:06:45,635 - Hello9
2023-08-17 22:06:45,635 - %Y-%m-%d_%H-%M-%S
2023-08-17 22:06:45,635 - re.compile('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$', re.ASCII)

# test.log.2023-08-17_22-06-30
2023-08-17 22:06:30,615 - Hello4
2023-08-17 22:06:30,615 - %Y-%m-%d_%H-%M-%S
2023-08-17 22:06:30,615 - re.compile('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$', re.ASCII)

解释:

  • 代码中,when='S'interval=3,表示每3s备份一次文件,backupCount=5,表示最多保留5份旧日志数据;
  • 从输出结果来看,每3s备份一次数据,只保留5份旧的数据,符合预期;
  • 另外,按秒切割的日志文件,备份文件是在原日志文件名后添加.%Y-%m-%d_%H-%M-%S的后缀,而删除旧日志文件使用的正则表达式为re.compile('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$', re.ASCII)

示例2: 轮换时间的计算则仅在轮换发生时执行

还是示例1的代码,这里做一个小改动,将最后一行的time.sleep(3)改为`time.sleep(2)

将已有日志文件全删除,再次运行代码。

输出文件:
示例2生成的日志文件

部分文件内容:

# test.log
2023-08-17 22:24:16,973 - Hello8
2023-08-17 22:24:16,973 - %Y-%m-%d_%H-%M-%S
2023-08-17 22:24:16,973 - re.compile('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$', re.ASCII)
2023-08-17 22:24:18,984 - Hello9
2023-08-17 22:24:18,984 - %Y-%m-%d_%H-%M-%S
2023-08-17 22:24:18,984 - re.compile('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$', re.ASCII)

# test.log.2023-08-17_22-24-12
2023-08-17 22:24:12,957 - Hello6
2023-08-17 22:24:12,957 - %Y-%m-%d_%H-%M-%S
2023-08-17 22:24:12,957 - re.compile('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$', re.ASCII)
2023-08-17 22:24:14,960 - Hello7
2023-08-17 22:24:14,960 - %Y-%m-%d_%H-%M-%S
2023-08-17 22:24:14,960 - re.compile('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$', re.ASCII)

解释:

  • 示例1结果不同的是,备份的日志文件名后添加的后缀时间间隔是4,不是interval=3设置的间隔。这是因为:初始轮换时间的计算是在处理程序被初始化时执行的。 后续轮换时间的计算则仅在轮换发生时执行,而只有当提交输出时轮换才会发生。

    例如,如果设置时间间隔为“每分钟”,那并不意味着你总会看到(文件名中)带有间隔一分钟时间的日志文件;如果在应用程序执行期间,日志记录输出的生成频率高于每分钟一次,那么你可以预期看到间隔一分钟时间的日志文件。 但如果日志记录消息每五分钟才输出一次,那么你将看到的是间隔五分钟时间的日志文件。

示例3: 自定义备份文件的后缀

import logging
from logging.handlers import TimedRotatingFileHandler
import time
import re

logger = logging.getLogger('mylogger')
logger.setLevel(logging.INFO)

handler = TimedRotatingFileHandler(
    filename="./test.log",
    when="S",
    interval=3,
    backupCount=5,
    encoding="utf8"
)

ffmt = logging.Formatter("%(asctime)s - %(message)s")
handler.setFormatter(ffmt)

handler.suffix = "%Y%m%d_%H%M%S"+".log"
extMatch = r"^d{4}d{2}d{2}_d{2}d{2}d{2}(.w+)?$"
handler.extMatch = re.compile(extMatch, re.ASCII)

logger.addHandler(handler)

for i in range(10):
    logger.info("Hello%d" % i)
    time.sleep(5)

文件输出:
示例3生成的日志文件

解释:

  • 使用 handler.suffix = "%Y%m%d_%H%M%S"+".log" 自定义备份文件的后缀,与此同时使用extMatch = r"^d{4}d{2}d{2}_d{2}d{2}d{2}(.w+)?$"handler.extMatch = re.compile(extMatch, re.ASCII)重新定义了删除旧备份文件使用的正则表达式,以保证新后缀的旧的备份文件能正常按备份数量进行删除。

示例4:
由于when='M''H''W0'~'W6'midnight 时,测试起来不太方便,我这里只解释规则,部分给出关键代码。

# when='M'时,suffix、extMatch默认值为
handler.suffix = "%Y-%m-%d_%H-%M"
extMatch = r"^d{4}-d{2}-d{2}_d{2}-d{2}(.w+)?$"
handler.extMatch = re.compile(self.extMatch, re.ASCII)

# when='H'时,suffix、extMatch默认值为
handler.suffix = "%Y-%m-%d_%H"
extMatch = r"^d{4}-d{2}-d{2}_d{2}(.w+)?$"
handler.extMatch = re.compile(self.extMatch, re.ASCII)

# when='W'时,suffix、extMatch默认值为
handler.suffix = "%Y-%m-%d"
extMatch = r"^d{4}-d{2}-d{2}(.w+)?$"
handler.extMatch = re.compile(self.extMatch, re.ASCII)

# when='midnight'时,suffix、extMatch默认值为
handler.suffix = "%Y-%m-%d"
extMatch = r"^d{4}-d{2}-d{2}(.w+)?$"
handler.extMatch = re.compile(self.extMatch, re.ASCII)
  • when='M'interval=1时,表示日志按分钟进行切割,但需要注意的是下次轮换时间是:处理器初始化时间+60s,比如:日志处理器初始化是在10:00:25,那么下次备份文件的生成是在10:01:25,而不是10:01:00(当然前提是10:01:25这个时间有日志提交,如果这个时间没日志提交,那么备份文件的生成时间为这个时间之后日志首次提交的时间)
  • when='M'interval=1时,表示日志按每小时进行切割,但同样需要注意下次轮换时间是:处理器初始化时间+60*60s,比如:日志处理器初始化是在10:20:30,那么下次备份文件生成是在11:20:30(当然前提是11:20:30这个时间有日志提交,如果这个时间没日志提交,那么备份文件的生成时间为这个时间之后日志首次提交的时间)
  • when='w2'时,如果不指定atTime,则意思为每周三,午夜12点进行日志轮转,即 周四 00:00:00 进行日志轮转;
  • when='w3'时,如果指定了atTime,如 atTime=datetime.time(22,00,00),表示每周四,晚上22:00:00 进行日志轮转;
from logging.handlers import TimedRotatingFileHandler
from datetime import time

# 每周五,早上 02:00:00 进行日志轮换
handler = TimedRotatingFileHandler(filename, when='w4', atTime=time(02, 20, 00))
  • when='midnight'时,如果未指定atTime,则意思为每天午夜12点进行日志轮转;
  • when='midnight'时,如果指定了atTime,如atTime=datetime.time(23,00,00),则意思为每天晚上23:00:00,进行日志轮转;
from logging.handlers import TimedRotatingFileHandler
from datetime import time

# 每天,早上 10:00:00 进行日志轮换
handler = TimedRotatingFileHandler(filename, when='midnight', atTime=time(10, 00, 00))


参考资料:
logging.handlers — 日志处理