Python日志管理
很多项目都会用到logger,之前不解为何print能搞定的事要用logging来实现,但是当我在写一个比较复杂的项目时,我发现logging真香。
简单入手
1 | import logging |
一般情况下会看到终端的输出是这样的:
1 | 我是中间结果 |
代码中期望输出5行信息,而终端的输出是3行,发现debug和info的信息不见了!这是因为不同的日志信息是有优先级的,默认情况下logging
的优先级是warning,那么优先级低于warning的信息就会被过滤掉(详细流程之后会讲到),而日志优先级从高到低正好是按照debug、info、warning、error、critical的顺序。
配置参数
说实话logging
的基础配置格式并不好看,信息完全挤作一团,所以为了更加详细地显示信息,必须更改默认格式,好在logging
的配置也是很简单的。这里以logging.basicConfig
为例。
日志等级
我们先解决最好配置的一个部分——日志等级。
既然logging给定了5个等级的信息,那么只能使用warning及以上就有些浪费了,特别是在搞开发的时候,debug过程中使用debug级别的信息给开发人员看,程序发行后提高日志过滤级别,这样用户就看不到debug时的敏感信息了。
日志等级定义在basicConfig
中的level中定义,在代码中插入一行配置
1 | import logging |
现在程序可以输出所有的日志信息啦
1 | DEBUG:root:This is a debug message |
输出重定向
日志的默认输出是一股脑全部怼到终端上,而我们有时候需要程序print一些中间结果,然而中间结果在纷杂的日志信息中“泯然众人矣”。有没有什么办法可以将日志信息和print信息分离呢?filename
了解一下。
filename
指定日志信息输入到哪个文件中。与之搭配的还有一个参数filemode
,该参数指定以何种方式写入文件,默认是‘a’(append),这样上古日志也不会被覆盖掉。如果有强迫症一定要保持日志文件的整洁,那么‘w’(write)就可以将之前的日志覆盖掉。
1 | logging.basicConfig(filename="log.txt", filemode='w', level=logging.DEBUG) |
现在终端的输出干净多了,复杂的日志信息都到log.txt
中去了。
1 | 我是中间结果 |
机制的小朋友已经发现
filemode
和文件输入输出open
的参数类似,然后说:“一定还可以加上’b’选项,输出二进制的日志!”。如果有小朋友尝试一下就会在终端得到一连串的报错信息混杂着日志信息。是的,重定向失效了。搞得小朋友有很多问号。这说明日志不支持二进制输出,其实这个也很好理解,日志是给人看的,而没有小朋友愿意看二进制日志。
格式化
好看的日志信息是调式的动力(吧)。如何配置看着欢喜的日志呢?
logging提供了下面的参数,也就是占位符
格式 | 描述 |
---|---|
%(levelno)s | 日志级别的数值 |
%(levelname)s | 日志级别名称 |
%(pathname)s | 当前执行程序的路径 |
%(filename)s | 当前执行程序名称 |
%(funcName)s | 日志的当前函数 |
%(lineno)d | 日志的当前行号 |
%(asctime)s | 日志的时间 |
%(thread)d | 线程id |
%(threadName)s | 线程名称 |
%(process)d | 进程ID |
%(message)s | 日志信息 |
如果要输出“时间戳 - 信息”的格式,那么可以这样配置:
1 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s') |
1 | 2020-03-31 11:27:28,827 - This is a debug message |
现在终端的输出是不是规整多了?
如果有小朋友对时间格式不服,也可以手动设置,并且其参数和time
模块的时间配置参数相同,只不过参数实在太多了,具体还请参考官方文档。
如果开发任务对具体的日期不作要求,那么只要保留时间就好了。下面这个例子只保留了“小时-分钟-秒”,是我最常用的格式:
1 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s', datefmt='%H:%M:%S') |
输出效果
1 | 11:36:27 - This is a debug message |
相信大家也都是不愿记住繁杂的格式的,那么只要能知道在这里能查询就好了。
自定义logging类
之前使用logger
的方式是开箱即用式的,先import
,然后配置完就直接开始用了,这样做的好处是简单易用,直接上手比较方便。这种开箱即用的设计有点类似matplotlib
。
不过这样的做法是存在一些限制的,先说一下缺陷出在哪里,开箱即用的logger
会让全局共享同一个logger
,那么在一个项目中的basicConfig
会影响到全局。比如一个项目中包含了300个模块,现在需要调试某一个模块,那么我们期望其他模块不要“吵闹”就比较麻烦了,否则300个模块中的日志都输出可太壮观了。
Logger
为了控制不同文件的日志输出,有必要产生多个不同logger
。logging.getLogger(name)
会产生一个Logger
对象。
1 | logger = logging.getLogger(name=__name__) |
一般比较推荐用python的__name__作为name参数。
和logging
不同的是,新产生的Logger
对象不能直接使用basicConfig
,如果要配置自定义logger
的输出格式和输出目的,需要自定义Handler
和Formatter
。
Handler
Handler
可以控制输出的目的地,可以是终端,也可以是文件,甚至还可以通过网络传输到其他服务器上。这些Handler
分别可以用下面语句获得:
1 | s_handler = logging.StreamHandler() # 输出到流,默认是stdout |
那么拿到了Handler
怎么用呢?Logger
有一个addHandler
接口,只要将Handler
放进去就可以实现定向输出了。更重要的是,addHandler
可以多词调用添加不同的输出目的地。
1 | logger.addHandler(s_handler) |
Formatter
格式的设置方法之前已经讲了,实例化的方式也是一句即可:
1 | s_format = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(message)s', datefmt="%H:%M:%S") |
需要注意添加Formatter
的位置不是Logger
,而是Handler
。
1 | s_handler.setFormatter(s_format) |
所以,Logger、Handler、Formatter的嵌套关系示意图如下:
1 | Logger[ Handler[ Formatter ] ] |
流程
对于需要进阶了解logging
的人,下面这张图应该会很有帮助。
提示
实际上不论basicConfig有无显式调用,在使用logging输出信息的时候已经被调用了,就是说debug()、info()、warning()、error()、critical()这些函数会偷偷执行basicConfig()。并且basicConfig只能调用一次,之后再次调用将不再生效。这有何影响?这表明basicConfig()必须尽早配置。要不尝试一下下面的代码吧,你将得不到任何日志信息。
1 | import logging |