阅读 499

Python 日志模块logging分析及使用-2

一、小总结二、Logger对象的日志等级三、使用多个处理器和多种格式化四、日志回滚1. RotatingFileHandler2. TimedRotatingFileHandler五、RotatingHandler存在的问题

本文作为Python日志模块的补充,主要介绍日志回滚RotatingFileHandlerTimedRotatingFileHandler的使用,以及其所带来的问题、Logger对象的日志等级是如何其作用的等内容。

一、小总结

通过前面介绍logging模块的博文【Python】—日志模块logging使用详解_ling620的专栏-CSDN博客,基本上可以正确使用日志模块。需要注意的几点如下:

  1. 直接使用logging模块的接口函数,无任何其他操作,如logging.info()来进行日志的输出是默认输出到控制台,默认的日志等级是logging.WARNING,且不可以设置日志等级。
  2. 使用logging模块的接口函数,内部实现是:判断root.handlers是否为空,为空则内部调用basicConfig()函数,默认创建StreamHandler
  3. 使用logging.basicConfig()函数可以满足基本使用,可以输出到文件或控制台中。内部是根据参数来创建FileHandlerStreamHandler来实现。
  4. Logger不可以直接实例化,需要使用logging.getLogger()获取Logger对象。
  5. 一个logger对象可以添加多个handler对象,通过addHandler()函数来添加。
  6. 每个handler对象可以有一个Formatter对象来指定格式,通过setFormatter()函数来设置。
  7. handlerlogger对象都需要设置一个日志等级,通过setLevel()函数来设置。
  8. logger的名称是一个以'.' 分割的层级结构,每个'.'后面的logger都是'.'前面的loggerchildren
  9. 不必为一个应用程序中所使用的所有loggers定义和配置handlers,只需要为一个顶层的logger配置handlers,然后按照需要创建child loggers就足够。
  10. logger有一个 "有效等级(effective level)" 的概念如果一个logger上没有被明确设置level,那么该logger就是使用它parentlevel;如果它的parent也没有明确设置level则继续向上查找parentparent的有效level,依次类推,直到找到个一个明确设置了level的祖先为止。

二、Logger对象的日志等级

由前文已知,Logger不可以直接实例化,需要使用logging.getLogger(name)来获取Logger的对象。通过setLevel()来设置日志等级。

在测试过程中,发现了如下问题,设置了日志等级为logging.DEBUG,输出Logger对象的level属性,得到的结果是10,但仍然不输出DEBUG等级的信息,这是为什么呢?如下:

 1>>> import logging
2
3>>> logger = logging.getLogger('example')
4>>> logger.level
50
6>>> logger.debug('this is a debug msg.')# 无输出
7>>> logger.warning('this is a warning msg.')
8this is a warning msg.
9>>> logger.setLevel(logging.DEBUG)
10>>> logger.level
1110
12>>> logger.debug('this is a debug msg.'# 无输出
13>>> logger.warning('this is a warning msg.')
14this is a warning msg.
复制代码

为什么设置了日志等级而没有起作用呢?-_-
首先分析一下,上面的代码中获取了Logger的对象logger,但是并没有添加任何handler对象。当logging.getLogger()做了什么呢?接口函数又做了哪些?

 1def getLogger(name=None):
2    """
3    Return a logger with the specified name, creating it if necessary.
4    If no name is specified, return the root logger.
5    """

6    if name:
7        return Logger.manager.getLogger(name)
8    else:
9        return root # 若未指定name,则返回root
10
11# root是什么?
12root = RootLogger(WARNING) # RootLogger的对象,日志等级为WARNING
13Logger.root = root
14Logger.manager = Manager(Logger.root)
复制代码

getLogger()函数内部根据是否指定name返回对应的root logger。即Logger的初始化对象,handler,filter参数等都为None。可见默认的日志等级是WARNING

logger.info()接口函数为例,看看又做了些什么?

函数调用关系:

接口函数调用关系
接口函数调用关系

分析下Logger.callHandlers()函数:

1def callHandlers(self, record):
2    if (found == 0):
3        if lastResort:
4            if record.levelno >= lastResort.level:
5                lastResort.handle(record)
6        elif raiseExceptions and not self.manager.emittedNoHandlerWarning:
7            sys.stderr.write("No handlers could be found for logger"
8                                 " \"%s\"\n" % self.name)
9            self.manager.emittedNoHandlerWarning = True
复制代码
  1. 循环Logger自己实例,直到获取到其祖辈,found来计数其handlers
  2. 判断found个数,如果为0,判断lastResort

这个lastResort是什么?如下:
它是_StderrHandler(WARNING)类的初始化对象,且默认传递的日志等级是WARNING,无法指定。
_StderrHandler类继承自StreamHandler,使用sys.stderr类似于StreamHandler

 1_defaultLastResort = _StderrHandler(WARNING) # 注意此处是WARNING
2lastResort = _defaultLastResort
3
4class _StderrHandler(StreamHandler):
5    """
6    This class is like a StreamHandler using sys.stderr, but always uses
7    whatever sys.stderr is currently set to rather than the value of
8    sys.stderr at handler construction time.
9    """

10    def __init__(self, level=NOTSET):
11        """
12        Initialize the handler.
13        """

14        Handler.__init__(self, level)
15
16    @property
17    def stream(self):
18        return sys.stderr
复制代码

看到这里已经明白了,之所以在获取Logger对象,设置日志等级后,依然没有生效的原因是,我们没有添加任何handler,程序内部默认调用指定等级为WARNING_StderrHandler,且该日志等级无法修改(使用setLevel()不影响该handler对象的等级)

当我们手动添加了handler对象后,则会调用添加的handler对象的等级或者root Logger的等级。

当调用logging.basicConfig()函数时,内部默认创建了FileHandlerStreamHandler对象,则我们再设置setLevel()可生效。如下:

1import loging
2logging.basicConfig(level=logging.DEBUG)
3logger = logging.getLogger()
4logger.debug('this is a debug msg.')
5# 输出
6INFO:root:this is a debug msg.
复制代码

三、使用多个处理器和多种格式化

日志记录器是普通的Python对象。addHandler()方法没有限制可以添加的日志处理器数量。有时候,应用程序需要将严重类的消息记录在一个文本文件,而将错误类或其他等级的消息输出在控制台中。要进行这样的设定,只需多配置几个日志处理器即可,在应用程序代码中的日志记录调用可以保持不变。

 1import logging
2
3logger = logging.getLogger('simple_example')
4logger.setLevel(logging.DEBUG)
5# 创建文件handler ,其等级为debug
6fh = logging.FileHandler('example.log')
7fh.setLevel(logging.DEBUG)
8# 创建控制台handler,日志等级为ERROR
9ch = logging.StreamHandler()
10ch.setLevel(logging.ERROR)
11# 创建formatter并添加至handlers
12formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
13ch.setFormatter(formatter)
14fh.setFormatter(formatter)
15# 将handlers添加至logger
16logger.addHandler(ch)
17logger.addHandler(fh)
18
19# 应用代码
20logger.debug('debug message')
21logger.info('info message')
22logger.warning('warn message')
23logger.error('error message')
24logger.critical('critical message')
复制代码

上述代码的结果是,控制台只输出errorcritical的日志信息,而文件中则包含所有5个日志信息。

四、日志回滚

通过前面的分析,我们可以将日志信息输出到一个文件中,随着时间的流逝,日志文件会变得越来越大,如何处理这种情况?

我们希望当日志文件不断记录增长至一定大小或增长到一定时间时,打开一个新的文件接着记录。你可能希望只保留一定数量的日志文件,当不断的创建文件到达该数量时,又覆盖掉最开始的文件形成循环。 对于这种使用场景,日志包提供了 logging.hanlders.RotatingFileHandlerlogging.hanlders.TimedRotatingFileHandler

在上篇文章中讲到过:

描述
logging.handlers.RotatingFileHandler 将日志消息发送到磁盘文件,并支持日志文件按大小切割
logging.hanlders.TimedRotatingFileHandler 将日志消息发送到磁盘文件,并支持日志文件按时间切割

1. RotatingFileHandler

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

当当前日志文件的长度接近maxBytes时,就会发生翻转。如果backupCount为>= 1,则系统将连续创建与基本文件路径名相同、但具有扩展名的新文件".1"".2"等附于其后。

例如,如果backupCount5,并且基本文件名为“app.log”,则会得到“app.log”“app.log.1”“app.log.2”,…,“app.log.5”

被写入的文件总是“app.log”

当它被填满时,它被关闭并重命名为“app.log.1”,如果文件“app.log.1”“app.log.2”等存在,然后将它们重命名为“app.log.2”“app.log.3”等。

  • 如果maxBytes为零,则不会发生翻转。
  • 如果想要翻转功能,则mode='a'

初始化函数定义:

1__init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)
复制代码

初始化参数:

参数名 含义
filename 文件名
mode 文件模式。使用回滚功能时,设置为a
maxBytes 文件大小,最大比特数,如1024*1024*1024表示一个G
backupCount 文件回滚个数,如设为3,则会保留3个备份文件,一共4个日志文件
encoding 文件编码格式,如果包含中文,则使用utf-8编码
delay 构建Handler,用来设置等级,格式等项。若为True,构建Handler对象,否则构建StreamHandler对象。默认False

2. TimedRotatingFileHandler

参数when决定了时间间隔的类型,参数interval决定了多少的时间间隔。如when=‘D’,interval=2,就是指两天的时间间隔,backupCount决定了能留几个日志文件。超过数量就会丢弃掉老的日志文件。

初始化函数定义

1__init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None)
复制代码

初始化参数:

参数名 含义
filename 文件名
when 时间间隔的类型
interval 时间间隔,
backupCount 文件回滚个数,如设为3,则会保留3个备份文件,一共4个日志文件
encoding 文件编码格式,如果包含中文,则使用utf-8编码
delay 构建Handler,用来设置等级,格式等项。若为True,构建Handler对象,否则构建StreamHandler对象。默认False
utc UTC时区的时间

参数when:

符号 含义
S
M 分钟
H 小时
D
W
W0-W6 周一到周日
midnight 在午夜,即每天凌晨

示例:

 1import glob
2import logging
3import logging.handlers
4
5my_logger = logging.getLogger('MyLogger')
6my_logger.setLevel(logging.DEBUG)
7
8# Add the log message handler to the logger
9handler = logging.handlers.RotatingFileHandler(
10              'logs/logging_demo.log', maxBytes=1024*1024, backupCount=5)
11
12my_logger.addHandler(handler)
复制代码

得到的日志文件如下,共6个文件:

1logging_demo.log
2logging_demo.log.1
3logging_demo.log.2
4logging_demo.log.3
5logging_demo.log.4
6logging_demo.log.5
复制代码

五、RotatingHandler存在的问题

Python 的logging模块提供了两个支持日志回滚的FileHandler类,分别是RotatingFileHandlerTimedRotatingFileHandler

logging是线程安全的,将单个进程中的多个线程日志记录至单个文件没有问题。但当有多个进程向同一个日志文件写入日志的时候,这两个RotatingHandler就会带来问题,比如日志丢失。

该部分内容涉及从多个进程记录至单个文件的方法,参考下一篇文章:

Python多进程日志记录 - 掘金