距离上一篇博客也是一年半过去了,真的是鸽了非常久…最近在学习一个开源项目的时候对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的行为进行配置,完成诸如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创建的信息发送到各自该去的位置。
在基础教学里,我们提到了logging.info
,logging.debug
这样的函数其实是去调用了root logger完成动作。在一个复杂的工程项目中,logger是有层级的,位于最顶端的始终是root logger。
怎么创建一个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,处理器,负责把记录信息分发到对应的目的地去(比如终端输出,文件,网口甚至email等)。讲得通俗点,大概就是下面这样:
logger:哎哎哎我这里收到一条记录,你们哪个handler来负责处理一下?
handler A:这条记录不符合我的规则,我就不管了。
handler B:哎这条符合我的规则,我来接手好了,我给发到终端里去。
handler C:这条也符合我的规则,我也来处理一下,我会写到文件里头去。
Logger类可以通过类方法addHandler
和removeHandler
来为自己增减处理器。一般来说,用户不需要直接创立logging.Hanlder
的实例,而是创立下面这些非常有用的、继承自logging.Handler
的处理器实例完成工作。
将信息输出到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)
class logging.FileHandler(filename, mode='a', encoding=None, delay=False, errors=None)
将信息输出到一个文本文件中。也是一个非常好用的handler,在执行长时间的计算任务、记录服务器动作时都能用上。
class logging.handlers.SocketHandler(host, port)
将信息发送到某个指定的TCP端口中,这样监听端口的程序可以收到消息。在涉及网络服务的程序中用得比较多。
logging
库还提供了其他很有趣的Handler。可以在这份官方指南中找到适合你的应用的handler。
一般的项目兴许还真不用细致到这一部分。能够熟练使用Handler和Logger已经足以让你在日常工作中所向披靡。但既然是写技术小文章,那就得对得起自己起的标题……看看也不赖,说不定哪天可以拿出来在同学或同事面前装个逼。
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,过滤器,用来过滤一下信息,确定是否需要将其记录下来。让我们来细化一下先前那个通俗例子:
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已经掌握了六七十了。毕竟它本身也不是个复杂高深的库。这篇就写到这里吧。谢谢各位~
logging.basicConfig
的配置键值查询:https://docs.python.org/3/library/logging.html#logging.basicConfig