logging–日志记录工具
1.日志级别:
级别 | 数值 | 何时使用 |
---|---|---|
CRITICAL | 50 | 严重的错误,尽当诊断问题时适用 |
ERROR | 40 | 由于严重的错误,程序的某些功能已经不能正常运行 |
WARNING | 30(默认级别) | 表明有已经或即将发生的意外,程序仍按预期运行 |
INFO | 20 | 确认程序按照预期运行 |
DEBUG | 10 | 细节信息,仅当诊断问题时使用 |
NOTSET | 0 | 未设置级别 |
日志级别是产生日志事件的严重程度,严重程度低于设置值的日志消息将被忽略
日志输出的一些常用方法:
debug()、info()、warning()、error()、critical()
日志的默认级别源于
root = RootLogger(WARNING)
2.格式字符串
格式 | 含义 |
---|---|
%(message)s | 日志消息内容(默认的输出格式) |
%(asctime)s | 创建LogRecord的可读时间 |
%(funcName)s | 日志调用所在的函数名 |
%(levelname)s | 消息的级别名称,例如"DEBUG","INFO"等 |
%(lineno)s | 消息的级别数字,对应上面的级别, 代码的行号 |
%(module)s | 模块(filename的名字部分) |
%(process)d | 进程ID |
%(thread)d | 线程ID |
%(processName)s | 进程名 |
%(threadName)s | 线程名 |
%(name)s | logger名字 |
3.logging.basicConfig()基本配置
参数 | 含义 |
---|---|
filename | 设置日志输出到的文件位置 |
filemode | 设置日志输出到文件的模式,默认为"a" |
format | 设置日志的输出格式 |
datefmt | 单独设置时间格式, 类似"%Y-%m-%d %H:%M:%S" |
style | |
level | 日志流的等级 |
handlers | 控制日志流的具体输出位置,注意类型应该为列表,注意次参数和filename以及stream有冲突,不能共存 |
stream | 设置日志输出到的stream流的位置 |
[外链图片转存失败(img-xYL4Pawo-1563281494060)(https://i.loli.net/2019/06/13/5d01ba3081ee129635.png)]
日志库采用模块化方法,并提供积累组件:记录器,处理程序,过滤器和格式化程序之间传递
- 记录器器暴露了应用程序代码直接使用的接口logger
- 处理程序将日志记录发送到适当的目标handler
- 过滤器提供了更精细的附加功能,用于确定输出的日志记录
- 格式化程序制定最终输出中日志记录的样式
日志事件信息在LogRecord实例的记录器、处理程序、过滤器和格式化程序之间传递
4.Logger对象(记录器)
1.在每个使用日志记录的模块中使用模块级记录器
logger = logging.getLogger(__name__) >> level = 0
logger1 = logging.getLogger() >> level = 30
理解logger对象的创建方式
源码分析:
def getLogger(name=None):
if name:
return Logger.manager.getLogger(name) # 等级level默认是0
else:
return root # 等级默认是waring(30)
root = RootLogger(WARNING)
Logger.root = root
Logger.manager = Manager(Logger.root)
def getLogger(self, name):
rv = None
if not isinstance(name, str):
raise TypeError('A logger name must be a string')
_acquireLock()
try:
if name in self.loggerDict:
rv = self.loggerDict[name]
if isinstance(rv, PlaceHolder):
ph = rv
rv = (self.loggerClass or _loggerClass)(name)
rv.manager = self
self.loggerDict[name] = rv
self._fixupChildren(ph, rv)
self._fixupParents(rv)
else:
rv = (self.loggerClass or _loggerClass)(name)
rv.manager = self
self.loggerDict[name] = rv
self._fixupParents(rv)
finally:
_releaseLock()
return rv
分析:
不传入参数创建logger默认返回的是root,而root = RootLogger(WARNING)
RootLogger是Logger的子类,level等级传入的参数为WARNING,所以root默认是根logger的实例
如果传入参数name, 返回的是Logger.manager.getLogger(name), 而Logger.manager = Manager(Logger.root),即Logger.manager是Manager的实例,getLogger为Manager的方法,背后使用了字典self.loggerDict来保证同一个名称返回同一个logger实例(每次返回这个字典的vlaue)
所以:logging.root和logging.getLogger()是一个对象
2.等级
等级分为level和有效level
import logging
import time
import sys
fmt = "%(asctime)s [%(name)s] %(levelname)s %(thread)s %(message)s"
dmt = "%Y-%m-%d,%H:%M:%S"
logging.basicConfig(format=fmt, datefmt=dmt, level=logging.INFO)
logger = logging.getLogger()
logger1 = logging.getLogger("a")
logger2 = logging.getLogger("a.b")
print(logger.level, logger.getEffectiveLevel()) # 20 20
print(logger1.level, logger1.getEffectiveLevel()) # 0 20
print(logger2.level, logger2.getEffectiveLevel()) # 0 20
logger1.setLevel(level)
logger.setLevel(logging.DEBUG)
logger1.setLevel(10)
logger2.setLevel("DEBUG")
logger对象的继承关系(涉及到有效等级)
Logger类的实例他们在概念上以 . 作为分隔符排列在命名空间的层次结构中
源码分析:有效级别的查找顺序
def getEffectiveLevel(self):
logger = self
while logger:
if logger.level:
return logger.level
logger = logger.parent # 一直朝着parent的方向进行查找level
return NOTSET
日志消息能否输出却取决于当前的有效级别
logger = logging.getLogger()返回的是root
logger1 = logging.getLogger("a")
logger2 = logging.getLogger("a.b")
如上所示,logger1是logger2的父类,logger1的有效level会先看自己有没有配置level,如果有就和当前的level相等; 如果当前level没有配置,就会就近寻找祖先们的level,如果存在那么当前logger实例的有效level就继承,否则一直到最后等于30
如果子logger没有设置setlevel,也就是继承自父logger,会一直继承,也就是父logger改变setlevel,子logger也会改变
记录器的有效级别,用于确定事件(也就是logger.info等创建日志信息)是否传递给记录器的处理程序。
5.Handler对象
Handler控制日志信息的输出目的地,可以是控制台、文件
- 可以单独设置 level 控制日志流的传播
- 可以单独设置格式, format
- 可以设置过滤器
5.1.Handler层次:
- Hnadler
- StreamHandler # 不指定使用stderr
- _StderrHandler # 标准错误输出
- FileHandler # 文件
- NullHandler # 什么都不做
- StreamHandler # 不指定使用stderr
5.2.handler对象的创建:
常见的主要是用的处理程序类型:
- logging.StreamHandler
- logging.FileHandler
5.3.handler对象的配置方法:
配置方法 | 含义 |
---|---|
handler1.setLevel() | 设置level,和记录器的setlevel区别开 |
handler1.setFormatter() | 设置格式,选择一个该处理程序使用的Formatter对象 |
handler1.addFilter()、handler1.removeFilter() | 设置和取消过滤器 |
注意:
- handler对象没有有效等级属只有level属性
- logger对象可以使用addHandler方法给自己添加零个或者多个处理程序对象(handler对象)
- handler的默认等级是0
logger对象的创建,是有层级结构的
logger对象的属性propagate可以控制日志流的传播
logger.debug() >> 消息的level为DEBUG(10)
记录流程:
记录器和处理程序中的日志时间信息流程如下图所示:
1.如果消息想要在某个logger对象上产生,首先消息的level要和logger对象的有效等级先比较,如果大于等于,就可以生成日志,否则传播结束
注意这个level比较,只需要过了当前的logger的有效level即可,后面所有的父logger都不需要比较了,相当于过了当前这一logger关就可以了
注意创建logger对象过程中的有效level的传递过程
2.如果loggers对象的handlers属性为空列表[],那么会看logging.basicConfig
会默认创建一个StreamHandler(会将当前logger的消息一般输出到控制台),注意如果logging.basicConfig设置了filename,就会创建一个FileHandler对象,如果设置了format参数,就会用它生成一个Formatter对象,否则会生成缺省Formatter,并把它加入到刚才创建的Handler上
注意:如果root的handlers不为空,logging.basicConfig什么都不做
3.如果loggers对象的handlers属性不是空列表,将消息的level和handler的level相比较,如果大于等于,就可以按照handler输出到日志
查看logger的属性propagate,如果为False,流程结束 为True就会继续寻找他的父类logger,把父logger的所有handler进行挨个等级比较,满足即输出,否则就一直寻找到root logger,进行handler比较,满足即输出
小知识补充:
关于sys模块的stdout、stdin、stderr, file-like对象
可以输出的都是可写的, 输入的都是可读的
6.Formatter对象
在logging模块中
class Formatter(object):
def __init__(self, fmt=None, datefmt=None, style='%')
pass
关于基本配置logging.basicConfig中的输出目的地问题
源码分析:
# 在logging.basicConfig函数中,如下
if handlers is None:
filename = kwargs.pop("filename", None)
mode = kwargs.pop("filemode", 'a')
if filename:
h = FileHandler(filename, mode)
else:
stream = kwargs.pop("stream", None)
h = StreamHandler(stream)
handlers = [h]
如果设置文件名,则为根Logger增加一个输出到文件的FileHandler;
如果没有设置文件名,则为根Logger加一个StreamHandler,默认输出到sys.stderr
也就是说根Logger一定会至少有一个handler的
注意参数
7.Filter对象
- filter对象的创建
filter = logging.Filter() - 可以给handler对象增加过滤器,所以这种过滤器之形象某一个handler,不会影响整个处理流程,属于handler级别
handler.addFilter(filter) - 可以给logger对象添加过滤器,当前对象在日志流的输出就会受影响,即如果被过滤掉某个logger,日志流在当前logger处会中断
logger.addFilter(filter)
源码分析(创建filter对象部分):
class Filter(object):
def __init__(self, name=''):
self.name = name # 这里name是logging.Filter传入的name
self.nlen = len(name)
def filter(self, record):
if self.nlen == 0:
return True
elif self.name == record.name: #record.name 是logger对象上的name
return True
elif record.name.find(self.name, 0, self.nlen) != 0:
# 等价于record.name.startswith(self.name)
return False
return (record.name[self.nlen] == ".")
# 表示self.name的下一个字符为 . 则返回True,就过滤了
logging模块最困难的是掌握日志流的传播顺序,模块里面涉及的细则有点多,源码里面使用了很多继承和高级编程技巧,可以帮我们更快的掌握
更多请参考官方文档-日志基础教程