关于logging的那些坑
python的logging日志记录模块非常强大,使用也很简单,但是特别容易出各种意外状况,打印各种出乎意料的log。最近对logging的一些原理进行了学习,再此做个记录,以备忘。
首先全面的了解一下整体的结构。logging默认就有一个root的Logger对象,输入logging.root可以看到,默认为warning级别:
>>> logging.root
<RootLogger root (WARNING)>
用户自行创建的所有logger对象,都是root的子对象。
>>> logger = logging.getLogger('logger')
>>> logger.parent
<RootLogger root (WARNING)>
需要注意的是,当getLogger()不带参数时,返回的就是rootLogger对象本身。
>>> logger = logging.getLogger()
>>> logger
<RootLogger root (WARNING)>
>>> logger is logging.root
True
创建了Logger对象,接下来我们需要创建handler对象,handler对象是用来配置处理log时用的格式,级别等等特性的,我们可以根据需求创建各种不同的handler,比如将log记录保存在文件的FileHandler,将log输出到屏幕的StreamHandler,支持日志根据时间回滚的TimedRotatingFileHandler,根据文件大小回滚的RotatingFileHandler等等(回滚不太好理解,根据时间或文件大小回滚其实就是根据时间或者文件大小来保存日志,比如只保存3天的日志,超过3天的就自动删除,或者设置日志文件只能为一定大小,超过就删除)。
各种handler网上各种文章都有介绍,或者查官方文档,这里就不列举了。稍微一提的是,最常用的FileHandler和StreamHandler在logging下,其它的handler在logging.handlers下。另外,logger和handler的level可以分别设置,以满足不同的需求。
创建了handler,就要把它添加到logger对象中去,logger对象有一个handlers属性,其实就是一个简单的列表,可以看到所有添加给它的hanlder。如下:
>>> logger = logging.getLogger('spam')
>>> handler = logging.StreamHandler()
>>> logger.addHandler(handler)
>>> logger.handlers
[<StreamHandler <stderr> (NOTSET)>]
可见,logger对象添加了handler,是一个输出到stderr,级别未设置的handler。此时,就可以通过logger对象输出各种logger了,如:
>>> logger = logging.getLogger('spam')
>>> handler = logging.StreamHandler()
>>> formatter = logging.Formatter("%(asctime)s-%(levelname)s-%(message)s")
>>> handler.setFormatter(formatter)
>>> handler.setLevel(logging.INFO)
>>> logger.addHandler(handler)
>>> logger.handlers
[<StreamHandler <stderr> (INFO)>]
>>> logger.setLevel(logging.INFO)
>>> logger.info('info')
2019-03-31 11:59:41,933-INFO-info
对整体有个大体的了解,接下来就可以谈谈常见的大坑了。
首先,平时在log输出的时候,经常会发现,莫名奇妙的会输出重复的内容,主要有以下几个可能的原因:
1、前面说过,任何自定义的logger对象都是rootLogger的子对象,就像这样,rootLogger(parent)->Logger(child),而当你使用自定义的logger记录日志的时候,它会从子到父,从左到右依次执行所有的handler来输出,看代码:
>>> rootLogger = logging.getLogger()
>>> rootLogger.setLevel(logging.INFO)
>>> roothandler = logging.StreamHandler()
>>> fmt = logging.Formatter("%(name)s-%(levelname)s-%(message)s")
>>> roothandler.setLevel(logging.INFO)
>>> roothandler.setFormatter(fmt)
>>> rootLogger.addHandler(roothandler)
>>> rootLogger.handlers
[<StreamHandler <stderr> (INFO)>]
>>> childLogger = logging.getLogger("child")
>>> childLogger.setLevel(logging.INFO)
>>> childhandler = logging.StreamHandler()
>>> childhandler.setLevel(logging.INFO)
>>> childhandler.setFormatter(fmt)
>>> childLogger.addHandler(childhandler)
>>> childLogger.handlers
[<StreamHandler <stderr> (INFO)>]
>>> childLogger.info('i am child')
child-INFO-i am child
child-INFO-i am child
日志输出了2次,注意输出的logger name,虽然有一个是rootLogger的handler输出的,但是name还是子logger的。
2、有人可能会说,上面这个情况不太可能会发生,因为我直接设置一个子logger就ok了,是这样没错,but,当你直接使用logging.basicConfig对rootLogger对象进行配置或者在创建自己的logger对象之前,用logging.info等命令输出过日志的时候,logging会自动的(偷偷摸摸的)给你创建一个streamhandler,如下:
>>> root = logging.getLogger()
>>> root.handlers
[]
>>> logging.basicConfig(level=logging.INFO)
>>> root.handlers
[<StreamHandler <stderr> (NOTSET)>]
>>> root = logging.getLogger()
>>> root.handlers
[]
>>> logging.info('i am root')
>>> root.handlers
[<StreamHandler <stderr> (NOTSET)>]
现在明白为什么有时候创建了一个logger以后,会莫名其妙重复多次输出日志了吧?
2021年7月7日补充:
今天重温笔记,对比官网代码如下:
FORMAT = '%(asctime)-15s %(clientip)s %(user)-8s %(message)s'
logging.basicConfig(format=FORMAT)
d = {'clientip': '192.168.0.1', 'user': 'fbloggs'}
logger = logging.getLogger('tcpserver')
logger.warning('Protocol problem: %s', 'connection reset', extra=d)
输出为:
2021-07-07 23:32:45,858 192.168.0.1 fbloggs Protocol problem: connection reset
只有一行。查看logger.handlers为空,查看logger.root.handlers为[<StreamHandler <stderr> (NOTSET)>]
。所以,实际上logger是没有handler的,warning消息向上传递,由root的handler处理了。真的是很容易出错啊。
3、还有一种情况,也堪称巨坑。当使用IDE工具编程的时候,整个IDE会话不结束,logger的handlers列表不清空!这就导致反复运行程序的时候,handler会反复添加,结果你会发现log输出越来越多。。。。。如下:
第一次运行,一个handler,第二次运行,变2个了,怎么办呢?可以在程序结尾加一行代码root.removeHandler(handler),或者简单粗暴一点,直接root.handlers=[],每次程序运行完,将handlers列表全清空。
差不多就这些吧,最后还有一个小知识点,有些同学会问,为啥我自定义的logger啥handler都没加的时候,也有输出呢?比如:
>>> import logging
>>> logger = logging.getLogger('child')
>>> logger.handlers
[]
>>> logging.root.handlers
[]
>>> logger.warning('i am child')
i am child
因为logging模块有一个默认的hanlder,可以通过logging.lastResort查看,如下:
>>> logging.lastResort
<_StderrHandler <stderr> (WARNING)>
这个handler没有和任何logger关联,专门处理用户啥都没配置的情况,可见,默认级别是warning,默认输出是stderr(注意是stderr而不是标准的stdout,因此如果你采取这种方式输出日志又进行了重定向,很有可能达不到想要的效果),仅仅输出一个message,其它啥都没有。
最后总结:logging确实很好用,但是个人觉得有些过于灵活了,很多事情私底下偷偷摸摸的做了,反而会给用户带来困扰。
原创不易,转载请注明出处。
====================================
2019年4月1日补充更新:
除了定制不同级别的level,logger还可以添加filter对象来对输出进行更加复杂的控制。filter和handler一样,先进行配置,然后再添加到logger对象中。所有日志在输出之前,都会先通过filter进行过滤。创建filter要先创建一个继承自logging.Filter的类,代码如下:
import logging
import sys
class ContextFilter(logging.Filter):
def filter(self, record):
if record.role == "admin":
return True
else:
return False
if __name__ == '__main__':
logger = logging.getLogger("Wechat")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(
'%(asctime)s %(levelname)s:%(message)s Role:%(role)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
f = ContextFilter()
logger.addFilter(f)
logger.info('An info message with %s', 'some parameters',
extra={"role": "admin"})
logger.info('An info message with %s', 'some parameters',
extra={"role": "hacker"})
从3.2以后,不一定非要创建一个logging.Filter子类,只要是有filter属性的任何对象或者函数都行,后台会检查这个对象是否有filter属性,如果有,则会调用这个对象的filter()方法,没有的话就把它当作一个callable对象,直接执行,并且把records当作一个参数传递给这个callable对象。
import logging
import sys
def myfilter(record):
if record.role == "admin":
return True
else:
return False
if __name__ == '__main__':
logger = logging.getLogger("Wechat")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s Role: %(role)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.addFilter(myfilter)
logger.info('An info message with %s', 'some parameters',
extra={"role":"admin"})
logger.info('An info message with %s', 'some parameters',
extra={"role":"hacker"}
部分代码参考https://www.cnblogs.com/progor/p/9269230.html,特此申明。
4.5日新增:
继续谈谈filter,需要注意的是,如果使用上面的例子,那么在每一条日志记录里面,都需要加上一条extra={"role":"xxx"},因为所有的日志record对象都会先通过filter过滤,此时会检查record是否有role属性,如果没有设置,显然会报错。
其实除了过滤这一种用法,filter还可以通过在函数里面给record添加属性,方便的增加自定义的字段。还是看例子:
def myfilter(record):
record.user = 'telecomshy'
return True
>>> logger = logging.getLogger()
>>> logger.addFilter(myfilter)
>>> import sys
>>> fmt = logging.Formatter("%(levelname)s-%(asctime)s-%(message)s-%(user)s")
>>> handler = logging.StreamHandler(sys.stdout)
>>> handler.setLevel("DEBUG")
>>> handler.setFormatter(fmt)
>>> logger.addHandler(handler)
>>> logger.setLevel("DEBUG")
>>> logger.info('hello world')
INFO-2019-04-05 11:55:28,764-hello world-telecomshy
如上,通过使用filter,现在可以在formatter里面使用'user'这个自定义的字段了。