day5-logging模块
一、概述
好的程序开发,往往会兼顾到日志输出的需求,以便给用户提供必要的日志信息便于确认程序运行状态、排错等等。这些日志一般包括程序的正常运行日志、访问日志、错误日志、数据保存日志等类型。在python中logging模块提供了标准的日志接口,可以满足我们对日志输出的各种需求,下面一一详述。
二、logging模块入门
2.1 日志级别
业内常用的日志有五个级别,分别是:debug,info,warning,error和critical,其中debug级别最低,critical级别最高,级别越低,打印的日志等级越多。各个级别的定义如下:
默认的级别是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可以定义日志输出的参数。关键参数如下:
当然了,这个日志级别是可以灵活自定义的。
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]')
日志文件输出结果:
2.4 format格式化输出日志
前面的示例中,日志的输出没有什么可读性而言,这里可以通过format来增加时间等字段,提高可读性。常用的format相关的参数如下:
示例程序:
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程序了!
还是多实践多啃骨头多开脑洞把,少年!