【旁门Python03】如何正确使用Python logging库

前言

距离上一篇博客也是一年半过去了,真的是鸽了非常久…最近在学习一个开源项目的时候对logging有了更深的理解,决定上班摸鱼写篇博客来讲讲。

logging库的好处

python新手可能会觉得:我有print函数了,啥不能解决?诚然,写代码的都知道print大法好,没事儿print/console.log/std::cout一下有益身心健康。但是logging库给予了我们一个灵活且标准的事件记录系统——所有的Python模组都能参与到logging当中来,所以当你在编写自己的项目且用到第三方库的时候,logging是非常统一的;此外,logging还提供了事件信息的格式化输出、输出目的地选择、输出信息的过滤等功能,这些要用print去一一实现可是很费时间的。

基础教学

最最基本的logging使用方式,就是import它,然后调用内置的函数就可以。下面5个函数的名字,正好对应了信息的严重性等级:

import logging
logging.debug("This is a debug message") # 输出DEBUG级别记录,一般用于问题诊断
logging.info("This is an info message") # 输出INFO级别记录,用于记录程序正常运行
logging.warning("This is a warning message") # 输出WARNING级别记录,用于记录运行出现的异常/需要注意的地方,但不影响运行
logging.error("This is an error message") # 输出ERROR级别记录,一般意味着程序不能执行某些功能,比如某函数执行错误等
logging.critical("This is a critical message") # 输出CRITICAL级别记录,表明程序已经不能继续运行

可以看到输出:

WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message

为什么尝试了5个函数,输出只有3条记录?那是因为logging库的默认输出级别是WARNING的,所以DEBUG/INFO级别的消息就被过滤了。后面会介绍如何配置logging来避免这种过滤。
再看一下默认消息的格式:[level]:[logger]:[message]分别代表了消息的严重性等级、logger和消息本身。Logger是真正执行记录动作的对象,后面也会讲。这里只需要知道,我们在调用以上5个函数时,是root logger完成了记录动作。

进阶:配置logging行为

接下来我们想对logging的行为进行配置,完成诸如log到文件里、更改消息格式这样的行为。这里需要用到一个函数:logging.basicConfig。它接受一些键值对,私底下为root logger完成一些配置。比较常用的key有:

key 释义
*filename 一串文件名,表示记录到该文件内
*filemode 文件打开的模式,默认为‘a’,即为内容追加
*level logging的严重性等级
*format logging输出消息的格式的配置字符串,具体格式见下面Formatter内容
*datefmt 描述日期和时间的格式的字符串,具体格式见time.strftime
*encoding 3.9新加的参数,描述记录到文件时候用的编码,比如’utf-8’
style 当format被指定时,用这种方式进行字符串格式化。可使用‘%‘或者‘{’或者‘$’。默认为‘%’。
stream 使用指定的流来初始化流处理器(StreamHandler)。与filename不兼容
handlers 使用的一系列处理器(Handler)。与stream与filename不兼容
force 布尔值,如果为True,则root logger里面已存在的处理器全部都被删除,然后再执行配置
errors 如果在使用filename的同时使用了这个键,则它的键值会在创建文件处理器(FileHandler)时候用到

上面打了*号的键,是我们比较常用的;剩下的那些要么不常用,要么得随着深入了解logging才会理解其含义。

需要注意的点:

  • logging.basicConfig函数必须在任何debug()info()这样的函数之前,不然这些函数会自己用默认参数调用basicConfig
  • 在多线程情况下,logging.basicConfig必须在主线程里调用,且在其他子线程启动之前。希望大家永远不要感受多线程/多进程的痛苦。

我们对基础教学里的例子做点改进:

import logging
logging.basicConfig(
	filename="hello_world.log",
	level=logging.INFO,
	format="[%(levelname)s] - %(message)s"
) # 配置了3样东西:记录到hello_world.log文件里,记录的信息严重性级别是INFO,并让格式化输出的样式变成只包含严重性和具体信息。
logging.info("This is an info message")
logging.warning("This is a warning message")

然后我们执行一下,就可以看见在hello_world.log文件中出现了这样的记录:

[INFO] - This is an info message
[WARNING] - This is a warning message

到这里为止,你所了解的logging已经足以应对绝大多数情况下的使用。如果想再进一步了解logging的细致内容,还请继续往下看。

高级:Logger和Handler

接下来我们讨论logger和handler。
一言以蔽之:logger是我们的应用程序调用的接口,handler则将logger创建的信息发送到各自该去的位置。

Logger类

在基础教学里,我们提到了logging.infologging.debug这样的函数其实是去调用了root logger完成动作。在一个复杂的工程项目中,logger是有层级的,位于最顶端的始终是root logger。

  • 如果项目是一个独立的存在(比如很多常见的深度学习模型库,自带了训练、评估等功能),那么每个文件夹下的每个文件都可以拥有自己的logger来专门负责本文件内所有的logging工作;
  • 如果项目需要作为一个第三方module被其他人所使用,那可以有一个唯一的、module级别的logger,把它在不同的函数中传递来去,完成logging动作。

怎么创建一个logger呢?标准的操作是这样的:

import logging

logger = logging.getLogger(__name__) # logger作为当前文件下的全局变量存在

def some_function():
	logger.info("In some_function")
	# do something here
	logger.debug("Out of some_function")

getLogger函数接受一个字符串作为logger的名字。而如果用__name__来命名的话,logger的名字就和项目里面的文件层级保持一致了,因为__name__返回的就是在Python命名空间下文件的名字,诸如apple.banana.cherry
这样做的好处就是我们可以通过记录下来的信息,得知是哪个文件的logger在起作用,从而理解程序运行到了什么地步。
注意:Python官方建议永远不要直接生成logger实例,而要使用getLogger函数。用同样的名字多次调用getLogger永远返回的是同一个logger实例,而直接生成就不是了。

Handler类

Handler,处理器,负责把记录信息分发到对应的目的地去(比如终端输出,文件,网口甚至email等)。讲得通俗点,大概就是下面这样:

logger:哎哎哎我这里收到一条记录,你们哪个handler来负责处理一下?
handler A:这条记录不符合我的规则,我就不管了。
handler B:哎这条符合我的规则,我来接手好了,我给发到终端里去。
handler C:这条也符合我的规则,我也来处理一下,我会写到文件里头去。

Logger类可以通过类方法addHandlerremoveHandler来为自己增减处理器。一般来说,用户不需要直接创立logging.Hanlder的实例,而是创立下面这些非常有用的、继承自logging.Handler的处理器实例完成工作。

logging.StreamHandler

将信息输出到sys.stdout、sys.stderr或是支持write()flush()的实例中。这是我们最常用的handler之一,也是logging库进行默认配置的时候会用的handler。

import logging
import sys
logger = logging.getLogger(__name__)
h = logging.StreamHandler(sys.stdout) # 输出到stdout
logger.addHandler(h) # 将handler添加到当前的logger中。这样logger在记录消息时就会自动调用这个handler。
print(logger.handlers)

logging.FileHandler

class logging.FileHandler(filename, mode='a', encoding=None, delay=False, errors=None)
将信息输出到一个文本文件中。也是一个非常好用的handler,在执行长时间的计算任务、记录服务器动作时都能用上。

logging.SocketHandler

class logging.handlers.SocketHandler(host, port)
将信息发送到某个指定的TCP端口中,这样监听端口的程序可以收到消息。在涉及网络服务的程序中用得比较多。

logging库还提供了其他很有趣的Handler。可以在这份官方指南中找到适合你的应用的handler。

出师:Formatter和Filter

一般的项目兴许还真不用细致到这一部分。能够熟练使用Handler和Logger已经足以让你在日常工作中所向披靡。但既然是写技术小文章,那就得对得起自己起的标题……看看也不赖,说不定哪天可以拿出来在同学或同事面前装个逼。

Formatter

Formatter,格式器,是用来格式化需要记录的信息字符串的。换言之:让记录变得好看点、更有章法一点。你可以创建Formatter,把它添加到一个Handler中,从而为它制定独特的记录格式,如时分秒、信息等级、信息来源、信息内容等等。

format_string = "%(asctime)s %(levelname)-2s {%(pathname)s:%(lineno)d}  %(message)s"
formatter = logging.Formatter(format_string)
handler.setFormatter(formatter)

没看懂例子里format_string是怎么来的?它用来表示一条消息应该怎么被组织起来,而%()中的内容是logging库规定好的,可以在LogRecord attributes中找到可用的值和对应的意思。这个例子里,我们的handler生成的一条消息会依次包含记录发生的时间、记录等级、产生log的文件路径、源文件第几行、log信息本体。我们在logging.basicConfig里面也遇见过名为format的键值,其实底层干的事情是一样的。

有些人喜欢消息约详尽越好,有些则喜欢精练的消息记录,这就萝卜青菜了。

Filter

Filter,过滤器,用来过滤一下信息,确定是否需要将其记录下来。让我们来细化一下先前那个通俗例子:

logger:哎哎哎我这里收到一条记录,你们哪个handler来负责处理一下?
handler A:稍等,让我咨询一下我的filter。(对Filter)兄弟,我需不需要记录这条消息啊?
handler A的filter:让我瞅瞅……嗯不需要。
handler A:(对外)这条记录不符合我的规则,我就不管了。

很好理解,对吧?更好的是,得益于Python的duck typing特性,我们可以随心所欲创建自己的filter,甚至不需要继承自logging.Filter父类。只需要满足你的filter类中包含filter这个函数就行了。下面是个例子:

# 自建的Filter类,没有继承logging.Filter,但是实现了filter函数。
# filter的输入是一条LogRecord,输出为0时不会记录该消息,输出非0时则反之。事实上你还可以在这里修改消息记录,只要你觉得合适。
class MyFilter(object):
	def __init__(self):
		pass
	def filter(record: logging.LogRecord) -> int:
		return int(record.msg.contains("Hello world")) # 当记录里包含Hello world字样时就记录,否则就不记录。

样例

这里,我们来参考一个开源项目对logging配置的样例,以便大家对此有更好的理解。碍于篇幅,我们就来看其中小部分。想拿来用的记得引用一下原项目,这是个好习惯。

class LogHandlerConfig:
    """配至Handler用的config结构"""
    def __init__(self, level: str, path: Optional[str] = None, filter_regexp: str = '') -> None:
        """
        :param level: logging的等级阈值,比如info
        :param path: log输出的文件地址。如果不填,那就输出到terminal里。
        :param filter_regexp: filter的正则表达式。用于过滤信息
        """
        self.level = level
        self.path = path
        self.filter_regexp = filter_regexp

        if self.path is not None:
            # Create the directory if not present already.
            _dir = os.path.dirname(self.path)
            if not os.path.exists(_dir):
                os.makedirs(_dir)

def configure_logger(
    handler_configs: List[LogHandlerConfig],
    format_str: str = '%(asctime)s %(levelname)-2s {%(pathname)s:%(lineno)d}  %(message)s',
) -> logging.Logger:
    """
    配置python默认的logger
    :param handler_configs: 一个列表的LogHandlerConfig。
    :param format_str: 格式化字符串,给Formatter用的。
    :return: 一个logger对象.
    """
    # 上来先清除掉logger中所有旧的Handler
    logger = logging.getLogger()
    for old_handler in logger.handlers:
        logger.removeHandler(old_handler)

    # 按照列表里的config,依次创建Handler,添加到logger里头去。
    for config in handler_configs:
    	# 如果config里没指定文件路径,那就默认输出到terminal里
        if not config.path:
            handler = logging.StramHandler(sys.stdout)
        else:
            handler = logging.FileHandler(config.path)
        handler.setLevel(LOGGING_LEVEL_MAP[config.level]) # 指定信息的严重性等级
        handler.setFormatter(logging.Formatter(format_str)) # 指定信息的格式
        handler.addFilter(PathKeywordMatch(config.filter_regexp)) # 指定信息的过滤器。是一个正则表达匹配。
        logger.addHandler(handler) # 添加到logger里。

    return logger

结尾

按照上手的顺序,我们遍历了一遍logging库里面所有主要的部件。配上一些例子,相信屏幕前的你对logging已经掌握了六七十了。毕竟它本身也不是个复杂高深的库。这篇就写到这里吧。谢谢各位~

参考文档

  1. logging.basicConfig的配置键值查询:https://docs.python.org/3/library/logging.html#logging.basicConfig
  2. 可用的Handlers以及用法查询:https://docs.python.org/3/library/logging.handlers.html#module-logging.handlers
  3. Formatter格式化字符串可用的键值查询:https://docs.python.org/3/library/logging.html#logrecord-attributes

你可能感兴趣的:(编程,python,开发语言)