【译】Python3.8官方Logging文档--高级部分

在《后端问题如何快速定位?》中老张强调了日志的重要性,也提到了使用日志需要的注意的一些问题。但是并没有提到实践方式,其实掌握一个工具最快的方法就是阅读官方文档,老张试着翻译了Python3.8官方Logging文档,由于官方文档实在是长,所以会分成两到三篇文章。官方文档给出了非常详细的设计思想、实践方式,相信你读完肯定会非常受用。

Logging库包含了模块化的方法,提供包含loggers, handlers, filters以及formatters在内的若干组件:

  • loggers对外暴露了可以直接使用的接口。

  • handlers处理日志记录(由logger生产)的流向。

  • filters很便捷的决定日志记录是否能够被输出。

  • formatters包含了日志记录的输出格式。

事件信息会以LogRecord实例的形式在loggers、handlers、filters以及formatters之间传递。

Logging的入口为Logger类(以下统称loggers)的实例方法。每个实例都有一个命名,他们共同存在于一个由点符号分割的命名空间内。举个例子,一个被命名为‘scan’的logger实例是‘scan.text’实例的上层实例。你可以随意命名logger实例,并且可以在日志消息里面显示调用程序的层级。

一个比较好的做法是利用模块层级来组织logger实例的命名,模块里面的命名方式如下:

logger = logging.getLogger(__name__)

这样使得logger的命名可以正确反映包(以及模块)的层级,使得可以通过日志记录里面的logger命名直观的追溯到代码位置。

在层级根部实例化的logger被命名为root。同所有logger实例一样,root实例提供了debug(), info(), warning(), error()以及critical()函数。这些函数共享签名。root实例在打印日志信息时会携带‘root’字符串。

一条日志消息可以有多个流向。logging包提供的处理方式包括:写入文件、发送Get或者Post形式的HTTP报文、SMTP形式的电子邮件、通用的套接字、队列以及不同操作系统的日志处理机制(诸如系统日志、Windows NT事件日志)。日志消息的流向由handler类来处理。如果内置的handler类不能满足你的需求,你也可以自定义handler。

缺省状态下并不会设置任何日志流向。你可以通过《基础部分》里面提到的basicConfig()函数设置诸如控制台、文件在内的日志流向。当你调用debug()等日志方法时,这些方法会检查你是否指定了日志流向,如果你没有指定的话,这些方法会默认指定控制台(sys.stderr)为日志流向、使用默认的日志格式,然后才将日志消息传递到logger类的root实例,最终生成你看到的日志消息。

basicConfig()的缺省日志格式为:

severity:logger name:message

你也可以显式的通过format参数显式的的指定日志格式。关于日志格式的构造选项,请参考过于formatter类的文档说明。

Logging包的处理流程

下图是关于一条日志消息在loggers和handlers之间怎样被处理的流程图:

Loggers

logger对象有三部的工作。第一,它对外暴露了若干方法,使得外部程序可以在运行的时候记录日志信息。第二,logger对象可以根据日志级别来决定是否需要过滤掉一条日志消息。第三,logger对象会将日志消息传递给已关联的handlers。

logger对象有两类使用最广泛的方法:配置以及发送日志消息。

下面是最常见的配置方式:

  • Logger.setLevel()可以配置允许生效的最低级别。在内置的日志级别中,DEBUG级别是最低级的,CRITICAL是最高级别。举个例子,配置的级别是INFO,那么logger实例只会处理INFO、WARNING、ERROR以及CRITICAL级别的日志消息,而DEBUG级别的会被过滤掉。

  • Logger.addHandler()和Logger.removerHandler()为logger实例提供了增、删handler对象的途径。稍后会详细介绍handler对象。

  • Logger.addFilter()和Logger.removerFilter()为logger实例提供了增、删filter对象的途径。

你并不需要每次创建logger实例时都调用它们。

对于给定的logger实例,下面的方法会生产一条日志消息:

  • Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), 以及 Logger.critical()都会生成一条日志记录,该记录包含日志消息和方法名对应的日志级别。这个消息实际上是一个格式化的字符串,可能包含标准的字符串格式符符号(比如  %s, %d, %f等)。剩下的入参可能包含一些在日志消息中预格式化的对象。对于**kwargs形式的关键字入参,日志函数只关心exc_info对应的变量值,它将决定是否记录异常信息。

  • Logger.exception()和Logger.error()生成的日志消息相似,他们的区别在Logger.exception()携带栈信息。确保只在异常处理时调用该函数。

  • Logger.log()需要指定日志级别作为入参。相比上面提到的开箱即用的日志函数,它显得有些繁琐,但是可以适用于需要自定义日志级别的场景。

Handlers

handler对象负责根据日志级别分配日志消息的最终流向。Logger对象默认不包含handler对象,但是可用通过addHandler()方法添加。拿一个应用场景来举例:假设你的应用程序需要将所有日志消息保存在日志文件中;把ERROR级别及以上的日志打印在标准输出;所有的CIRITCAL级别的日志通过电子邮件发给你。整个场景需要三个handler实例,每个实例都会根据不同的日志级别采取不同的方式处理日志消息。

标准库只内置少量的handler类型;本文档主要拿StreamHandler和FileHandler来举例。

开发人员只需要关心Handler对象的少数几个方法。在内置的handler对象(非自定义的handler)里面,跟开发人员密切相关的配置方法如下所示:

  • setLevel()方法跟logger对象的方法一样,配置handler会处理的最低日志级别。为什么会有两个setLevel()方法呢?logger对象设置的日志级别决定了日志能够被传递到handler对象。而handler对象设置的日志级别决定了日志消息是否会被记录下来。

  • setFormaterr()可以为handler对象配置Formatter对象。

  • addFilter()和removerFilter()可以增删filter对象。

应用程序不应该直接实例化Handler对象。因为Handler对象是一个基类,它定义了所有Handler子类都应该继承或者复写的接口方法。

Formatters

formatter对象决定了一条的日志消息的顺序、结构以及内容。不同于logger.Handler是基类,应用程序需要自己实例化Formatter类。当然如果你有特殊需求,也可以实例化Formatter的子类。它接收三个参数:

  • 一个预格式化的消息字符串

  • 一个预格式化的时间字符串

  • 一个类型符号

logging.Formatter.__init__(fmt=None, datefmt=None, style='%')

如果没有显式的传入消息格式,会使用缺省设置。如果没有显式的传入时间格式,缺省的时间格式如下:

%Y-%m-%d %H:%M:%S

并且会在后面追加毫秒。类型符号可以选择‘%’、‘{'或者’$‘。缺省的类型符号为’%’。

  • 如果类型符号为‘%’,日志消息的格式化方式采用%(<dictionary key>)s的替换方式;可用的键值请单独翻阅LogRecord的说明。

  • 如果类型符号为‘{’,日志消息的格式化方式采用与str.formate()方法兼容的处理(也就是使用关键字)。

  • 如果类型符号为‘$’,日志消息的格式化方式需要与string.Template.substitute()方法保持一致。

3.2版本的改动说明:增加了style入参。

'%(asctime)s - %(levelname)s - %(message)s'

上面是一个带有时间的可读性高的预格式化方式,日志级别和日志内容被有序的添加在里面。

formatters提供了用户可配置的函数,方便日志生成时间转化为时间元组。默认的是使用time.localtime()。如果想要在formatter实例中自定义,可以通过给 converter属性赋值的形式改变默认行为,需要注意的是新赋的值需要是同time.localtime()或者time.gmtime()签名一致的函数。假设这么一个场景:你需要是所有的日志时间都展示为GMT时区,你可以将Formatter的 converter属性赋值为time.gmtime()的形式,改变所有formatter实例行为。

Logging配置

开发人员可以通过以下三种配置来配置logging:

  1. 在代码中使用前面提到的方式在代码中依次创建loggers、handlers以及formatters对象。

  2. 新建一个配置文件,并通过fileConfig()函数载入配置。

  3. 新建一个配置文件夹,并通过dictConfig()函数载入配置。

关于后两种配置方式更详细的说明,请自行查阅Configuration函数文档。下面是Python代码示例,它包含了一个简单的logger实例、一个控制台handler和一个简单的formatter:

import logging


# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)


# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)


# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')


# add formatter to ch
ch.setFormatter(formatter)


# add ch to logger
logger.addHandler(ch)


# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

运行结果如下:

$ python simple_logging_module.py
2005-03-19 15:10:26,618 - simple_example - DEBUG - debug message
2005-03-19 15:10:26,620 - simple_example - INFO - info message
2005-03-19 15:10:26,695 - simple_example - WARNING - warn message
2005-03-19 15:10:26,697 - simple_example - ERROR - error message
2005-03-19 15:10:26,773 - simple_example - CRITICAL - critical message

下面代码的效果同上面一样,也是新建logger实例、handler实例和formatter实例,唯一不同的地方是对象的命名。

import logging
import logging.config


logging.config.fileConfig('logging.conf')


# create logger
logger = logging.getLogger('simpleExample')


# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

logging.conf文件的内容如下:

[loggers]
keys=root,simpleExample


[handlers]
keys=consoleHandler


[formatters]
keys=simpleFormatter


[logger_root]
level=DEBUG
handlers=consoleHandler


[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0


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


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

执行之后的输出信息同不用配置文件的差不多:

$ python simple_logging_config.py
2005-03-19 15:38:55,977 - simpleExample - DEBUG - debug message
2005-03-19 15:38:55,979 - simpleExample - INFO - info message
2005-03-19 15:38:56,054 - simpleExample - WARNING - warn message
2005-03-19 15:38:56,055 - simpleExample - ERROR - error message
2005-03-19 15:38:56,130 - simpleExample - CRITICAL - critical message

能够很明显看到的是,配置文件的内容格式相对Python代码而言有一些简化的地方。像这样将配置和代码分离,能够帮助没有开发经验的用户更好的配置日志行为。


警告:fileConfig函数有一个特殊的入参——disable_existing_loggers。为了保持向后兼容,其默认值为True。这个参数导致的行为可能会让你困惑,它会使得在fileConfig()函数调用之前已存在的非root的logger实例失效,除非某个logger同配置文件中的配置同名。如果需要关于它的更多细节,可以自行查阅相关说明。当然你也可以根据实际需要显式的传入False。

需要注意的是配置文件提到的接口引用,要么必须是logging包内部的,要么是可通过import导入的绝对路径。举个例子,你可以使用WatchedFileHandler(logging包内部),你也可以用mypackage.mymodule.MyHandler(一个在mypackage包--mymodule模块定义的类,当然整个路径必须能够被import正确导入)。

从Python3.2开始,引入了一种新的日志配置方式--通过目录组织配置信息。它能够提供上面其他方式更强大的功能,推荐开发人员在新建项目时使用这种方式。因为文件夹的配置方式除了正常的配置之外,还可以根据不同的用途灵活的移动文件夹。举个例子,你可以使用JSON格式添加配置信息,如果你之前接触过YAML程序开发,你也使用YAML格式。当然,你也可以选择Python代码的配置方式、接收套接字的配置方式,或者其他你认为方便的方式。

下面是基于文件夹配置使用YAML格式的配置示例,效果跟之前的示例一样:

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
    stream: ext://sys.stdout
loggers:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

如果需要关于文件夹配置的更多资料,可以自行查阅Configration的说明。

如果没有提供配置信息会发生什么

如果没有提供配置信息,大概率会在打印日志事件的时候发现没有handlers实例可用。当然,实际会发生什么跟Python版本有关。

对于Python3.2之前的版本,最后会发生:

  • 如果logging.raiseExceptions选项为False(线上环境),日志会被丢弃。

  • 如果logging.raiseExceptions选项为True(开发环境),会在控制台输出‘No handlers could be found for logger X.Y.Z’ 

Python3.2及以后的版本,最后会发生:

  • 日志事件会通过loggin.lastResort对象中的兜底handler处理。

    这个内部的handler并没有被任何logger使用,它的效果跟StreamHandler一样,会将日志消息输出到sys.stderr(所以需要你谨慎的对待可能被改动的重定向)。

    它只会打印出来日志消息,并没有携带任何格式。

    这个handler的默认级别是DEBUG,所以基本上任何消息都会被打印出来。

如果你希望禁用3.2版本之后的默认行为,可以将logging.lastResort显式的的设置为None。

在库中使用logging

如果你会在自己开发的库里面使用logging,那么你需要仔细确认库是如何使用logging的,举个例子:关于loggers的名字。仔细确认如何配置logging是必须的。如果调用方没有使用logging,但是库内部使用了logging,WARNING级别及以上的日志消息将会输出到sys.stderr。看过了之前的介绍,你应该知道这是默认行为。

如果你期望在没有主动配置的情况不输出这些日志消息,你可以给你的库里面最高层的logger实例添加一个没有任何操作的handler。这样就可以避免日志被打印出来,原因就是对于库内部而言日志消息都会交给这个handler处理,但是这个handler并不会打印输出任何东西。一旦,调用程序主动配置添加了handler对象,你的库内部产生日志消息也会被处理,如果配置的日志级别适配,那么消息将会被打印输出。

在Python3.1版本之后,NullHandler被引入,但是它实际上并不会对日志消息做任何处理。如果你不希望你的库内部日志在配置缺失的情况下被输出到sys.stderr,你可以实例化一个NullHandler,并把它添加到顶层的logger实例。举个例子,如果一个名为foo的库实例化了诸如‘foo.x’、‘foo.x.y’的logger,你可以这样:

import logging
logging.getLogger('foo').addHandler(logging.NullHandler())

在一个组织提供了多个库的场景下,这样做使得实际当中logger的命名为orgname.foo而不是foo。

提示:对于库,除了NullHandler之外,不要再添加任何其他handler。这是因为你应该把控制权交给调用方。只有调用方结合自己的实际情况能够决定使用什么样的handler。如果你在库里面添加了handler,你可能会影响调用方的单元测试结果,并且输出一些他们不需要的东西。

点击下方标题查看文档的基础部分:

【译】Python3.8官方Logging文档--基础部分

在看点这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值