文章目录
- 一、小总结
- 二、Logger对象的日志等级
- 三、使用多个处理器和多种格式化
- 四、日志回滚
- 1. RotatingFileHandler
- 2. TimedRotatingFileHandler
- 五、RotatingHandler存在的问题
- 六、从多个进程记录至单个文件
本文作为Python日志模块的补充,主要介绍日志回滚
RotatingFileHandler
和TimedRotatingFileHandler
的使用,以及其所带来的问题、Logger
对象的日志等级是如何其作用的等内容。
一、小总结
通过前面介绍logging
模块的博文【Python】—日志模块logging使用详解_ling620的专栏,基本上可以正确使用日志模块。需要注意的几点如下:
- 直接使用
logging
模块的接口函数,无任何其他操作,如logging.info()
来进行日志的输出是默认输出到控制台,默认的日志等级是logging.WARNING
,且不可以设置日志等级。 - 使用
logging
模块的接口函数,内部实现是:判断root.handlers
是否为空,为空则内部调用basicConfig()
函数,默认创建StreamHandler
。 - 使用
logging.basicConfig()
函数可以满足基本使用,可以输出到文件或控制台中。内部是根据参数来创建FileHandler
或StreamHandler
来实现。 -
Logger
不可以直接实例化,需要使用logging.getLogger()
获取Logger
对象。 - 一个
logger
对象可以添加多个handler
对象,通过addHandler()
函数来添加。 - 每个
handler
对象可以有一个Formatter
对象来指定格式,通过setFormatter()
函数来设置。 -
handler
和logger
对象都需要设置一个日志等级,通过setLevel()
函数来设置。 -
logger
的名称是一个以'.'
分割的层级结构,每个'.'
后面的logger
都是'.'
前面的logger
的children
。 - 不必为一个应用程序中所使用的所有
loggers
定义和配置handlers
,只需要为一个顶层的logger
配置handlers
,然后按照需要创建child loggers
就足够。 -
logger
有一个 “有效等级(effective level)” 的概念如果一个logger
上没有被明确设置level
,那么该logger
就是使用它parent
的level
;如果它的parent
也没有明确设置level
则继续向上查找parent
的parent
的有效level
,依次类推,直到找到个一个明确设置了level
的祖先为止。
二、Logger对象的日志等级
由前文已知,Logger
不可以直接实例化,需要使用logging.getLogger(name)
来获取Logger
的对象。通过setLevel()
来设置日志等级。
在测试过程中,发现了如下问题,设置了日志等级为logging.DEBUG
,输出Logger
对象的level
属性,得到的结果是10
,但仍然不输出DEBUG
等级的信息,这是为什么呢?如下:
>>> import logging
>>> logger = logging.getLogger('example')
>>> logger.level
0
>>> logger.debug('this is a debug msg.')# 无输出
>>> logger.warning('this is a warning msg.')
this is a warning msg.
>>> logger.setLevel(logging.DEBUG)
>>> logger.level
10
>>> logger.debug('this is a debug msg.') # 无输出
>>> logger.warning('this is a warning msg.')
this is a warning msg.
为什么设置了日志等级而没有起作用呢?-_-
首先分析一下,上面的代码中获取了Logger
的对象logger
,但是并没有添加任何handler
对象。当logging.getLogger()
做了什么呢?接口函数又做了哪些?
def getLogger(name=None):
"""
Return a logger with the specified name, creating it if necessary.
If no name is specified, return the root logger.
"""
if name:
return Logger.manager.getLogger(name)
else:
return root # 若未指定name,则返回root
# root是什么?
root = RootLogger(WARNING) # RootLogger的对象,日志等级为WARNING
Logger.root = root
Logger.manager = Manager(Logger.root)
getLogger()
函数内部根据是否指定name
返回对应的root logger
。即Logger
的初始化对象,handler,filter
参数等都为None
。可见默认的日志等级是WARNING
。
以logger.info()
接口函数为例,看看又做了些什么?
函数调用关系:
分析下Logger.callHandlers()
函数:
def callHandlers(self, record):
if (found == 0):
if lastResort:
if record.levelno >= lastResort.level:
lastResort.handle(record)
elif raiseExceptions and not self.manager.emittedNoHandlerWarning:
sys.stderr.write("No handlers could be found for logger"
" \"%s\"\n" % self.name)
self.manager.emittedNoHandlerWarning = True
- 循环
Logger
自己实例,直到获取到其祖辈,found
来计数其handlers
。 - 判断
found
个数,如果为0,判断lastResort
这个lastResort
是什么?如下:
它是_StderrHandler(WARNING)
类的初始化对象,且默认传递的日志等级是WARNING
,无法指定。_StderrHandler
类继承自StreamHandler
,使用sys.stderr
类似于StreamHandler
。
_defaultLastResort = _StderrHandler(WARNING) # 注意此处是WARNING
lastResort = _defaultLastResort
class _StderrHandler(StreamHandler):
"""
This class is like a StreamHandler using sys.stderr, but always uses
whatever sys.stderr is currently set to rather than the value of
sys.stderr at handler construction time.
"""
def __init__(self, level=NOTSET):
"""
Initialize the handler.
"""
Handler.__init__(self, level)
@property
def stream(self):
return sys.stderr
看到这里已经明白了,之所以在获取Logger
对象,设置日志等级后,依然没有生效的原因是,我们没有添加任何handler
,程序内部默认调用指定等级为WARNING
的_StderrHandler
,且该日志等级无法修改(使用setLevel()
不影响该handler
对象的等级)
当我们手动添加了handler
对象后,则会调用添加的handler
对象的等级或者root Logger
的等级。
当调用logging.basicConfig()
函数时,内部默认创建了FileHandler
或StreamHandler
对象,则我们再设置setLevel()
可生效。如下:
import loging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger()
logger.debug('this is a debug msg.')
# 输出
INFO:root:this is a debug msg.
三、使用多个处理器和多种格式化
日志记录器是普通的Python对象。addHandler()
方法没有限制可以添加的日志处理器数量。有时候,应用程序需要将严重类的消息记录在一个文本文件,而将错误类或其他等级的消息输出在控制台中。要进行这样的设定,只需多配置几个日志处理器即可,在应用程序代码中的日志记录调用可以保持不变。
import logging
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)
# 创建文件handler ,其等级为debug
fh = logging.FileHandler('example.log')
fh.setLevel(logging.DEBUG)
# 创建控制台handler,日志等级为ERROR
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# 创建formatter并添加至handlers
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
fh.setFormatter(formatter)
# 将handlers添加至logger
logger.addHandler(ch)
logger.addHandler(fh)
# 应用代码
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')
上述代码的结果是,控制台只输出error
和critical
的日志信息,而文件中则包含所有5个日志信息。
四、日志回滚
通过前面的分析,我们可以将日志信息输出到一个文件中,随着时间的流逝,日志文件会变得越来越大,如何处理这种情况?
我们希望当日志文件不断记录增长至一定大小或增长到一定时间时,打开一个新的文件接着记录。你可能希望只保留一定数量的日志文件,当不断的创建文件到达该数量时,又覆盖掉最开始的文件形成循环。 对于这种使用场景,日志包提供了 logging.hanlders.RotatingFileHandler
和logging.hanlders.TimedRotatingFileHandler
。
在上篇文章中讲到过:
类 | 描述 |
logging.handlers.RotatingFileHandler | 将日志消息发送到磁盘文件,并支持日志文件***按大小切割*** |
logging.hanlders.TimedRotatingFileHandler | 将日志消息发送到磁盘文件,并支持日志文件***按时间切割*** |
1. RotatingFileHandler
默认情况下,文件会无限增长。可以指定maxBytes
和backupCount
的特定值,以允许文件以预定的大小滚动。
当当前日志文件的长度接近maxBytes
时,就会发生翻转。如果backupCount为>= 1
,则系统将连续创建与基本文件路径名相同、但具有扩展名的新文件".1"
、".2"
等附于其后。
例如,如果backupCount
为5
,并且基本文件名为“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'
。
初始化函数定义:
__init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)
初始化参数:
参数名 | 含义 |
filename | 文件名 |
mode | 文件模式。使用回滚功能时,设置为 |
maxBytes | 文件大小,最大比特数,如 |
backupCount | 文件回滚个数,如设为 |
encoding | 文件编码格式,如果包含中文,则使用 |
delay | 构建 |
2. TimedRotatingFileHandler
参数when
决定了时间间隔的类型,参数interval
决定了多少的时间间隔。如when=‘D’,interval=2
,就是指两天的时间间隔,backupCount
决定了能留几个日志文件。超过数量就会丢弃掉老的日志文件。
初始化函数定义
__init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None)
初始化参数:
参数名 | 含义 |
filename | 文件名 |
when | 时间间隔的类型 |
interval | 时间间隔, |
backupCount | 文件回滚个数,如设为 |
encoding | 文件编码格式,如果包含中文,则使用 |
delay | 构建 |
utc | UTC时区的时间 |
参数when
:
符号 | 含义 |
S | 秒 |
M | 分钟 |
H | 小时 |
D | 天 |
W | 周 |
W0-W6 | 周一到周日 |
midnight | 在午夜,即每天凌晨 |
示例:
import glob
import logging
import logging.handlers
my_logger = logging.getLogger('MyLogger')
my_logger.setLevel(logging.DEBUG)
# Add the log message handler to the logger
handler = logging.handlers.RotatingFileHandler(
'logs/logging_demo.log', maxBytes=1024*1024, backupCount=5)
my_logger.addHandler(handler)
得到的日志文件如下,共6个文件:
logging_demo.log
logging_demo.log.1
logging_demo.log.2
logging_demo.log.3
logging_demo.log.4
logging_demo.log.5
五、RotatingHandler存在的问题
Python 的logging
模块提供了两个支持日志回滚的FileHandler
类,分别是RotatingFileHandler
和 TimedRotatingFileHandler
。
logging
是线程安全的,将单个进程中的多个线程日志记录至单个文件没有问题。但当有多个进程向同一个日志文件写入日志的时候,这两个RotatingHandler
就会带来问题,比如日志丢失。
六、从多个进程记录至单个文件
logging
是线程安全的,将单个进程中的多个线程日志记录至单个文件也是受支持的;但将多个进程中的日志记录至单个文件则不受支持,因为在Python
中并没有在多个进程中实现对单个文件访问的序列化的标准方案。
如果你需要将多个进程中的日志记录至单个文件,有一个方案是让所有进程都将日志记录至一个 SocketHandler
,然后用一个实现了套接字服务器的单独进程一边从套接字中读取一边将日志记录至文件。(如果愿意的话,你可以在一个现有进程中专门开一个线程来执行此项功能。) 这一部分文档对此方式有更详细的介绍,并包含一个可用的套接字接收器,你自己的应用可以在此基础上进行适配。
这部分内容还未细看。