1万字详解 python logging日志模块

By 刘志军 , 2021-12-23, 分类: Python

logging

说到日志,无论是写框架代码还是业务代码,都离不开日志的记录,他能给我们定位问题带来极大的帮助。

记录日志最简单的方法就是在你想要记录的地方加上一句 print , 我相信新手和老鸟都经常这么干。在简单的代码中或者小型项目中这么干一点问题都没有。但是在一些稍大一点的项目,有时候定位一个问题,需要查看历史日志来定位问题,用print就不合时宜了。

为什么这么说?

因为 print 打印出来的日志没有时间,不知道日志记录的位置,也没有可读的日志格式, 还不能把日志输出到指定文件等等。除非这些功能你都全部自己重复造一遍轮子。

最佳的做法是使用内置的logging模块, 因为 logging 模块给开发者提供了非常丰富的功能。

微信截图_20211215005057.png

比如上图就是用标准库logging模块记录生成的日志,它记录了日志的具体时间、日志发生的位置、日志级别以及日志的具体内容等等

怎么用呢,来看个例子

微信截图_20211215005411.png

直接导入logging模块,然后使用logging提供的日志消息记录方法就可以。 仔细看上图你会发现还有两个日志没打印出来。 为什么呢?别急,接着往下看。

日志级别

日志根据重要程度分为5个级别

Level.png

日志级别 使用场景
DEBUG debug级别用来记录详细的信息,方便定位问题进行调试,在生产环境我们一般不开启DEBUG
INFO 用来记录关键代码点的信息,以便代码是否按照我们预期的执行,生产环境通常会设置INFO级别
WARNING 记录某些不预期发生的情况,如磁盘不足
ERROR 由于一个更严重的问题导致某些功能不能正常运行时记录的信息
CRITICAL 当发生严重错误,导致应用程序不能继续运行时记录的信息

日志级别重要程度逐次提高,logging分别提供了5个对应级别的方法。默认情况下日志的级别是WARGING, 低于WARING的日志信息都不会输出。

从上面代码中可以看到loging.warging以后的日志内容都打印在标准输出流,也就是命令行窗口,但是logging.debuginfo记录的日志并没有打印出来。因为默认的日志级别是 warning。

如何修改日志级别

如何让debug级别的信息也输出?

当然是修改默认的日志级别,在开始记录日志前可以使用logging.basicConfig方法来设定日志级别

import logging
logging.basicConfig( level=logging.DEBUG)
logging.debug("this is debug")
logging.info("this is info")
logging.error("this is error")

设置为debug级别后,所有的日志信息都会输出

DEBUG:root:this is debug
INFO:root:this is info
ERROR:root:this is error

如何将日志记录到文件

前面的日志默认会把日志输出到标准输出流,就是只在命令行窗口输出,程序重启后历史日志没地方找,所以把日志永久记录在磁盘是一个很常见的需求。同样通过配置函数logging.basicConfig可以指定日志输出到什么地方

import logging
logging.basicConfig(filename="test.log", level=logging.INFO)
logging.debug("this is debug")
logging.info("this is info")
logging.error("this is error")

这里我指定日志输出到当前目录下的文件test.log中,并指定日志级别为 INFO,运行程序后文件中记录的内容如下:

INFO:root:this is info
ERROR:root:this is error

每次重新运行时,日志会以追加的方式保存, 如果每次运行前要覆盖之前的日志,则需指定 filemode='w', 这个和 open 函数写数据到文件用的参数是一样的。

如何指定日志记录格式

默认输出的格式包含3部分,日志级别、日志记录器的名字、以及日志内容,中间用“:”连接。 如果我们想改变日志格式,例如想加入日期时间、显示日志器名字,我们是可以用format参数来设置日志的格式

import logging
logging.basicConfig(format='%(asctime)s %(levelname)s %(name)s %(message)s')
logging.error("this is error")

输出

2021-12-15 07:44:16,547 ERROR root this is error

日志格式化输出提供了非常多的参数,除了时间、日志级别、日志消息内容、日志记录器的名字个外,还可以指定线程名,进程名,日志记录所在行号等等。

微信截图_20211215075422.png

到这里为止,日志模块的基本用法就这些了,也能满足大部分应用场景,更高级的方法接着往下看,可以帮助你更好的处理日志

记录器(logger)

上一节介绍的日志记录,每一次日志记录其实都是通过一个叫做日志记录器(Logger)的实例对象来负责记录的,每个记录器都有一个名称,当我们直接使用logging来记录日志时,系统会默认创建一个名为 root 的记录器,这个记录器我们称为根记录器。

记录器像树结构一样支持层级,子记录器可以不单独设置日志级别以及Handler(后面会介绍),如果子记录器没有单独设置,则它的所有行为会委托给父级。也就是说如果儿子没开口时老子说了算。

image-20230611220845854

记录器名称可以是任意名称,不过最佳实践是直接用模块的名称__name__当作记录器的名字。命名如下

# a.py
import logging
logger = logging.getLogger(__name__)
print(logger)

logger2 = logging.getLogger("test")

如果a.py作为程序的入口文件执行时,logger的名字是__main__, 如果a.py作为模块被导入到其它模块中使用,那么该logger的名字就是a

默认情况下,记录器采用树状层级结构,用句点.作为分隔符排列在命名空间的层次结构中。层次结构中位于下方的记录器是上面记录器的子级。例如,如果有个名叫 foo 的记录器,而名字是 foo.barfoo.bar.baz,和 foo.bam 的记录器都是 foo 的子级。

image-20230611233037786

root.png

main.py 为入口文件

import foo
from foo import bar

if __name__ == '__main__':
    pass
# foo/__init__.py

import logging

logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

logger.info("this is foo")

这里我只设置foo这个记录器的级别为INFO

foo/bar/__init__.py
import logging

logger = logging.getLogger(__name__)
logger.info("this is bar")

foo模块下的子模块bar我并没有设置日志级别,最后的输出结果是

INFO:foo:this is foo
INFO:foo.bar:this is bar

这是因为 foo.bar 这个记录器没有设置日志级别,就会向上找到已经设置了日志级别的祖先,这里刚好找到父记录器foo的级别为INFO,如果foo也没设置,就会找到根记录器root,root默认的级别为WARGING。

处理器(Handler)

记录器专门负责日志的记录,但是日志最终记录在哪里记录器并不关心,而是交给了另一个家伙--处理器(Handler)去处理。

例如一个Flask项目,你可能会将INFO级别的日志记录到文件,将ERROR级别的日志记录到标准输出,将某些关键日志(例如有订单或者严重错误)发送到某个邮件地址通知老板。这时候你的记录器可以添加多个不同的处理器来处理不同的消息日志,根据消息的重要性发送的特定的位置。

logger.png

Python内置模块中有很多实用的处理器,常用的有:

1、StreamHandler 标准流处理器,将日志消息发送到标准输出流、错误流 2、FileHandler 文件处理器,将日志消息发送到文件 3、RotatingFileHandler 文件处理器,文件达到指定大小后,启用新文件存储日志
4、TimedRotatingFileHandler 文件处理器,日志以特定的时间间隔轮换日志文件

处理器的常用方法

Handler.png

Handler 提供了4个方法给开发者使用,细心的你可以发现了,记录器(Logger)可以设置日志级别 level,Handler也可以设置Level。通过setLevel可以将记录器记录的不同级别的消息发送到不同的地方去。

例如将INFO级别的日志记录在文件中,将WARING日志直接输出到标准流,这时候我们只需要将该记录器绑定两个不同的处理器即可实现。

import logging
from logging import StreamHandler
from logging import FileHandler

logger = logging.getLogger(__name__)

# 将记录器设置为DEBUG级别
logger.setLevel(logging.DEBUG)

# 标准流处理器,设置的级别为WARAING
stream_handler = StreamHandler()
stream_handler.setLevel(logging.WARNING)

# 将处理器绑定到记录器中
logger.addHandler(stream_handler)

# 文件处理器,设置的级别为INFO
file_handler = FileHandler(filename="test.log")
file_handler.setLevel(logging.INFO)

# 将处理器绑定到记录器中
logger.addHandler(file_handler)

logger.debug("this is debug")
logger.info("this is info")
logger.error("this is error")
logger.warning("this is warning")

运行后,在命令行窗口输出的日志内容是:

this is error
this is warning

输出在文件的日志内容是:

this is info
this is error
this is warning

虽然我们将logger的级别设置为了DEBUG,但是debug记录的消息并没有输出,这是因为我给两个Handler设置的级别都比DEBUG要高,所以这条消息被过滤掉了。

格式器(formatter)

格式器在前面部分其实已经有所介绍,不过是通过logging.basicConfig来指定的,其实格式器还可以以对象的形式来设置在Handler上。格式器可以指定日志的输出格式,要不要展示时间,时间格式什么,要不要展示日志的级别,要不要展示记录器的名字等等,都可以通过一个格式器对消息进行格式化输出。

import logging
from logging import StreamHandler

logger = logging.getLogger(__name__)

# 标准流处理器
stream_handler = StreamHandler()
stream_handler.setLevel(logging.WARNING)

# 创建一个格式器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 作用在handler上
stream_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(stream_handler)

logger.info("this is info")
logger.error("this is error")
logger.warning("this is warning")

注意,格式器只能作用在处理器上,通过处理器的setFromatter方法设置格式器。注意一个Handler只能设置一个格式器。是一对一的关系。而 logger 与 handler 是一对多的关系,一个logger可以添加多个handler。 handler 和 logger 都可以设置日志的等级。

微信截图_20211221011251.png

logging.basicConfig

回到最开始的地方,logging.basicConfig() 方法为我们干了啥?现在你大概能猜出来了。来看python源码中是怎么说的

Do basic configuration for the logging system.

This function does nothing if the root logger already has handlers configured. It is a convenience method intended for use by simple scripts to do one-shot configuration of the logging package.

The default behaviour is to create a StreamHandler which writes to sys.stderr, set a formatter using the BASIC_FORMAT format string, and add the handler to the root logger.

A number of optional keyword arguments may be specified, which can alter the default behaviour.

1、创建一个root记录器
2、设置root的日志级别为warning
3、为root记录器添加StreamHandler处理器
4、为处理器设置一个简单格式器

logging.basicConfig()
logging.warning("hello")

这两行代码其实就等价于:

import sys
import logging
from logging import StreamHandler
from logging import Formatter


logger = logging.getLogger("root")
logger.setLevel(logging.WARNING)
handler = StreamHandler(sys.stderr)
logger.addHandler(handler)
formatter = Formatter(" %(levelname)s:%(name)s:%(message)s")
handler.setFormatter(formatter)
logger.warning("hello")

logging.basicConfig 方法做的事情是相当于给日志系统做一个最基本的配置,方便开发者快速接入使用,如果 root 记录器已经指定有其它处理器,这时候你再调用basciConfig,则该方式将失效,它什么都不做。

给大家留一道思考题:

下面这段代码会输出日志吗?为什么?

import logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.info("this is foo")

日志配置

日志的配置除了前面介绍的将配置直接写在代码中,还可以将配置信息单独放在配置文件中,实现配置与代码分离。

日志配置文件 logging.conf

[loggers]
keys=root

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler


[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

加载配置文件

import logging
import logging.config

# 加载配置
logging.config.fileConfig('logging.conf')

# 创建 logger
logger = logging.getLogger()

# 应用代码
logger.debug("debug message")
logger.info("info message")
logger.warning("warning message")
logger.error("error message")

输出

2021-12-23 00:02:07,019 - root - DEBUG - debug message
2021-12-23 00:02:07,019 - root - INFO - info message
2021-12-23 00:02:07,019 - root - WARNING - warning message
2021-12-23 00:02:07,019 - root - ERROR - error message

参考链接:

https://docs.python.org/3/library/logging.html#

https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial

https://awaywithideas.com/python-logging-a-practical-guide/

https://rmcomplexity.com/article/2020/12/01/introduction-to-python-logging.html


关注公众号「Python之禅」,回复「1024」免费获取Python资源

python之禅

猜你喜欢

2022-01-07
python logging 日志模块续