python logging模块,升级print调试到logging。
简介:
我们在写python程序的时候,很多时候都有bug,都是自己写的,自己造的孽,又的时候报错又是一堆,不知道是那部分出错了。
我这初学者水平,就是打print,看哪部分执行了,哪部分没执行,由此来看问题大概在什么地方。
其实python有更好的处理方案,logging模块。
从Python2.3起,Python的标准库加入了logging模块.logging模块给运行中的应用提供了一个标准的信息输出接口.典型的logging机制实现是把要输出的数据简单地写到一个txt文件中去.写log文件的方式是一种常见的打log的方式,而logging模块提供的更多,它可以把输出信息输出到所有类文件的对象中去,甚至TCP和UDP的sockets,email服务器,Unix的syslog系统,NT系列的事件log系统,内存的buffer和HTTP服务器,当然还有”真正的”文件中去.
Logging库被设计成模块的方式,它提供了以下几个子模块:loggers,handlers,filters和formatters.Loggers把应用需要直接调用的接口暴露出来.Handlers把log记录发到相应的目的地.Filters决定哪些记录需要发给handler.Formatters定义了log记录的输出格式.
Logger对象扮演了三重角色.首先,它暴露给应用几个方法以便应用可以在运行时写log.其次,Logger对象按照log信息的严重程度或者根据filter对象来决定如何处理log信息(默认的过滤功能).最后,logger还负责把log信息传送给相关的loghandlers.
Logger中最长使用的方法分成两部分中:configuration和message sending.
用于Configuration的方法:
setLevel(level)
addFilter(filter)
removeFilter(filter)
addHandler(handler)
removeHandler(handler)
setLevel()方法定义了一个logger处理的最底严重程度(比如说中/高/底三种,我定义为中,那么只有严重程度为中或者高的log才会被处理).debug级别是内置的最低级别,critical是最高级别.举例来说,如果严重级别设为info级,logger仅仅处理info,warning,error和critical级的log,而debug级别的则忽略掉.
根据logger对象的设置,以下的方法被用来写log:
debug(log_message, [*args[, **kwargs]])
info(log_message, [*args[, **kwargs]])
warning(log_message, [*args[, **kwargs]])
error(log_message, [*args[, **kwargs]])
critical(log_message, [*args[, **kwargs]])
exception(message[, *args])
log(log_level, log_message, [*args[, **kwargs]])
Handler对象负责分配合适的log信息(基于log信息的严重程度)到handler指定的目的地.Logger对象可以用addHandler()方法添加零个或多个handler对象到它自身.一个常见的场景是,一个应用可能希望把所有的log信息都发送到一个log文件中去,所有的error级别以上的log信息都发送到stdout,所有critical的log信息通过email发送.这个场景里要求三个不同handler处理,每个handler负责把特定的log信息发送到特定的地方.
标准库里面包括以下的handlers:
StreamHandler (流式,控制台模式?)
FileHandler (文件式)
RotatingFileHandler (自动覆盖文件)
TimedRotatingFileHandler (按时间自动覆盖文件)
SocketHandler
DatagramHandler
SysLogHandler
NTEventLogHandler
SMTPHandler (SMTP邮件处理)
MemoryHandler
HTTPHandler
一:基本使用
1.无脑测试
import logging import sys # 获取logger实例,如果参数为空则返回root logger logger = logging.getLogger("AppName") # 指定logger输出格式 formatter = logging.Formatter('%(asctime)s %(levelname)-8s: (%(name)s)%(pathname)s %(message)s') # 文件日志 file_handler = logging.FileHandler("test.log") file_handler.setFormatter(formatter) # 可以通过setFormatter指定输出格式 # 控制台日志 console_handler = logging.StreamHandler(sys.stdout) console_handler.formatter = formatter # 也可以直接给formatter赋值 # 为logger添加的日志处理器 logger.addHandler(file_handler) logger.addHandler(console_handler) # 指定日志的最低输出级别,默认为WARN级别 logger.setLevel(logging.DEBUG) # 输出不同级别的log logger.debug('this is debug info') logger.info('this is information') logger.warning('this is warning message') logger.error('this is error message') logger.fatal('this is fatal message, it is same as logger.critical') logger.critical('this is critical message') # 记录异常信息 try: 1 / 0 except: logger.exception('except:') # 移除文件日志处理器,那么log文件就不记录这些日志了 logger.removeHandler(file_handler) logger.debug('this is debug info----2') #修改日志输出级别 logger.setLevel(logging.ERROR) logger.info('this is information----2') logger.warning('this is warning message----2') logger.error('this is error message----2') logger.fatal('this is fatal message, it is same as logger.critical----2') logger.critical('this is critical message----2')
2.结果
控制台有输出,文件有记录,在改变了日志等级以后,会有部分信息被隐藏。
这不正式我们需要的么?
以前自己打print,调试完了还要再去注释掉或者删除。
用这个就好了。
写代码调试的时候,用logging.debug
调试完了,把logging配置为warning,或者error。debug就不输出了。
二:配置
1.logging自身的方法设置
上面的无脑测试,就是通过logging自身的方法进行的设置,只是不方便统一调用
2.通过文件加载配置(fileConfig)
通过配置文件,加载配置,据说版本比较老,部分配置参数不支持,推荐用字典加载。
3.通过字典加载配置(dictConfig)
推荐用这种方式加载配置信息。
logging的字典配置信息主要分5个部分:基本设置、日志内容格式、过滤器、处理器和管理器。
三:字典加载配置
1.无脑测试
# !/usr/bin/python3 # -*- coding: utf-8 -*- # @Time : 2018-06-26 9:10 # @Author : Jackadam # @Email :jackadam@sina.com # @File : logging_conf.py # @Software: PyCharm import logging.config, logging, os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DEBUG = True # 标记是否在开发环境 # 给过滤器使用的判断 class RequireDebugTrue(logging.Filter): # 实现filter方法 def filter(self, record): return DEBUG LOGGING = { # 基本设置 'version': 1, # 日志级别 'disable_existing_loggers': False, # 是否禁用现有的记录器 # 日志格式集合 'formatters': { # 标准输出格式 'standard': { # [具体时间][线程名:线程ID][日志名字:日志级别名称(日志级别ID)] [输出的模块:输出的函数]:日志内容 'format': '[%(asctime)s][%(threadName)s:%(thread)d][%(name)s:%(levelname)s(%(lineno)d)][%(module)s:%(funcName)s]:%(message)s' } }, # 过滤器 'filters': { 'require_debug_true': { '()': RequireDebugTrue, } }, # 处理器集合 'handlers': { # 输出到控制台 'console': { 'level': 'DEBUG', # 输出信息的最低级别 'class': 'logging.StreamHandler', 'formatter': 'standard', # 使用standard格式 'filters': ['require_debug_true', ], # 仅当 DEBUG = True 该处理器才生效 }, # 输出到文件 'log': { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'formatter': 'standard', 'filename': os.path.join(BASE_DIR, 'debug.log'), # 输出位置 'maxBytes': 1024 * 1024 * 5, # 文件大小 5M 'backupCount': 5, # 备份份数 'encoding': 'utf8', # 文件编码 }, }, # 日志管理器集合 'loggers': { # 管理器 'default': { 'handlers': ['console', 'log'], 'level': 'DEBUG', 'propagate': True, # 是否传递给父记录器 }, # 管理器 'shoujitiku': { 'handlers': ['console', 'log'], 'level': 'DEBUG', 'propagate': True, # 是否传递给父记录器 }, } } def log_main(): # 加载前面的标准配置 logging.config.dictConfig(LOGGING) # 获取loggers其中的一个日志管理器 logger = logging.getLogger("shoujitiku") return logger loger = log_main() loger.debug('hello')
2.调用方式
函数log_main():
代码中定义了一个字典LOGGING,
加载配置就用
logging.config.dictConfig(LOGGING)
然后使用其中一个日志管理器叫做shoujitiku
logger=logging.getLogger('shoujitiku')
然后返回logger
3.配置文件中的调用
logging.getLogger('shoujitiku'),首先获取日志管理器集合当中的一个管理器shoujitiku,其中handlers,包括了两个处理器,一个是控制台输出console,用方法'class': 'logging.StreamHandler',,一个是日志文件循环输出log,用方法'class': 'logging.handlers.RotatingFileHandler',
每个处理器当中都有'level': 'DEBUG', # 输出信息的最低级别 'class': 'logging.StreamHandler', # 使用什么方法 'formatter': 'standard', # 使用standard格式
4.class
- StreamHandler instances send error messages to streams (file-like objects). 控制台输出
- FileHandler instances send error messages to disk files. 文件记录
- RotatingFileHandler instances send error messages to disk files, with support for maximum log file sizes and log file rotation. 滚动文件记录
- TimedRotatingFileHandler instances send error messages to disk files, rotating the log file at certain timed intervals. 滚动时间记录
- SocketHandler instances send error messages to TCP/IP sockets. TCP端口?不懂,没用。
- DatagramHandler instances send error messages to UDP sockets. UDP端口?不懂,没用。
- SMTPHandler instances send error messages to a designated email address. SMTP发邮件
5.formatter
Formatter对象定义了log信息的结构和内容,构造时需要带两个参数:
- 一个是格式化的模板
fmt
,默认会包含最基本的level
和message
信息 - 一个是格式化的时间样式
datefmt
,默认为2003-07-08 16:49:45,896 (%Y-%m-%d %H:%M:%S)
fmt
中允许使用的变量可以参考下表。
- %(name)s Logger的名字
- %(levelno)s 数字形式的日志级别
- %(levelname)s 文本形式的日志级别
- %(pathname)s 调用日志输出函数的模块的完整路径名,可能没有
- %(filename)s 调用日志输出函数的模块的文件名
- %(module)s 调用日志输出函数的模块名|
- %(funcName)s 调用日志输出函数的函数名|
- %(lineno)d 调用日志输出函数的语句所在的代码行
- %(created)f 当前时间,用UNIX标准的表示时间的浮点数表示|
- %(relativeCreated)d 输出日志信息时的,自Logger创建以来的毫秒数|
- %(asctime)s 字符串形式的当前时间。默认格式是“2003-07-08 16:49:45,896”。逗号后面的是毫秒
- %(thread)d 线程ID。可能没有
- %(threadName)s 线程名。可能没有
- %(process)d 进程ID。可能没有
- %(message)s 用户输出的消息
6.过滤器
一个日志管理器可以包含多个处理器。有时在某些环境或场景下,希望不使用某些处理器。
以console那个处理器为例。若用py2exe等打包Python脚本,不要添加console处理器。
打包之后的程序会把控制台的消息也输出一个log日志(而且还弹窗提示)。此时我要通过过滤器判断是否需要输出到控制台该处理器。
过滤器同样放在一个过滤器集合中,如下代码:
- #过滤器
- 'filters':{
- 'require_debug_true': {
- '()': RequireDebugTrue,
- }
- },
其中键名是过滤器的名称,键值是过滤器的内容。
过滤器内容只需要设置一个属性,该属性值是继承了logging.Filter的类。如下代码:
- DEBUG = True #标记是否在开发环境
- #给过滤器使用的判断
- class RequireDebugTrue(logging.Filter):
- #实现filter方法
- def filter(self, record):
- return DEBUG
继承该类需要实现filter方法,返回一个布尔值。
在开发环境,我设置DEBUG为True;在客户端,我设置DEBUG为False。从而控制是否需要使用某些处理器。
当然,该值你也可以想办法通过一下判断动态设置。
貌似是设置一个类,里面有默认方法filter,返回一个布尔值。再过滤器中引用这个类就可以了。
四:SMTPHandler
1.处理器
# 发个邮件 'email':{ 'level':'DEBUG', 'class':'logging.handlers.SMTPHandler', 'formatter': 'standard', 'mailhost':'smtp.163.com', #SMTP地址 'fromaddr':'jackadam@163.com', #发件人邮箱 'toaddrs':'jackadam@sina.com', #收件人邮箱,可以['a@163.com','a@sina.com','a@google.com'] 'subject':'log info', #邮件标题 'credentials':['jackadam','**********'] #邮箱登陆信息,用户名 ***是密码 },
五:TimedRotatingFileHandler
1.处理器
'log_debug': { 'level': 'DEBUG', 'class': 'logging.handlers.TimedRotatingFileHandler', 'formatter': 'standard', 'filename': os.path.join(BASE_DIR, 'TESTdebug.log'), # 输出位置 'when': 'S', #分割单位 'interval': 1,#单位长度 'backupCount': 10, # 备份份数 'encoding': 'utf8', # 文件编码 },
2.常用参数
- when:是一个字符串,用于描述滚动周期的基本单位,字符串的值及意义如下:
“S”: Seconds 秒
“M”: Minutes 分钟
“H”: Hours 小时
“D”: Days 天
“W”: Week day (0=Monday) 周几
“midnight”: Roll over at midnight 午夜 - interval: 滚动周期,单位有when指定,比如:when=’D’,interval=1,表示每天产生一个日志文件;
- backupCount: 表示日志文件的保留个数;
3.注意事项
除了上述参数之外,TimedRotatingFileHandler还有两个比较重要的成员变量,它们分别是suffix和extMatch。
suffix是指日志文件名的后缀,suffix中通常带有格式化的时间字符串,filename和suffix由“.”连接构成文件名(例如:filename=“runtime”, suffix=“%Y-%m-%d.log”,生成的文件名为runtime.2015-07-06.log)。
extMatch是一个编译好的正则表达式,用于匹配日志文件名的后缀,它必须和suffix是匹配的,如果suffix和extMatch匹配不上的话,过期的日志是不会被删除的。
比如,suffix=“%Y-%m-%d.log”, extMatch的只应该是re.compile(r”^\d{4}-\d{2}-\d{2}.log$”)。
默认情况下,在TimedRotatingFileHandler对象初始化时,suffxi和extMatch会根据when的值进行初始化:
‘S’: suffix=”%Y-%m-%d_%H-%M-%S”, extMatch=r”\^d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}”;
‘M’:suffix=”%Y-%m-%d_%H-%M”,extMatch=r”^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}”;
‘H’:suffix=”%Y-%m-%d_%H”,extMatch=r”^\d{4}-\d{2}-\d{2}_\d{2}”;
‘D’:suffxi=”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;
‘MIDNIGHT’:”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;
‘W’:”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;
如果对日志文件名没有特殊要求的话,可以不用设置suffix和extMatch,如果需要,一定要让它们匹配上。
貌似这是老版本的信息,我在python3.6中没能实现。
六:RotatingFileHandler
1.处理器
'log': { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'formatter': 'standard', 'filename': os.path.join(BASE_DIR, 'TESTdebug.log'), # 输出位置 'maxBytes': 1024 * 1024 * 5, # 文件大小 5M 'backupCount': 5, # 备份份数 'encoding': 'utf8', # 文件编码 },
这个是根据文件大小进行滚动记录的。
会自动添加数字后缀。
七:优先级
我们使用getLogger方法时,读取的是日志管理器集合,由日志管理器集合来决定使用哪个或哪几个日志管理器。
而我们在日志管理器集合和处理器都定义了level,那么以级别高的为准,级别低的就不输出了。
八:总结
1.定义日志格式集合,或单个
2.定义处理器,并确定使用什么格式
3.定义日志管理器集合,决定使用什么处理器,控制台 文件 邮件,任意组合。
4.再任意文件中引入
loger = log_main()
loger.debug('hello')
九:新用法--定时循环的任务,发送一个循环的记录到指定邮箱。
1.定义一个文件处理器:
'log_email': { 'level': 'INFO', 'class': 'logging.FileHandler', 'formatter': 'email', 'filename': os.path.join(BASE_DIR, 'email.log'), # 输出位置 'encoding': 'utf8', # 文件编码 },
2.加入日志管理器集合
3.定义一个函数来进行读取email.log文件,并把内容返回回去,然后删除这个email.log
import os def Smtp_log(): file=os.getcwd() + '/log/email.log' fp=open(file,'r+',encoding='utf-8') text = fp.readlines() resoult='' for i in text: print(i) resoult=resoult+i os.remove(file) return resoult
4.在任意文件中引入
loger.warning(SMTP_log.Smtp_log())
4.一个问题
由于原来发邮件的日志等级高,所以warning的内容也记录在info debug级的日志中。
现在warning发送的是一段时间的日志,造成了info debug日志的重复,虽然有时间记录,可以用来区分,还是不方便,且经常造成软件崩溃。
目前的做法时,另外实例化一个logging
专门用来处理发邮件。
相关代码如下:
def log_main(): # 加载前面的标准配置 logging.config.dictConfig(LOGGING) # 获取loggers其中的一个日志管理器 logger = logging.getLogger("default") return logger def loger_email(): # 加载前面的标准配置 logging.config.dictConfig(LOGGING) # 获取loggers其中的一个日志管理器 logger = logging.getLogger("email") return logger def Smtp_log(): file=os.getcwd() + '/log/email.log' fp=open(file,'r+',encoding='utf-8') text = fp.readlines() resoult='' for i in text: print(i) resoult=resoult+i os.remove(file) return resoult
LOGGING = { # 基本设置 'version': 1, # 日志级别 'disable_existing_loggers': False, # 是否禁用现有的记录器 'addLevelName': '5, "EMAIL"', # 日志格式集合 'formatters': { # 标准输出格式 'standard': { # [具体时间][线程名:线程ID][日志名字:日志级别名称(日志级别ID)] [输出的模块:输出的函数]:日志内容 'format': '[%(asctime)s]:%(levelname)s(%(lineno)d)][%(module)s:%(funcName)s]:%(message)s' }, 'email': { # [具体时间][线程名:线程ID][日志名字:日志级别名称(日志级别ID)] [输出的模块:输出的函数]:日志内容 'format': '[%(asctime)s]:%(message)s' } }, # 过滤器 'filters': { 'require_debug_true': { '()': RequireDebugTrue, } }, # 处理器集合 'handlers': { # 输出到控制台 'console': { 'level': 'INFO', # 输出信息的最低级别 'class': 'logging.StreamHandler', 'formatter': 'standard', # 使用standard格式 'filters': ['require_debug_true', ], # 仅当 DEBUG = True 该处理器才生效 }, # 输出到文件 'log_debug': { 'level': 'INFO', 'class': 'logging.handlers.TimedRotatingFileHandler', 'formatter': 'standard', 'filename': os.path.join(BASE_DIR, 'debug.log'), # 输出位置 'when': 'midnight', 'interval': 1, 'backupCount': 31, # 备份份数 'encoding': 'utf8', # 文件编码 }, 'log_info': { 'level': 'WARNING', 'class': 'logging.handlers.TimedRotatingFileHandler', 'formatter': 'standard', 'filename': os.path.join(BASE_DIR, 'info.log'), # 输出位置 'when': 'midnight', 'interval': 1, 'backupCount': 31, # 备份份数 'encoding': 'utf8', # 文件编码 }, 'log_email': { 'level': 'INFO', 'class': 'logging.FileHandler', 'formatter': 'email', 'filename': os.path.join(BASE_DIR, 'email.log'), # 输出位置 'encoding': 'utf8', # 文件编码 }, 'email': { 'level': 'DEBUG', 'class': 'logging.handlers.SMTPHandler', 'formatter': 'email', 'mailhost': 'smtp.163.com', # SMTP地址 'fromaddr': 'jackadam@163.com', # 发件人邮箱 'toaddrs': 'jackadam@sina.com', # 收件人邮箱,可以['a@163.com','a@sina.com','a@google.com'] 'subject': '答题机报告', # 邮件标题 'credentials': ['jackadam', '********'] # 邮箱登陆信息,用户名 ***是密码 }, }, # 日志管理器集合 'loggers': { # 管理器 'default': { 'handlers': ['console', 'log_debug', 'log_info', 'log_email'], 'level': 'DEBUG', 'propagate': True, # 是否传递给父记录器 }, 'email': { 'handlers': ['email'], 'level': 'DEBUG', 'propagate': True, # 是否传递给父记录器 }, } }