day5-logging模块

一、概述

好的程序开发,往往会兼顾到日志输出的需求,以便给用户提供必要的日志信息便于确认程序运行状态、排错等等。这些日志一般包括程序的正常运行日志、访问日志、错误日志、数据保存日志等类型。在python中logging模块提供了标准的日志接口,可以满足我们对日志输出的各种需求,下面一一详述。

二、logging模块入门

2.1 日志级别

业内常用的日志有五个级别,分别是:debug,info,warning,error和critical,其中debug级别最低,critical级别最高,级别越低,打印的日志等级越多。各个级别的定义如下:

image

默认的级别是warning,只有在warning级别和以上级别的日志才会输出,这样可以避免日志过多的问题。当然这个默认的级别是可以自定义修改的,下文详述。

2.2 标准日志输出

最简单的日志输出做输出到stdout,看代码:

  1 import logging
  2 
  3 logging.debug('[debug]')
  4 logging.info('[info]')
  5 logging.warning('[warning]')
  6 logging.error('[error]')
  7 logging.critical('[critical]')
  8 
  9 输出:
 10 WARNING:root:[warning]
 11 ERROR:root:[error]
 12 CRITICAL:root:[critical]
 13 
 14 #默认的日志级别是warning,该级别以下的日志没有输出

2.3 日志输出到文件

日志输出到文件具有持久化保存的功能,便于随时回溯查看。通过basicConfig可以定义日志输出的参数。关键参数如下:

image

当然了,这个日志级别是可以灵活自定义的。

  1 import logging
  2 
  3 logging.basicConfig(filename='running.log', level=logging.INFO)
  4 logging.debug('[debug]')
  5 logging.info('[info]')
  6 logging.warning('[warning]')
  7 logging.error('[error]')
  8 logging.critical('[critical]')

日志文件输出结果:

image

2.4 format格式化输出日志

前面的示例中,日志的输出没有什么可读性而言,这里可以通过format来增加时间等字段,提高可读性。常用的format相关的参数如下:

image

示例程序:

  1 import logging
  2 
  3 logging.basicConfig(filename='running.log', level=logging.INFO, format='%(asctime)s [%(levelname)s] %(module)s %(message)s')
  4 logging.debug('[debug]')
  5 logging.info('service started')
  6 logging.warning('memory leak')
  7 logging.error('can not started')
  8 logging.critical('fatal error')
  9 
 10 程序输出:
 11 2018-02-21 07:30:22,970 [INFO] log service started
 12 2018-02-21 07:30:22,971 [WARNING] log memory leak
 13 2018-02-21 07:30:22,971 [ERROR] log can not started
 14 2018-02-21 07:30:22,971 [CRITICAL] log fatal error

因为示例程序的文件名为log.py,所以这里日志输出的模块名显示为log。还可以通datefmt参数自定义日期时间输出格式:

  1 import logging
  2 
  3 logging.basicConfig(filename='running.log', level=logging.INFO, format='%(asctime)s [%(levelname)s] %(module)s %(message)s',datefmt='%m/%d/%Y %H:%M:%S')
  4 logging.debug('[debug]')
  5 logging.info('service started')
  6 logging.warning('memory leak')
  7 logging.error('can not started')
  8 logging.critical('fatal error')
  9 
 10 输出:
 11 02/21/2018 07:41:02 [INFO] log service started
 12 02/21/2018 07:41:02 [WARNING] log memory leak
 13 02/21/2018 07:41:02 [ERROR] log can not started
 14 02/21/2018 07:41:02 [CRITICAL] log fatal error

三、logging模块进阶

前文讲述了日志输出的一些基本用法,复杂的用法还需要进阶学习:

3.1 logging模块的主要类概述

python使用logging模块记录日志涉及四个主要类:

①logger:记录器,提供了应用程序可以直接使用的接口。

②handler:处理器,将(logger创建的)日志记录发送到合适的目的输出。必须有handler才能捕获输出的日志。

③filter:过滤器,对日志进行过滤来决定输出哪条日志记录。

④formatter:格式化器,决定日志记录的最终输出格式。

3.2 logger详解

Logger是一个树形层级结构,在使用接口debug,info,warn,error,critical之前必须创建Logger实例,即创建一个记录器,如果没有显式的进行创建,则默认创建一个root logger,并应用默认的日志级别(WARN),处理器Handler(StreamHandler,即将日志信息打印输出在标准输出上),和格式化器Formatter(默认的格式即为第一个简单使用程序中输出的格式)。

创建方法: logger = logging.getLogger(logger_name)

创建Logger实例后,可以使用以下方法进行相关设置:

  • logger.setLevel(logging.ERROR) # 设置日志级别为ERROR,即只有日志级别大于等于ERROR的日志才会输出
  • logger.addHandler(handler_name) # 为Logger实例增加一个处理器
  • logger.removeHandler(handler_name) # 为Logger实例删除一个处理器
  • logger.addFilter(filt) #为logger实例增加过滤器
  • logger.removeFilter(filt) #为logger实例删除过滤器
  • logger.debug()、logger.info()、logger.warning()、logger.error()、logger.critical() #设定指定级别的日志输出
  • len(logger.handlers) #获取handler的个数

3.3 handler详解

handler对象负责发送相关的信息到指定目的地。Python的日志系统有多种Handler可以使用。有些Handler可以把信息输出到控制台,有些Logger可以把信息输出到文件,还有些 Handler可以把信息发送到网络上。如果觉得不够用,还可以编写自己的Handler。可以通过addHandler()方法添加多个多handler 。

Handler处理器类型有很多种,比较常用的有三个,StreamHandler,FileHandler,NullHandler,详情可以访问Python logging.handlers
创建StreamHandler之后,可以通过使用以下方法设置日志级别,设置格式化器Formatter,增加或删除过滤器Filter。

  • ch.setLevel(logging.WARN) # 指定日志级别,低于WARN级别的日志将被忽略
  • ch.setFormatter(formatter_name) # 设置一个格式化器formatter
  • ch.addFilter(filter_name) # 增加一个过滤器,可以增加多个
  • ch.removeFilter(filter_name) # 删除一个过滤器
StreamHandler

创建方法: sh = logging.StreamHandler(stream=None)

FileHandler

创建方法: fh = logging.FileHandler(filename, mode='a', encoding=None, delay=False)

NullHandler

NullHandler类位于核心logging包,不做任何的格式化或者输出。
本质上它是个“什么都不做”的handler,由库开发者使用。
下面讲解handler的重要知识点:

  • logging.StreamHandler

说明:创建一个handler对象,使用这个Handler可以向类似与sys.stdout或者sys.stderr的任何文件对象(file object)输出信息,也就是屏幕输出。
它的构造函数是:StreamHandler([strm]),其中strm参数是一个文件对象,默认是sys.stderr。

  1 import logging
  2 
  3 logger = logging.getLogger('testlog') #创建一个logger对象
  4 logger.setLevel(logging.DEBUG) #设定logger的日志级别
  5 
  6 stdout = logging.StreamHandler() #创建一个handler对象,日志输出到stdout
  7 stdout.setLevel(logging.INFO) #设定handler的日志级别
  8 
  9 logger.addHandler(stdout)#为logger增加指定的handler
 10 logger.debug('debug log') #输出日志
 11 logger.info('info log') #输出日志
 12 
 13 输出:
 14 info log

上述示例程序中,由于handler中设置的日志级别是info,比logger中的debug还要高,因此结果输出中仅仅输出了info级别的日志。

  • logging.FileHandler

说明:和StreamHandler类似,用于向一个文件输出日志信息,不过FileHandler会帮你打开这个文件。

它的构造函数是:FileHandler(filename[,mode])。filename是文件名,必须指定一个文件名。mode是文件的打开方式。参见Python内置函数open()的用法。默认是’a',即添加到文件末尾。

  1 import logging
  2 
  3 logger = logging.getLogger('testlog')
  4 logger.setLevel(logging.DEBUG)
  5 
  6 fh = logging.FileHandler('test.log')  #创建一个fileHandler对象,默认追加写
  7 fh.setLevel(logging.INFO) #设置handler的日志级别
  8 
  9 formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s --%(module)s')
 10 fh.setFormatter(formatter) #为handler设定格式化器
 11 
 12 logger.addHandler(fh)
 13 logger.debug('debug log')
 14 logger.info('info log')
 15 logger.error('error log')
 16 
 17 输出:
 18 2018-02-21 10:55:37,804 [INFO] info log --log
 19 2018-02-21 10:55:37,804 [ERROR] error log --log
  • logging.handlers.RotatingFileHandler

说明:这个Handler类似于上面的FileHandler,但是它可以管理文件大小。当文件达到一定大小(字节数)之后,它会自动进行日志文件滚动切割:将当前日志文件改名,然后创建 一个新的同名日志文件继续输出。比如日志文件是chat.log。当chat.log达到指定的大小之后,RotatingFileHandler自动把 文件改名为chat.log.1。不过,如果chat.log.1已经存在,会先把chat.log.1重命名为chat.log.2。。。最后重新创建 chat.log,继续输出日志信息。

它的构造函数是:RotatingFileHandler( filename, mode, maxBytes, backupCount),其中filename和mode两个参数和FileHandler一样。
maxBytes用于指定日志文件的最大文件大小。如果maxBytes为0,意味着日志文件可以无限大,这时上面描述的重命名过程就不会发生。
backupCount用于指定保留的备份文件的个数。比如,如果指定为2,当上面描述的重命名过程发生时,原有的chat.log.2并不会被更名,而是被删除。

  1 import logging
  2 from logging import handlers
  3 
  4 logger = logging.getLogger(__name__)
  5 logger.setLevel(logging.DEBUG)
  6 
  7 #单个日志文件最大50字节,最多保留3个文件
  8 fh = logging.handlers.RotatingFileHandler(filename='time.log', maxBytes=50, backupCount=3)
  9 fh.setLevel(logging.INFO)
 10 
 11 #设定formatter
 12 formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s --%(module)s')
 13 fh.setFormatter(formatter)
 14 
 15 logger.addHandler(fh)
 16 logger.debug('debug log')
 17 logger.info('info log')
 18 logger.error('error log')
 19 
 20 输出:
 21 time.log.1 文件	2018-02-21 11:08:26,253 [INFO] info log --log
 22 time.log 文件	2018-02-21 11:08:26,254 [ERROR] error log --log

上述示例程序中由于一行日志的输出刚好达到50字节的日志文件切割临界值,所以两行log输出到了两个日志文件中,且越早输出的日志会写到了后缀越大的日志文件中(最新的日志文件,可认为后缀为0)

最新补充:
自己在写一次作业时发现,.RotatingFileHandler存在一下问题:

(1) 默认写日志的编码不是utf-8,直接通过参数encoding='utf-8'解决

  • logging.handlers.TimedRotatingFileHandler

说明:这个Handler和RotatingFileHandler类似,不过,它没有通过判断文件大小来决定何时重新创建日志文件,而是间隔一定时间就自动创建新的日志文件。重命名的过程与RotatingFileHandler类似,不过新的文件不是附加数字,而是当前时间。

它的构造函数是:TimedRotatingFileHandler( filename,when,interval,backupCount),其中filename参数和backupCount参数和RotatingFileHandler具有相同的意义。

interval是时间间隔。
when参数是一个字符串。表示时间间隔的单位,不区分大小写。它有以下取值:①S:秒②M:分③H:小时④D:天⑤W :每星期(interval==0时代表星期一)⑥midnight:每天凌晨

  1 import logging
  2 from logging import handlers
  3 import time
  4 
  5 logger = logging.getLogger(__name__)
  6 logger.setLevel(logging.DEBUG)
  7 
  8 #每隔5秒切割一次日志文件,最多保留3份
  9 fh = logging.handlers.TimedRotatingFileHandler(filename='time.log', when='S', interval=3, backupCount=3)
 10 fh.setLevel(logging.INFO)
 11 
 12 formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s --%(module)s')
 13 fh.setFormatter(formatter)
 14 
 15 logger.addHandler(fh)
 16 logger.debug('debug log')
 17 time.sleep(3)
 18 logger.info('info log')
 19 time.sleep(3)
 20 logger.error('error log')
 21 
 22 输出:
 23 time.log				2018-02-21 11:18:05,867 [ERROR] error log --log
 24 time.log.2018-02-21_11-18-02	2018-02-21 11:18:02,842 [INFO] info log --log
 25 time.log.2018-02-21_11-17-59        文件为空,但因为有sleep导致达到日志滚动的时间阈值,所以滚动生成了一个空日志文件

3.4 Formatter 格式化器

使用Formatter对象设置日志信息最后的规则、结构和内容,默认的时间格式为%Y-%m-%d %H:%M:%S。

创建方法: formatter = logging.Formatter(fmt=None, datefmt=None)

其中,fmt是消息的格式化字符串,datefmt是日期字符串。如果不指明fmt,将使用'%(message)s'。如果不指明datefmt,将使用ISO8601日期格式。

3.5 filter 过滤器

Handlers和Loggers可以使用Filters来完成比级别更复杂的过滤。Filter基类只允许特定Logger层次以下的事件。例如用‘A.B’初始化的Filter允许Logger ‘A.B’, ‘A.B.C’, ‘A.B.C.D’, ‘A.B.D’等记录的事件,logger‘A.BB’, ‘B.A.B’ 等就不行。 如果用空字符串来初始化,所有的事件都接受。

创建方法: filter = logging.Filter(name='')

 

 

 

filter的示例程序待后续补充更新。

四、综合运用--把日志同时输出到标准输出和日志文件

思路比较简单了,先后创建StreamHandler和FileHandler,定义各自的level、formatter,最后add到logger即可。

  1 import logging
  2 
  3 logger = logging.getLogger(__name__)
  4 logger.setLevel(logging.INFO)
  5 
  6 formater = logging.Formatter('%(asctime)s  [%(levelname)s] %(name)s %(message)s ---%(module)s')
  7 
  8 sh = logging.StreamHandler()
  9 sh.setLevel(logging.WARN)
 10 sh.setFormatter(formater)
 11 
 12 fh = logging.FileHandler('test.log')
 13 fh.setLevel(logging.WARN)
 14 fh.setFormatter(formater)
 15 
 16 logger.addHandler(sh)
 17 logger.addHandler(fh)
 18 
 19 logger.debug('debug log')
 20 logger.info('info log')
 21 logger.error('error log')
 22 
 23 输出:
 24 2018-02-21 11:39:38,398  [ERROR] __main__ error log ---log
 25 

五、概念总结

以下是相关概念总结:

熟悉了这些概念之后,有另外一个比较重要的事情必须清楚,即Logger是一个树形层级结构;
Logger可以包含一个或多个Handler和Filter,即Logger与Handler或Fitler是一对多的关系;
一个Logger实例可以新增多个Handler,一个Handler可以新增多个格式化器或多个过滤器,而且日志级别将会继承,但Handler中的日志级别会覆盖Logger中的日志级别设置

 

六.最新实践踩坑记录--调用handler导致日志重复打印问题

常言实践出真理,今天在一次作业小项目中实际用到了logging模块来记录交易日志,结果只要不是第一次记录日志,日志都要被妥妥滴重复记录,而且重复的次数很有规律,第二次调用就打印两行重复的日志,第3次调用就是3行重复的,我能想象得到第n次调用就会重复n次了。刚开始以为是自己定义的日志记录函数有问题,后面反复调试确认没有逻辑问题,随后在网上一搜,遇到这个问题的还不少,呵呵哒。下面记录一个给出最简单且容易理解的解释和解决方案(除此之外我看到的大部分都是修改源码,my god,暂时还不是我的菜,而且牵一发而动全身,能避免就避免把)。

以下论述引用自https://www.cnblogs.com/xqtesting/p/8734001.html

问题原因:

当第二次调用log的时候,根据getLogger(__name__)里的name获取同一个logger,而这个logger里已经有了第一次你添加的handler,第二次调用又添加了一个handler,也就是说这个logger里有了两个同样的handler,所以就会出现调用几次就会有几个handler

解决方案:
明确了问题原因,搞定他当然就水到渠成了:
1. 添加handler时不盲目添加,先判断一下,如果已经有了就直接写日志

 

if not logger.handlers:
    logger.addHandler(fh)

 

2. 每次写完日志就把handler remove掉

logger.removeHandler(streamhandler)

哈哈,算是知道了removeHandler大法的一大实际应用场景了。

还是总结一下吧,经过这次踩坑经历,发现还是要多实践综合性强的实际项目,demo程序一般不超过25行代码,即使有坑也不一定能踩到,而且都是些相对比较片面局限,仅仅针对某个或某几个知识点而来,一个综合性强的项目要胜过n打demo程序了!

还是多实践多啃骨头多开脑洞把,少年!

posted @ 2018-02-21 11:49  里纳斯-派森  阅读(1110)  评论(0编辑  收藏  举报