通常在前期调试代码时,我们会使用 print()
在 IDE 控制台打印一些信息,判断运行情况。但在运行整个自动化测试项目的过程中,通过 print()
打印信息的方式获取运行情况显然行不通。这时就需要收集日志,每次运行后通过查看日志来获取项目运行情况。
日志概述
日志是一种可以追踪某些软件运行时所发生事件的方法。软件开发人员可以向他们的代码中调用日志记录相关的方法来表明发生了某些事情。一个事件可以用一个可包含可选变量数据的消息来描述。此外,事件也有重要性的概念,这个重要性也可以被称为严重性级别(level)。
一条日志信息对应的是一个事件的发生,而一个事件通常需要包括以下几个内容:
- 事件发生时间
- 事件发生位置
- 事件的严重程度(日志级别)
- 事件内容
上面这些都是一条日志记录中可能包含的字段信息,当然还可以包括一些其他信息,如进程ID、进程名称、线程ID、线程名称等。日志格式就是用来定义一条日志记录中包含那些字段的,且日志格式通常都是可以自定义的。
输出一条日志时,日志内容和日志级别是需要开发人员明确指定的。对于而其它字段信息,只需要选择是否显示在日志中就可以了。
几乎所有开发语言都会内置日志相关功能,或者会有比较优秀的第三方库来提供日志操作功能,比如:log4j
,log4php
等。它们功能强大、使用简单。Python
自身也提供了一个用于记录日志的标准库模块 logging
。
logging 模块
logging
模块定义的函数和类为应用程序和库的开发实现了一个灵活的事件日志系统。logging
模块是 Python 的一个标准库模块,由标准库模块提供日志记录 API 的关键好处是所有 Python 模块都可以使用这个日志记录功能。所以,你的应用日志可以将你自己的日志信息与来自第三方模块的信息整合起来。
logging模块的日志级别
logging模块默认定义了以下几个日志等级:(模块允许开发人员自定义其他日志级别,但是这是不被推荐的,尤其是在开发供别人使用的库时,因为这会导致日志级别的混乱)
日志等级 (level) | 描述 |
---|---|
DEBUG | 最详细的日志信息,典型应用场景是问题诊断 |
INFO | 信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作 |
WARNING | 当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的 |
ERROR | 由于一个更严重的问题导致某些功能不能正常运行时记录的信息 |
CRITICAL | 当发生严重错误,导致应用程序不能继续运行时记录的信息 |
开发应用程序或部署开发环境时,可以使用 DEBUG 或 INFO 级别的日志获取尽可能详细的日志信息来进行开发或部署调试;应用上线或部署生产环境时,应该使用 WARNING 或 ERROR 或 CRITICAL 级别的日志来降低机器的 I/O 压力和提高获取错误日志信息的效率。日志级别的指定通常都是在应用程序的配置文件中进行指定的。
- 日志等级从上到下依次升高,即:DEBUG < INFO < WARNING < ERROR < CRITICAL,而日志的信息量则按这个顺序依次减少;
- 当为某个应用程序指定一个日志级别后,应用程序会记录所有日志级别大于或等于指定日志级别的日志信息,小于该等级的日志记录将会被丢弃。
logging 模块的使用方式
logging模块提供了两种记录日志的方式:
- 第一种方式是使用
logging
提供的模块级别的函数 - 第二种方式是使用
logging
日志系统的四大组件,进行日志流处理
模块函数方法
常用函数
logging
模块定义的模块级别的常用函数如下:
函数 | 说明 |
---|---|
logging.debug(msg, *args, **kwargs) |
创建一条严重级别为DEBUG,内容为 msg 的日志记录 |
logging.info(msg, *args, **kwargs) |
创建一条严重级别为INFO,内容为 msg 的日志记录 |
logging.warning(msg, *args, **kwargs) |
创建一条严重级别为WARNING,内容为 msg 的日志记录 |
logging.error(msg, *args, **kwargs) |
创建一条严重级别为ERROR,内容为 msg 的日志记录 |
logging.critical(msg, *args, **kwargs) |
创建一条严重级别为CRITICAL,内容为 msg 的日志记录 |
logging.log(level, *args, **kwargs) |
创建一条严重级别为 level (可以自定义),内容为 msg 的日志记录 |
logging.basicConfig(**kwargs) |
对 root logger 进行一次性配置 |
其中 logging.basicConfig(**kwargs)
函数用于指定“要记录的日志级别”、“日志格式”、“日志输出位置”、“日志文件的打开模式”等信息,其他几个都是用于记录各个级别日志的函数。该函数可接收的关键字参数如下:
参数名称 | 描述 |
---|---|
filename |
指定日志输出目标文件的文件名,指定该设置项后日志信心就不会被输出到控制台了 |
filemode |
指定日志文件的打开模式,默认为'a' 。需要注意的是,该选项要在 filename 指定时才有效;打开模式有:'w' 写入;'W' 清空后写入;'r' 只读;'a' 追加(即在文件原有的数据后面添加) |
format |
指定日志格式字符串,即指定日志输出时所包含的字段信息以及它们的顺序。logging 模块定义的格式字段参见下一张表格 |
datefmt |
指定日期/时间格式。需要注意的是,该选项要在 format 中包含时间字段 %(asctime)s 时才有效 |
level |
指定日志记录器的日志级别,只有大于或等于该级别的日志信息会被记录 |
stream |
指定日志输出目标 stream ,如 sys.stdout 、 sys.stderr 以及网络 stream 。需要说明的是,stream 和 filename 不能同时提供,否则会引发 ValueError 异常 |
style |
Python 3.2 中新添加的配置项。指定 format 格式字符串的风格,可取值为'%'、'{'和'$',默认为'%' |
handlers |
Python 3.3 中新添加的配置项。该选项如果被指定,它应该是一个创建了多个 Handler 的可迭代对象,这些 handler 将会被添加到 root logger。需要说明的是:filename 、stream 和 handlers 这三个配置项只能有一个存在,不能同时出现 2 个或 3 个,否则会引发 ValueError 异常。 |
logging
模块中定义好的可以用于 format
格式字符串的字段如下:
字段/属性名称 | 使用格式 | 描述 |
---|---|---|
asctime | %(asctime)s | 日志事件发生的时间--人类可读时间,如:2003-07-08 16:49:45,896 |
created | %(created)f | 日志事件发生的时间--时间戳,就是当时调用time.time()函数返回的值 |
relativeCreated | %(relativeCreated)d | 日志事件发生的时间相对于 logging 模块加载时间的相对毫秒数 |
msecs | %(msecs)d | 日志事件发生时间的毫秒部分 |
levelname | %(levelname)s | 该日志记录的文字形式的日志级别('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') |
levelno | %(levelno)s | 该日志记录的数字形式的日志级别(10, 20, 30, 40, 50) |
name | %(name)s | 所使用的日志记录器名称,默认是 'root',因为默认使用的是 rootLogger |
message | %(message)s | 日志记录的文本内容,通过 msg % args 计算得到的 |
pathname | %(pathname)s | 调用日志记录函数的源码文件的全路径 |
filename | %(filename)s | pathname 的文件名部分,包含文件后缀 |
module | %(module)s | filename的名称部分,不包含后缀 |
lineno | %(lineno)d | 调用日志记录函数的源代码所在的行号 |
funcName | %(funcName)s | 调用日志记录函数的函数名 |
process | %(process)d | 进程ID |
processName | %(processName)s | 进程名称,Python 3.1 新增 |
thread | %(thread)d | 线程ID |
threadName | %(thread)s | 线程名称 |
代码实例
代码实例:--- 默认情况,不使用 logging.basicConfig()
配置
1 | import logging |
输出结果:
1 | WARNING:root:This is a warning log. |
- 由于未指定输出文件,因此日志直接输出到控制台(日志记录函数所使用的日志记录器设置的处理器所指定的日志输出位置默认为
sys.stderr
); - 日志记录函数所使用的日志记录器的默认日志级别是 WARNING,因此只有 WARNING 级别的日志记录以及大于它的 ERROR 和 CRITICAL 级别的日志记录被输出了,而小于该级别的 DEBUG 和 INFO 级别的日志记录被丢弃了;
- 输出结果中每行日志记录的各个字段含义是:
日志级别:日志记录器名称:日志内容
,之所以会这样输出,是因为logging
模块提供的日志记录函数所使用的日志记录器设置的日志格式默认是 BASIC_FORMAT,其值为:"%(levelname)s:%(name)s:%(message)s"
;
代码实例:--- 使用 logging.basicConfig()
配置
1 | import logging |
将上述代码命名为 test.py
,则运行后输出结果会被保存在同目录下的 my.log
文件中,内容如下:
1 | 07/05/2021 15:12:27 PM test.py[line:9] DEBUG This is a debug log. |
一些说明
logging.basicConfig()
函数是一个一次性的简单配置工具,只有在第一次调用该函数时会起作用,多次调用的设置并不是累加操作;日志记录器(Logger)是有层级关系的,上面调用的
logging
模块级别的函数所使用的日志记录器是 RootLogger 类的实例,其名称为 'root',它是处于日志记录器层级关系最顶层的日志记录器,且该实例是以单例模式存在的;如果要记录的日志中包含变量数据,可使用一个格式字符串作为这个事件的描述消息(
logging.debug()
、logging.info()
等函数的第一个参数msg
),然后将变量数据作为第二个参数*args
的值进行传递,如:logging.warning('%s is %d years old.', 'Tom', 10)
,输出内容为:WARNING:root:Tom is 10 years old.
;logging.debug()
,logging.info()
等函数的定义中,除了msg
和*args
参数外,还有一个**kwargs
参数。它们支持以下 3 个关键字参数:exc_info
: 其值为布尔值,如果该参数的值设置为True
,则会将异常异常信息添加到日志消息中。如果没有异常信息则添加None
到日志信息中;stack_info
: 其值为布尔值,默认值为False
。如果该参数的值设置为True
,栈信息将会被添加到日志信息中;extra
: 这是一个字典(dict
)参数,它可以用来自定义消息格式中所包含的字段,但是它的key
不能与logging
模块定义的字段冲突;
一个代码实例如下:--- 在日志消息中添加
exc_info
和stack_info
信息,并添加两个自定义的字段ip
和user
1
2
3
4
5LOG_FORMAT = "%(asctime)s - %(levelname)s - %(user)s[%(ip)s] - %(message)s"
DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"
logging.basicConfig(format=LOG_FORMAT, datefmt=DATE_FORMAT)
logging.warning("Some one delete the log file.", exc_info=True, stack_info=True, extra={'user': 'Tom', 'ip':'47.98.53.222'})输出结果:
1
2
3
4
505/08/2017 16:35:00 PM - WARNING - Tom[47.98.53.222] - Some one delete the log file.
NoneType
Stack (most recent call last):
File "C:/Users/wader/PycharmProjects/LearnPython/day06/log.py", line 45, in <module>
logging.warning("Some one delete the log file.", exc_info=True, stack_info=True, extra={'user': 'Tom', 'ip':'47.98.53.222'})
日志流处理
logging
模块的四大组件:
组件名称 | 对应类名 | 功能描述 |
---|---|---|
记录器 | Logger |
又叫日志器,提供了应用程序可一直使用的接口 |
处理器 | Handler |
将 Logger 创建的日志记录发送到合适的目的地输出 |
过滤器 | Filter |
提供了更细粒度的控制工具来决定输出哪条日志记录,丢弃哪条日志记录 |
格式器 | Formatter |
决定日志记录的最终输出格式 |
logging
模块就是通过这些组件来完成日志处理的,之前所使用的 logging
模块级别的函数也是通过这些组件对应的类来实现的。
这些组件之间的关系描述:
- 日志记录器(logger)需要通过处理器(handler)将日志信息输出到目标位置,如:文件、
sys.stdout
、网络等; - 不同的处理器(handler)可以将日志输出到不同的位置;
- 日志记录器(logger)可以设置多个处理器(handler)将同一条日志记录输出到不同的位置;
- 每个处理器(handler)都可以设置自己的过滤器(filter)实现日志过滤,从而只保留感兴趣的日志;
- 每个处理器(handler)都可以设置自己的格式器(formatter)实现同一条日志以不同的格式输出到不同的地方。
总的来说:日志记录器(logger)是入口,真正干活儿的是处理器(handler),处理器(handler)还可以通过过滤器(filter)和格式器(formatter)对要输出的日志内容做过滤和格式化等处理操作。
Logger
Logger
对象有 3 个任务要做:
- 向应用程序代码暴露几个方法,使应用程序可以在运行时记录日志消息;
- 基于日志严重等级(默认的过滤设施)或
Filter
对象来决定要对哪些日志进行后续处理; - 将日志消息传送给所有感兴趣的日志
Handler
;
Logger对象最常用的方法分为两类:配置方法 和 消息发送方法
配置方法:
方法 | 描述 |
---|---|
Logger.setLevel() |
设置该 logger 将会处理的日志消息的最低日志等级 |
Logger.addHandler() 和 Logger.removeHandler() |
为该 logger 对象添加 或 移除一个 handler 对象 |
Logger.addFilter() 和 Logger.removeFilter() |
为该 logger 对象添加 或 移除一个 filter 对象 |
消息发送方法:
方法 | 描述 |
---|---|
Logger.debug() ,Logger.info() ,Logger.warning() ,Logger.error() ,Logger.critical() |
创建一个与它们的方法名对应等级的日志记录 |
Logger.exception() |
创建一个类似于 Logger.error() 的日志消息 |
Logger.log() |
使用一个明确的日志 level 参数(可以自定义)来创建一个日志记录 |
Logger.exception()
与Logger.error()
的区别在于:Logger.exception()
将会输出堆栈追踪信息,另外通常只是在一个exception handler
中调用该方法;Logger.log()
与Logger.debug()
、Logger.info()
等方法相比,虽然需要多传一个level
参数,显得不是那么方便,但是当需要记录自定义等级的日志时还是需要该方法来完成;
那么,怎样得到一个 Logger
对象呢?一种方式是通过 Logger
类的实例化方法创建一个 Logger
类的实例,但是我们通常都是用第二种方式:logging.getLogger()
方法。logging.getLogger()
方法有一个可选参数 name
,该参数表示将要返回的日志记录器的名称标识,如果不提供该参数,则其值为 'root'。若以相同的 name
参数值多次调用 getLogger()
方法,将会返回指向同一个 logger
对象的引用。
关于
logger
的层级结构与有效等级的说明:
logger
的名称是一个以'.'分割的层级结构,每个'.'后面的logger
都是该'.'前面的logger
的 children,例如,有一个名称为 foo 的logger
,则其它名称为 foo.bar, foo.bar.baz 或 foo.bam 的logger
都是 foo 的后代;logger
有一个"有效等级(effective level)"的概念。如果一个logger
上没有被明确设置一个 level,那么该logger
就是使用它 parent 的 level;如果它的 parent 也没有明确设置 level 则继续向上查找 parent 的 parent 的有效 level,以此类推,直到找到个一个明确设置了 level 的祖先为止。需要说明的是,root logger
总是会有一个明确的 level 设置(默认为 WARNING)。当决定是否去处理一个已发生的事件时,logger
的有效等级将会被用来决定是否将该事件传递给该logger
的handler
s 进行处理;- child
logger
s 在完成对日志消息的处理后,默认会将日志消息传递给与它们的祖先logger
s 相关的handler
s。因此,我们不必为一个应用程序中所使用的所有logger
s 定义和配置handler
s,只需要为一个顶层的logger
配置handler
s,然后按照需要创建 childlogger
s 就可足够了。我们也可以通过将一个logger
的 propagate 属性设置为False
来关闭这种传递机制。
Handler
Handler
对象的作用是(基于日志消息的 level)将日志消息分发到指定的位置(文件、网络、邮件等)。Logger
对象可以通过 addHandler()
方法为自己添加 0 个或者更多个 handler
对象。比如,一个应用程序可能想要实现以下几个日志需求:
- 把所有日志都发送到一个日志文件中;
- 把所有严重级别大于等于 ERROR 的日志发送到
sys.stdout
(标准输出); - 把所有严重级别为 CRITICAL 的日志发送到一个 email 邮件地址;
这种场景就需要 3 个不同的 handler
s,每个 handler
负责发送一个特定等级的日志到一个特定的位置。
一个 handler
中只有非常少数的方法是需要应用开发人员去关心的。对于使用内建 handler
对象的应用开发人员来说,似乎需要关注的方法主要就是下面这几个配置方法:
方法 | 描述 |
---|---|
Handler.setLevel() |
设置 handler 将会处理的日志消息的最低等级 |
Handler.setFormatter() |
为 handler 设置一个 formatter 对象 |
Handler.addFilter() 和 Handler.removeFilter() |
为 handler 添加 或 移除 一个 filter 对象 |
需要说明的是,应用程序代码不应该直接实例化和使用 Handler
实例。因为 Handler
是一个基类,它只定义了素有 handler
s 都应该有的接口,同时提供了一些子类可以直接使用或覆盖的默认行为。下面是一些常用的 Handler
:
Handler | 描述 |
---|---|
logging.StreamHandler |
将日志消息发送到输出到 Stream ,如 std.out , std.err 或任何 file-like 对象 |
logging.FileHandler |
将日志消息发送到磁盘文件,默认情况下文件大小会无限增长 |
logging.handlers.RotatingFileHandler |
将日志消息发送到磁盘文件,并支持日志文件按大小切割 |
logging.hanlders.TimedRotatingFileHandler |
将日志消息发送到磁盘文件,并支持日志文件按时间切割 |
logging.handlers.HTTPHandler |
将日志消息以GET或POST的方式发送给一个HTTP服务器 |
logging.handlers.SMTPHandler |
将日志消息发送给一个指定的 email 地址 |
logging.NullHandler |
该 Handler 实例会忽略 error messages,通常被想使用 logging 的 library 开发者使用来避免 'No handlers could be found for logger XXX' 信息的出现 |
Formatter
Formatter
对象用于配置日志信息的最终顺序、结构和内容。与 logging.Handler
基类不同的是,应用代码可以直接实例化 Formatter
类。另外,如果你的应用程序需要一些特殊的处理行为,也可以实现一个 Formatter
的子类来完成。
Formatter类的构造方法定义如下:
1 | logging.Formatter.__init__(fmt=None, datefmt=None, style='%') |
可见,该构造方法接收3个可选参数:
fmt
:指定消息格式化字符串,如果不指定该参数则默认使用 message 的原始值datefmt
:指定日期格式字符串,如果不指定该参数则默认使用 "%Y-%m-%d %H:%M:%S"style
:Python 3.2 新增的参数,可取值为 '%', '{'和 '$',如果不指定该参数则默认使用 '%'
Filter
Filter
可以被 Handler
和 Logger
用来做比日志等级 level 更细粒度的、更复杂的过滤功能。Filter
是一个过滤器基类,它只允许某个 logger
层级下(即该 logger
的 children)的日志事件通过过滤。该类定义如下:
1 | class logging.Filter(name='') |
比如,一个 filter
实例化时传递的 name
参数值为 'A.B',那么该 filter
实例将只允许 'A.B' 的 children(即名称为类似如下规则的logger
s:'A.B','A.B,C','A.B.C.D','A.B.D', ...)产生的日志记录通过过滤,而名称为 'A.BB', 'B.A.B' 等的 logger
s 产生的日志则会被过滤掉。如果 name
的值为空字符串,则允许所有的日志事件通过过滤。
filter
方法用于具体控制传递的record
记录是否能通过过滤,如果该方法返回值为 0 表示不能通过过滤,返回值为非 0 表示可以通过过滤;- 如果有需要,也可以在
filter(record)
方法内部改变该record
,比如添加、删除或修改一些属性; filter
还可以做一些统计工作,例如:可以计算被某个特定的logger
或handler
所处理的record
的数量等;
整体流程
- (在用户代码中进行)日志记录函数调用,如:
logger.info(...)
,logger.debug(...)
等; - 判断要记录的日志级别是否满足日志记录器设置的级别要求(要记录的日志级别要大于或等于日志记录器设置的级别才算满足要求),如果不满足则该日志记录会被丢弃并终止后续的操作,如果满足则继续下一步操作;
- 根据日志记录函数调用时掺入的参数,创建一个日志记录(
LogRecord
类)对象; - 判断日志记录器上设置的过滤器是否拒绝这条日志记录,如果日志记录器上的某个过滤器拒绝,则该日志记录会被丢弃并终止后续的操作,如果日志记录器上设置的过滤器不拒绝这条日志记录或者日志记录器上没有设置过滤器则继续下一步操作:将日志记录分别交给该日志记录器上添加的各个处理器;
- 判断要记录的日志级别是否满足处理器设置的级别要求(要记录的日志级别要大于或等于该处理器设置的日志级别才算满足要求),如果不满足记录将会被该处理器丢弃并终止后续的操作,如果满足则继续下一步操作;
- 判断该处理器上设置的过滤器是否拒绝这条日志记录,如果该处理器上的某个过滤器拒绝,则该日志记录会被当前处理器丢弃并终止后续的操作,如果当前处理器上设置的过滤器不拒绝这条日志记录或当前处理器上没有设置过滤器测继续下一步操作;
- 如果能到这一步,说明这条日志记录经过了层层关卡允许被输出了,此时当前处理器会根据自身被设置的格式器(如果没有设置则使用默认格式)将这条日志记录进行格式化,最后将格式化后的结果输出到指定位置(文件、网络、类文件的
Stream
等); - 如果日志记录器被设置了多个处理器的话,上面的第 5-8 步会执行多次;
- 这里才是完整流程的最后一步:判断该日志记录器输出的日志消息是否需要传递给上一级
logger
(之前提到过,日志记录器是有层级关系的)的处理器,如果propagate
属性值为 1 则表示日志消息将会被输出到处理器指定的位置,同时还会被传递给 parent 日志记录器的handler
s 进行处理(此时上一级logger
的日志等级并不会对该日志消息进行等级过滤)直到当前日志记录器的propagate
属性为 0 停止,如果propagate
值为 0 则表示不向 parent 日志记录器的handler
s 传递该消息,到此结束。
代码实例
需求
现在有以下几个日志记录的需求:
- 要求将所有级别的所有日志都写入磁盘文件中
all.log
文件中记录所有的日志信息,日志格式为:日期和时间 - 日志级别 - 日志信息
error.log
文件中单独记录error及以上级别的日志信息,日志格式为:日期和时间 - 日志级别 - 文件名[line:行号] - 日志信息
- 要求
all.log
在每天凌晨进行日志切割
分析
- 要记录所有级别的日志,因此日志器的有效 level 需要设置为最低级别---DEBUG;
- 日志需要被发送到两个不同的目的地,因此需要为
logger
设置两个handler
;另外,两个目的地都是磁盘文件,因此这两个handler
都是与FileHandler
相关的; all.log
要求按照时间进行日志切割,因此他需要用logging.handlers.TimedRotatingFileHandler
;而error.log
没有要求日志切割,因此可以使用FileHandler
;- 两个日志文件的格式不同,因此需要对这两个
handler
分别设置格式器;
代码
1 | import logging |
输出结果:
all.log
:
1 | 2021-07-06 14:45:46,080 - DEBUG - debug message |
error.log
:
1 | 2021-07-06 14:45:46,080 - ERROR - tete.py[line:26] - error message |
配置方式
可以通过以下 3 种方式配置 logging
:
- 使用 Python 代码显式地创建
logger
s,handler
s 和formatter
s 并分别调用它们的配置函数; - 创建一个日志配置文件,然后使用
fileConfig()
函数来读取该文件的内容; - 创建一个包含配置信息的
dict
,然后把它传递给dictConfig()
函数;
第一种方式就是代码实例中的方法,后两种方式可以参考 python之配置日志的几种方式
添加上下文信息
除了传递给日志记录函数的参数外,有时候我们还想在日志输出中包含一些额外的上下文信息。比如,在一个网络应用中,可能希望在日志中记录客户端的特定信息,如:远程客户端的 IP 地址和用户名。有以下几种实现方式:
- 通过向日志记录函数传递一个extra参数引入上下文信息
- 使用LoggerAdapters引入上下文信息
- 使用Filters引入上下文信息
具体说明可以参考 Python之向日志输出中添加上下文信息
本文的主要参考文章如下: