日志的基本配置
日志的概念
日志(log)指的是对程序在运行过程中所发生的一些情况或行为的记录。在平时 debug 中,并不是所有错误都容易调试。有一些错误是不容易复现的,可能会在软件运行很久后才产生,但这个错误可能是由很早之前的某个事件引发的。这个时候就需要一种方式监控并不断反馈程序运行的状况。日志则专门用于实现此功能,日志的存在方便于定位错误的位置并追踪错误产生的原因。
相比与简单的 print() ,日志输出的信息更加完善且规范,便于筛选与定位,并且可以通过简单的配置更改日志的各种输出需求。
日志的级别
对于日志记录,其中有一个非常重要的概念就是日志信息的级别(level)。级别用于区分信息的重要性,在检查日志时方便筛选重要信息。
在 Python 内,日志基本上分为以下 5 种级别:
info
warning
error
critical
级别等级从上到下是依次提高。因此,如果日志中出现了 error 和 critical 级别的信息,那么表明程序中出现了意料之外的错误,这个错误很可能是引起程序崩溃的原因。而 warning 只是正常的警告,可能是某些功能使用不当引起的。info 和 debug 则是软件运行过程中正常打印出来的一些信息,标识软件运行过程中的一些状态。
一般来说,debug 只用于调试,会记录一些无关紧要的辅助信息,输出信息的重要性最低,因此输出的信息较多,而级别越往上信息量一般会减少。如果日志中 error 和 critical 信息比较多的话,说明软件的运行是不正常的,软件的设计可能有较大问题。
logging 是 Python 内置的日志模块。在 logging 库中,调用同名的函数即可生成对应等级的一条日志:
执行结果为:
logging 的打印和 print() 的打印不同,它并不只显示日志消息,而是具有特定格式,这样方便筛选得到合适的日志记录。
另外注意一点,debug 和 info 级别的日志没有打印,这是因为 logging 模块有一个默认的配置,这个配置包含了日志的输出级别:只有日志的级别大于等于配置的输出级别时,这条日志才会输出到控制台中显示。默认的输出级别是 warning ,也就说只有高于或等于 warning 级别的日志才会打印。
可以通过 basicConfig() 函数对日志做一些简单的配置,该函数需要放在输出第一条日志的语句前,否则配置将不会生效。basicConfig() 有一个参数 level 用于控制日志的输出级别。如果更改日志级别为 info :
重新执行后,显示的日志信息就会包含 info 以及更高级别的日志信息:
logging中的等级实际上使用整数表示,数值越高则等级越高。例如最低的NOTSET为0,然后DEBUG为10,最高的CRITICAL为50。除此之外,logging还提供了log(level, msg)函数,可以通过第一个参数提供日志的等级。因此,如果对
logging提供的等级划分不满意,也可以自行添加等级,例如:SUCCESS = 35def success(msg, *args, **kwargs):logging.log(SUCCESS, msg, *args, **kwargs)定义后最好通过
addLevelName注册这个等级的信息,这样输出时才能得到正确的等级名:logging.addLevelName(SUCCESS, 'SUCCESS')
在开发阶段,需要记录尽可能详细的内容,方便查找各种问题;而到了部署阶段,由于软件功能已经基本稳定,只需要记录比较重要的信息即可,这样在可以快速发现一些关键的问题。日志的等级便提供了这样的功能,只需要简单修改配置即可实现内容的过滤。
日志的文件记录
logging 中的 basicConfig() 除了可以设置 level 以外,还可以做一些其它的配置。例如,默认情况下日志将被打印到控制台中,这样既不利于回看日志,也不便于检索信息。filename 参数允许将日志保存到文件中,例如:
重新执行后,就可以在当前目录下生成一个新文件 demo.log ,其中的内容为:
如果再次执行代码,文件中的内容会增加,而不是覆盖:
这是由于在打开日志文件时,使用的读写方式是 'a' ,也就是在末尾追加内容的方式。可以通过 basicConfig() 的 filemode 参数改变文件的打开方式,例如用表示覆盖的写方式 "w" :
此时多次运行该程序,可以看到日志文件中的内容将会被覆盖:
除此之外,打开文件时还有一个比较重要的参数 encoding ,代表打开文件的编码。如果要在日志信息中包含中文,那么就需要指定编码,否则可能会产生乱码。
日志的格式
在使用 logging 模块的时候,它打印出的日志信息前还有通过冒号隔开的 LEVEL 和 root,而不只包括提示的信息,这表明日志应该有一个默认的格式。
默认的格式可以通过检查 logging 模块的 BASIC_FORMAT 常量得知:
不难看出,日志的格式就是使用 Python 中的百分号形式的格式字符串表示的,logging 会使用对应的内容填充格式字符串的对应字段,因此只需要自行提供一个格式字符串,其中包含 logging 支持的字段名即可:
然后将其传入 basicConfig() 的 format 参数中:
这样日志显示的内容为:
logging 支持的完整字段名可以查阅官方文档,可以根据实际需求创建合适的日志格式。
此外,如果不想使用默认的日期时间表示格式,可以通过 basicConfig() 的 datefmt 参数提供日期时间格式,它和 Python 标准库 time 的 strftime() 所用的日期时间表示法相同,详见官方文档。
日志的高级使用
创建新的日志对象
之前一直都是直接使用 logging 模块提供的日志函数,然后使用 basicConfig() 统一所有日志函数的格式、输出和过滤方式。但是这样做有一个问题:程序的不同模块可能需要将日志输出到不同的位置,或是具有不同的输出格式或过滤等级,但不可能在程序中频繁地通过 basicConfig() 切换日志形式。如果该程序需要作为一个第三方库在其它位置被使用,两者的日志处理也应该是隔离的。这就要求创建不同的日志处理对象。
logging 模块中,Logger 对象实现了组件化日志记录接口。可以通过 getLogger(name) 函数创建一个 Logger 对象:
以相同
name调用的getLogger()将会返回相同的日志对象。此外,日志对象有一个层级的概念:日志对象可以像模块一样通过在
name中使用点号.表示层级结构,每个点号.后面的日志对象是前面日志对象的子类。例如"demo.test"是"demo"的子类。
每个 Logger 对象也提供了同名的方法创建日志记录:
这时,默认的运行效果为:
可以看到,Logger 对象默认打印格式只有消息本身,并且默认只打印等级为 warning 及以上等级的日志。
为了配置一个 Logger 对象,接下来要引入 Handler 对象的概念。Handler 对象决定一条日志应该如何输出,包括将日志记录输出到合适的位置,以及采用合适的输出格式。
logging 模块提供了多种 Handler ,每种 Handler 分别用于不同的工作方式。以下创建了两个 Handler 对象,StreamHandler 用于将日志输出到指定的流中(默认是标准错误流 sys.stderr ),而 FileHandler 则将日志输出到指定的文件对象:
日志的层级使得日志处理时有一个向上传播的机制:某一级日志对象在处理日志记录后,会将该记录传给上一级日志对象处理,上一级日志对象也会继续这样。
因为是
Logger对象具有层级,但真正处理日志的是Handler对象,因此在具有层级的情况下,只需要为顶级的Logger对象提供Handler即可,否则一条日志会被多次处理。可以通过将一个
Logger对象的.propagate属性设置为False来关闭这种传播机制。
一个 Logger 对象可以有多个 Handler ,通过 Logger 对象的 .addHandler() 方法可以添加一个 Handler :
这时再运行代码,logger 通过这两个 Handler 对象,分别将日志信息发送到了控制台和 demo.log 文件中。更多实用的 Handler 可以查看官方文档。
格式化和过滤记录
如果要自定义日志信息的最终输出格式,需要自定义 Formatter 对象。
例如,以下创建了两个 Formatter 对象,分别给它们提供不同的格式。Formatter 对象还支持在格式化字符串中使用不同格式的占位符:
然后通过 Handler 对象的 .setFormatter() 方法设置输出格式:
Logger和Handler对象是多对多的关系,一个日志对象可以拥有多个输出方式;但是Handler和Formatter对象是多对一的关系,一个输出渠道只能拥有一种格式。所以为Handler添加Formatter用到的方法以 set 开头。
现在运行程序,可以看到两路不同的输出有着不同的输出格式:
最后一个话题是关于信息的过滤。在前文中,使用 basicConfig() 的 level 参数实现了对日志输出级别的控制。如果只是要根据等级过滤日志,可以使用 .setLevel(level) 方法实现。
不过要注意的是,Logger 和 Handler 都提供了 .setLevel() 方法,也就是说每条日志会被处理两次:当 Logger 对象接收一条日志记录时,它会先根据自己的等级决定是否要过滤该记录,交给 Handler 处理后,Handler 再根据自身的等级确定是否要过滤记录。因此,以下等级设置:
在控制台会输出 info 及以上等级的日志,在文件中会写入 error 及以上等级的日志。
除了默认的按等级过滤的方式外,logging 还提供了自定义过滤器的方式:通过继承 Filter 类并重写 .filter() 方法,可以实现更复杂的过滤逻辑。
.filter() 方法在记录可以产生时返回 True ,在记录需要被过滤时返回 False 。该方法只接收一个参数 record ,它是一个特别的 LogRecord 对象,其中包含了表示运行状况的一些属性,它的属性名和 Formatter 的字段名其实是一样的。以下实现了这样一个示例,可以过滤某些特别函数中的日志记录:
Filter还有一个作用,就是可以在过滤时顺便向日志记录添加一些额外的上下文信息,详见官方文档。
Logger 和 Handler 都可以添加一个和多个过滤器,它们的规则也是一样的:Logger 的过滤器先发挥作用,剩余的日志再交给 Handler 的过滤器处理。例如:
这样会过滤掉当前文件 main() 函数的所有日志。
最后再总结一下日志的完整工作流程:
Logger对象被用户调用,产生一条日志- 根据
.setLevel()和Filter决定日志是否被过滤 - 未被过滤的日志将发送给
Handler处理,它根据自己的.setLevel()和Filter再次决定日志是否被过滤 - 未被过滤的日志在被
Formatter格式化后发送到合适的位置被记录
对于有层级的
Logger对象,它同时还会将记录发送给上一级处理。
官方文档提供了这样一张图片,非常详细地说明了日志处理的完整流程:
实际上,之前通过 basicConfig() 的基本使用方式和上述流程也是相同的,只不过它默认生成了一个名为 'root' 的 Logger 对象,并且自动为其准备了 Handler 和 Formatter 。
通过文件配置日志
之前已经完整地介绍了 logging 模块的完整使用流程,但是这一过程其实十分公式化,每次都要按部就班地创建一系列对象再逐个绑定不免有些麻烦。好在 logging 提供了文件配置的方式,可以通过配置文件快速生成一系列对象以及它们之间的关系。
logging 的标准配置文件是 .conf 文件,实质上是 ini 格式。接下来通过一个示例说明该配置文件的写法。
首先,该配置文件需要提供 loggers 、handlers 和 formatters 字段表示日志所用到的所有对象,其中使用 keys 键名表示这些对象的名字:
注意:通过文件形式配置的日志必须要提供名为
"root"的日志对象。并且通过文件似乎无法同时配置Filter对象,解决方法是使用后面介绍的字典形式配置。
接下来,使用 logger_name 字段表示要配置 name 代表的 Logger 对象:
level 键表示该 Logger 对象要过滤的等级,handlers 键表示需要给它添加的 Handler 对象,它们的配置形式相同:
同样通过 handle_ 加上 Handler 名称表示的字段名代表要配置该 Handler 对象,class 代表所属的 Handler 类。由于不同的 Handler 类在构造时具有不同的参数,因此 args 键需要对应元组形式的值。
最后 formatter 的写法也类似:
在编写完成 .conf 文件的基础上,导入文件的配置是非常简单的:
然后就可以直接使用 Logger 对象,而无需再接触 Handler 、Formatter 之类的对象:
另外一种配置方法是使用 Python 的字典,实际上任何能对应到 Python 字典的配置文件都可以使用这种配置方式,例如 JSON 。不管如何,只需要在将其解析为字典后,通过以下代码完成配置:
Python 官方推荐的配置文件是 YAML 格式,不过需要先安装解析 YAML 的第三方库:
它的基本使用方式为:
这种配置方式和 ini 格式很像,除去 Filter 外其它内容一个写法示例如下:
两者的一些区别为:字典格式的配置不再需要提前书写对象名,而是通过二级属性提供,并且生成对象需要通过关键字形式传递参数。Filter 的一个写法如下:
参考资料/延伸阅读
logging cookbook 文档,其中包含一些更高级的内容,例如日志的多线程和异常处理、更多 Handler 的用法等。