在《后端问题如何快速定位?》中老张强调了日志的重要性,也提到了使用日志需要的注意的一些问题。但是并没有提到实践方式,其实掌握一个工具最快的方法就是阅读官方文档,老张试着翻译了Python3.8官方Logging文档,由于官方文档实在是长,所以会分成两到三篇文章。官方文档给出了非常详细的设计思想、实践方式,相信你读完肯定会非常受用。
Logging库包含了模块化的方法,提供包含loggers, handlers, filters以及formatters在内的若干组件:
loggers对外暴露了可以直接使用的接口。
handlers处理日志记录(由logger生产)的流向。
filters很便捷的决定日志记录是否能够被输出。
formatters包含了日志记录的输出格式。
logger = logging.getLogger(__name__)
severity:logger name:message
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.debug(), Logger.info(), Logger.warning(), Logger.error(), 以及 Logger.critical()都会生成一条日志记录,该记录包含日志消息和方法名对应的日志级别。这个消息实际上是一个格式化的字符串,可能包含标准的字符串格式符符号(比如 %s, %d, %f等)。剩下的入参可能包含一些在日志消息中预格式化的对象。对于**kwargs形式的关键字入参,日志函数只关心exc_info对应的变量值,它将决定是否记录异常信息。
Logger.exception()和Logger.error()生成的日志消息相似,他们的区别在Logger.exception()携带栈信息。确保只在异常处理时调用该函数。
Logger.log()需要指定日志级别作为入参。相比上面提到的开箱即用的日志函数,它显得有些繁琐,但是可以适用于需要自定义日志级别的场景。
setLevel()方法跟logger对象的方法一样,配置handler会处理的最低日志级别。为什么会有两个setLevel()方法呢?logger对象设置的日志级别决定了日志能够被传递到handler对象。而handler对象设置的日志级别决定了日志消息是否会被记录下来。
setFormaterr()可以为handler对象配置Formatter对象。
addFilter()和removerFilter()可以增删filter对象。
一个预格式化的消息字符串
一个预格式化的时间字符串
一个类型符号
logging.Formatter.__init__(fmt=None, datefmt=None, style='%')
%Y-%m-%d %H:%M:%S
如果类型符号为‘%’,日志消息的格式化方式采用%(<dictionary key>)s的替换方式;可用的键值请单独翻阅LogRecord的说明。
如果类型符号为‘{’,日志消息的格式化方式采用与str.formate()方法兼容的处理(也就是使用关键字)。
如果类型符号为‘$’,日志消息的格式化方式需要与string.Template.substitute()方法保持一致。
'%(asctime)s - %(levelname)s - %(message)s'
converter
属性赋值的形式改变默认行为,需要注意的是新赋的值需要是同time.localtime()或者time.gmtime()签名一致的函数。假设这么一个场景:你需要是所有的日志时间都展示为GMT时区,你可以将Formatter的 converter
属性赋值为time.gmtime()的形式,改变所有formatter实例行为。在代码中使用前面提到的方式在代码中依次创建loggers、handlers以及formatters对象。
新建一个配置文件,并通过fileConfig()函数载入配置。
新建一个配置文件夹,并通过dictConfig()函数载入配置。
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
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')
[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
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]
如果logging.raiseExceptions选项为False(线上环境),日志会被丢弃。
如果logging.raiseExceptions选项为True(开发环境),会在控制台输出‘No handlers could be found for logger X.Y.Z’
日志事件会通过loggin.lastResort对象中的兜底handler处理。
这个内部的handler并没有被任何logger使用,它的效果跟StreamHandler一样,会将日志消息输出到sys.stderr(所以需要你谨慎的对待可能被改动的重定向)。
它只会打印出来日志消息,并没有携带任何格式。
这个handler的默认级别是DEBUG,所以基本上任何消息都会被打印出来。
在库中使用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,你可能会影响调用方的单元测试结果,并且输出一些他们不需要的东西。
点击下方标题查看文档的基础部分: