关于logging的那些坑

python的logging日志记录模块非常强大,使用也很简单,但是特别容易出各种意外状况,打印各种出乎意料的log。最近对logging的一些原理进行了学习,再此做个记录,以备忘。
首先全面的了解一下log对象的结构。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以后,会莫名其妙重复多次输出日志了吧?
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确实很好用,但是个人觉得有些过于灵活了,很多事情私底下偷偷摸摸的做了,反而会给用户带来困扰。
原创不易,转载请注明出处。

猜你喜欢

转载自www.cnblogs.com/telecomshy/p/10630888.html