日志的基本配置
日志的概念
日志(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
的用法等。