日志是对软件执行时所发生事件的一种追踪方式。软件开发人员对他们的代码添加日志调用,借此来指示某事件的发生。一个事件通过一些包含变量数据的描述信息来描述(比如:每个事件发生时的数据都是不同的)。开发者还会区分事件的重要性(重要性也被称为 等级 或 严重性)。而我们在做app自动化测试时,遇到异常情况,查看日志也是必不可少的。那我们在什么情况下使用日志呢?
一.何时使用日志
对于简单的日志使用来说日志功能提供了一系列便利的函数。它们是 debug(),info(),warning(),error() 和 critical()
。想要决定何时使用日志,请看下表:
你想要执行的任务 | 此任务对应最好工具 |
---|---|
对于命令行或程序的应用,结果显示在控制台 | print() |
在对程序的普通操作发生时提交事件报告(比如:状态监控和错误调查) | logging.info() 函数(当有诊断目的需要详细输出信息时使用 logging.debug() 函数) |
提出一个警告信息基于一个特殊的运行时事件 | warnings.warn() 位于代码库中,该事件是可以避免的,需要修改客户端应用以消除告警 |
对一个特殊的运行时事件报告错误 | 引发异常 |
报告错误而不引发异常(如在长时间运行中的服务端进程的错误处理) | logging.error(), logging.exception() 或 logging.critical() 分别适用于特定的错误及应用领域 |
日志功能应以所追踪事件级别或严重性而定。各级别适用性如下(以严重性递增):
级别 | 何时使用 | 数值 |
---|---|---|
DEBUG | 细节信息,仅当诊断问题时适用。 | 10 |
INFO | 确认程序按预期运行 | 20 |
WARNING | 表明有已经或即将发生的意外(例如:磁盘空间不足)。程序仍按预期进行 | 30 |
ERROR | 由于严重的问题,程序的某些功能已经不能正常执行 | 40 |
CRITICAL | 严重的错误,表明程序已不能继续执行 | 50 |
默认的级别是WARNING
,意味着只会追踪该级别及以上的事件,除非更改日志级别配置。
例子:
import logging
logging.warning('This is waring!') # 会在控制台输出This is waring!
logging.info('This is info') # 不会输出任何内容
那么如果想把日志文件记录到指定的文件件呢,该如何配置?
# -*- coding:utf-8 -*-
import logging
# 通过下面的方式进行简单配置输出方式与日志级别
logging.basicConfig(filename='logs.log', level=logging.DEBUG,)
logging.debug('debug message')
logging.info('info message')
logging.warning('warn message')
logging.error('error message')
logging.critical('critical message')
控制台未显示任何信息,而在当前工作目录下生成了logs.log,内容如下:
DEBUG:root:debug message
INFO:root:info message
WARNING:root:warn message
ERROR:root:error message
CRITICAL:root:critical message
二.重要概念
Logger 记录器:
暴露了应用程序代码能直接使用的接口。Handler 处理器:
将(记录器产生的)日志记录发送至合适的目的地。Filter 过滤器:
提供了更好的粒度控制,它可以决定输出哪些日志记录。Formatter 格式化器:
指明了最终输出中日志记录的布局。
2.1 Logger处理器
Logger类永远不会直接实例化,而是始终通过模块级函数实例化 logging.getLogger(name)
,getLogger()具有相同名称的多次调用将始终返回对同一Logger对象的引用。 如果没有显式创建,默认创建一个名称为root的logger,并应用默认的日志级别(WARN),默认处理器Handler(StreamHandler,即屏幕打印),和默认格式化器Formatter(日志级别:logger名称:消息)。
创建方式:
#实例化一个logger对象
logger = logging.getLogger(name)
创建Logger实例后,可以使用以下方法进行日志级别设置,增加处理器Handler。
- logger.setLevel(logging.ERROR) :设置日志级别为ERROR,即只有日志级别大于等于ERROR的日志才会输出
- logger.addHandler(handler_name) :为Logger实例增加一个处理器
- logger.removeHandler(handler_name) : 为Logger实例删除一个处理器
主要函数 | 用法 |
---|---|
setLevel(level) | 设置记录器级别,高于level的日志才会被记录。如果Handler设置了更高的日志级别,则以Handler日志级别为准。 |
getChild(suffix) | 返回后代记录器,logging.getLogger(‘abc’).getChild(‘def.ghi’)与logging.getLogger(‘abc.def.ghi’)一致。 |
addFilter(filter) | 添加过滤器 |
removeFilter(filter) | 删除过滤器 |
addHandler(handler) | 增加处理器 |
removeHandler(handler) | 删除处理器 |
2.2 Handler 处理器
Handler处理器类型有很多种,比较常用的有四个:StreamHanlder, FileHandler,RotatingFileHandler,TimedRotatingFileHandler
。
Handler类型 | 主要用途 |
---|---|
StreamHandler | 输出日志到标准输出设备(如屏幕) |
FileHandler | 输出日志到文件 |
RotatingFileHandler | 输出到文件,支持文件达到一定大小后创建一个新的日志文件 |
TimedRotatingFileHandler | 输出到文件,支持经过一段时间后创建一个新的日志文件 |
创建StreamHandler之后,可以通过使用以下方法设置日志级别,设置格式化器Formatter,增加或删除过滤器Filter。
- sh.setLevel(logging.WARN) : 指定日志级别,低于WARN级别的日志将被忽略
- sh.setFormatter(formatter_name) :设置一个格式化器formatter
- sh.addFilter(filter_name) : 增加一个过滤器,可以增加多个
- sh.removeFilter(filter_name) : 删除一个过滤器
StreamHandler——流处理器
创建方式:
#实例化控制台输出对象
sh = logging.StreamHandler()
FileHandler——文件处理器
创建方式:
#实例化文件输出对象
fh = logging.FileHandler(filename, mode='a', encoding='UTF-8', delay=False)
RotatingFileHandler——循环文件处理器
长期运行的程序(如web服务),日志文件会越来越大,大到可能难以读写——循环处理器就是解决这一问题——自动切分日志。
创建方式:
#maxBytes是当前日志最大字节数,backupCount是老日志保留个数
rh = logging.RotatingFileHandler(filename,mode=‘a’,maxBytes=0, backupCount=0, encoding=None, delay=False)
- 如果maxBytes或者backupCount任意一个等于0,则永不会触发循环,日志无限增长。
- 如果当前日志达到设定的非零maxBytes,则新建一个日志文件,老的日志文件被重命名为后缀‘.1’,’.2’…等,如果新的日志又达到设定大小,则再新建一个日志,当前所有backup日志的后缀+1,最多保持backupCount个后缀,注意当前日志是没有后缀的。
TimeRotatingFileHandler——时间循环文件处理器
创建方式:
#when和interval共同决定循环时间间隔,when是单位,interval是数量。循环后老日志会自动加上一个后缀,当前日志是没有后缀的。
rh = logging.TimedRotatingFileHandler(filename, when=‘h’, interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None)
- 当when设置为W0到W6时,interval失效。backupCount=0则永远不会删除老日志。
- 系统将老日志自动保存,添加一个以’%Y-%m-%d_%H-%M-%S’格式的拓展名。
- 如果when设置成了1分钟,但一分钟内程序没有打印日志,则也不会自动循环(这个好理解,不然会造成很多空日志文件)。
2.3 Formatter格式化器
使用Formatter对象设置日志信息最后的规则、结构和内容,默认的时间格式为%Y-%m-%d %H:%M:%S。
创建方法:
#fmt是消息的格式化字符串,datefmt是日期字符串。如果不指明fmt,将使用'%(message)s'。如果不指明datefmt,将使用ISO8601日期格式。
formatter = logging.Formatter(fmt=None, datefmt=None)
其中,fmt是消息的格式化字符串,datefmt是日期字符串。如果不指明fmt,将使用’%(message)s’。如果不指明datefmt,将使用ISO8601日期格式。
日志格式 | 含义 |
---|---|
%(levelno)s | 打印日志级别的数值 |
%(levelname)s | 打印日志级别名称 |
%(pathname)s | 打印当前执行程序的路径 |
%(filename)s | 打印当前执行程序名称 |
%(funcName)s | 打印日志的当前函数 |
%(lineno)d | 打印日志的当前行号 |
%(asctime)s | 打印日志的时间 |
%(thread)d | 打印线程id |
%(threadName)s | 打印线程名称 |
%(process)d | 打印进程ID |
%(message)s | 打印日志信息 |
%(module)s | 当前模块名 |
%(name)s | 打印这条消息的Logger名 |
2.4 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’ 等就不行。 如果用空字符串来初始化,所有的事件都接受。
创建方式:
#包含了name的记录将会被过滤掉,不管是什么日志级别;如name=’’,则不过滤。
filter= logging.Filter(name=’’)
它们之间的关系:Logger可以包含一个或多个Handler和Filter,一个Handler可以新增多个Formatter和Filter,而且日志级别将会继承。
三. Logging 封装
实例:
import logging
import os
PATA=lambda p:os.path.abspath(os.path.join(os.path.dirname(__file__),p))
class Log:
def __init__(self,name=None):
self.logger=logging.getLogger(name)#实例化一个logger对象
self.logformat=logging.Formatter('%(asctime)s - %(filename)s- %(name)s - %(levelname)s - %(message)s')
self.logger.setLevel(logging.DEBUG)
def __console(self,level,message):
if not self.logger.handlers:
fh=logging.FileHandler(PATA('../logfile/mylog.txt'),'a',encoding='utf-8')#实例化文件输出对象
fh.setFormatter(self.logformat)#增加输出格式
fh.setLevel(logging.DEBUG)#设置日志级别
self.logger.addHandler(fh) # 增加文件输出对象
if level.lower()=='info':
self.logger.info(message)
elif level.lower()=='debug':
self.logger.debug(message)
elif level.lower()=='warning':
self.logger.warning(message)
elif level.lower()=='error':
self.logger.error(message)
else:
self.logger.exception(message)
self.logger.removeHandler(fh)#避免日志输出重复
fh.close()#关闭打开的日志文件
def info(self,message):
self.__console('info',message)
def debug(self,message):
self.__console('debug',message)
def warning(self,message):
self.__console('warning',message)
def error(self,message):
self.__console('error',message)
def exception(self,message):
self.__console('exception',message)
if __name__ == '__main__':
log=Log('test')
log.info('info测试')
log.debug('debug')
log.warning('warning')
log.error('error')
四.多模块下使用
多次调用 logging.getLogger(‘someLogger’) 时会返回对同一个 logger 对象的引用。 这不仅是在同一个模块中,在其他模块调用也是如此,只要是在同一个 Python 解释器进程中。 是应该引用同一个对象,此外,应用程序也可以在一个模块中定义和配置父 logger,而在单独的模块中创建(但不配置)子 logger,对子 logger 的所有调用都将传给父 logger。 这里是主模块:
import logging
import auxiliary_module
import os
PATA=lambda p:os.path.abspath(os.path.join(os.path.dirname(__file__),p))
#实例化名为“auto_test”的logger对象
logger = logging.getLogger('auto_test')
logger.setLevel(logging.DEBUG)
# 实例化文件输出对象
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
# 实例化控制台输出对象
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# 设置输出格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
#设置文件和控制台输出格式
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 增加文件和控制台输出对象
logger.addHandler(fh)
logger.addHandler(ch)
logger.info('creating an instance of auxiliary_module.Auxiliary')
a = auxiliary_module.Auxiliary()
logger.info('created an instance of auxiliary_module.Auxiliary')
logger.info('calling auxiliary_module.Auxiliary.do_something')
a.do_something()
logger.info('finished auxiliary_module.Auxiliary.do_something')
logger.info('calling auxiliary_module.some_function()')
auxiliary_module.some_function()
logger.info('done with auxiliary_module.some_function()')
这里是辅助模块:
import logging
# create logger
module_logger = logging.getLogger('spam_application.auxiliary')
class Auxiliary:
def __init__(self):
self.logger = logging.getLogger('spam_application.auxiliary.Auxiliary')
self.logger.info('creating an instance of Auxiliary')
def do_something(self):
self.logger.info('doing something')
a = 1 + 1
self.logger.info('done doing something')
def some_function():
module_logger.info('received a call to "some_function"')
五.配置文件配置日志
这种配置方式可以将日志配置和代码分离,方便代码的维护和日志管理。
- 实现logger对应的配置信息必须是 logger_name: name为loggers中key的值
- level : 日志级别,级别有 DEBUG,INFO,WARNING,ERROR,CRITICAL
- handlers : 日志处理器,可以有多个 以逗号隔开
- qualname : logger的名称,通过logging.getLogger(name)获取,这里的name便是qualname,如果获取的logger 名称不存在,则调用默认(root)logger
- propagate 是否继承父类的配置信息,0:否 1:是
新建配置文件logging.conf,用于存放logging配置文件的信息:
# 配置logger信息。必须包含一个名字叫做root的logger,当使用无参函数logging.getLogger()时,
#默认返回root这个logger,其他自定义logger可以通过 logging.getLogger("fileAndConsole") 方式进行调用
[loggers]
keys=root,file,fileAndConsole
# 定义声明handlers信息。
[handlers]
keys=fileHandler,consoleHandler
# 设置日志格式
[formatters]
keys=simpleFormatter
# 对loggers中声明的logger进行逐个配置,且要一一对应,在所有的logger中,必须制定level和handlers这两个选项,对于非roothandler,
#还需要添加一些额外的option,其中qualname表示它在logger层级中的名字,在应用代码中通过这个名字制定所使用的handler,
#即 logging.getLogger("fileAndConsole"),handlers可以指定多个,中间用逗号隔开,比如handlers=fileHandler,consoleHandler,
#同时制定使用控制台和文件输出日志
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_file]
level=DEBUG
handlers=fileHandler
qualname=file
propagate=1
[logger_fileAndConsole]
level=DEBUG
handlers=fileHandler,consoleHandler
qualname=fileAndConsole
propagate=0
# 在handler中,必须指定class和args这两个option,常用的class包括 StreamHandler(仅将日志输出到控制台)、FileHandler(将日志信息输出保存到文件)、
#RotaRotatingFileHandler(将日志输出保存到文件中,并设置单个日志wenj文件的大小和日志文件个数),
#args表示传递给class所指定的handler类初始化方法参数,它必须是一个元组(tuple)的形式,即便只有一个参数值也需要是一个元组的形式;
#里面指定输出路径,比如输出的文件名称等。level与logger中的level一样,而formatter指定的是该处理器所使用的格式器,
#这里指定的格式器名称必须出现在formatters这个section中,且在配置文件中必须要有这个formatter的section定义;
#如果不指定formatter则该handler将会以消息本身作为日志消息进行记录,而不添加额外的时间、日志器名称等信息;
[handler_consoleHandler]
class=StreamHandler
args=(sys.stdout,)
level=DEBUG
formatter=simpleFormatter
[handler_fileHandler]
class=FileHandler
args=('loggers.log', 'a')
level=DEBUG
formatter=simpleFormatter
[formatter_simpleFormatter]
format=%(asctime)s - %(module)s - %(thread)d - %(levelname)s : %(message)s
datefmt=%Y-%m-%d %H:%M:%S
logging日志文件的使用:
import logging
import logging.config
logging.config.fileConfig('logger.conf')
logger=logging.getLogger()
logger.debug('debug message')
logger.info('info message')