Python logging 模块
前言
Python在日志记录处理上是非常灵活的,且功能完备,足以满足所有对日志方面的需求。
如此强大,当然是日志系统设计非常的流弊了。什么封闭开放原则/什么里氏替换原则/依赖倒置原则/单一职责原则等思想融入的淋漓尽致。
里面的对象关系网:Manager对象,Logger对象,Handler对象,Formatter对象, Filter对象,LogRecord对象等。
除此之外,了解其设计时,要梳理清楚模块中几个重要对象之间的关系,和他们各自负责的Mission任务!
logging模块提供的特性
- logger在逻辑上的继承关系,由Manager对象维护并添加到logger对象中。可以延着继承关系将logRecorder传递,还可以获取有效的日志等级设置。
- 日志输出可到多种i/o设备
- 日志格式和内容丰富
- 日志分级别,也是日志在级别维度上的分类。
- 可根据日志对象的级别进行过滤、指定输出i/o。
- logger主要负责继承关系基本单位
- handler负责输出方向的基本单位
- formater负责日志格式
- filter负责过滤,可安置于logger和handler,配合上继承传播机制,灵活过滤。
- logging.basicConfig()调用进行默认初始化。
- Logger对象
- handler对象
- formatter对象
- filter对象
- log recoder对象
- 内置扩展config模块一次性加载所有相关对象
- 内置handers模块提供多种handler类型
logging模块的设计过程
可以参考JAVA的日志系统设计,这篇文章采用幽默诙谐的语言基调,从需求出发,阐明日志系统是怎么设计出来的。
我这里按照这篇文章,简单勾画下重点
- 需求
- 日志消息可以输出到多种媒介中
- 日志消息内容可以做格式化
- 程序代码是通过package,module来组织分类的,要满足不同包,不同模块的代码,有自己的日志处理方式,同时也要有日志分类(日志其实就是某个Event事件发生了的记录),由于是某个事件发生触发了日志,所以这个事件的级别要通过日志来体现,所以日志还要分级别。
- 按照上一条需求,日志有了级别,那么就要在这种日志分类上有文章可做。比如,不同日志级别的处理方式不同,不同日志级别之间还有有轻重缓急的关系。根据轻重缓急可以已设置某个日志级别以上的处理方式和以下的级别的处理方式。
- 日志的处理和日志的输出两个地方还可以定制化过滤需求。
- 分析需求
- 首先,要有个类来抽象出日志消息,这个类至少有两个属性时间戳,消息本身。日志消息本来就是记录了一个事件的发生。那就用LogRecord类来表示日志消息。
- LogRecord要输出到不同媒介中,那就抽象一个Handler类,不同的Handler处理LogRecord到不同的媒介中
- LogRecord要格式化输出,那么就要有一个Formatter类,不同Formatter可以处理为不同的格式类型。
- 要满足不同包,模块,有自己的处理方式。从用户最后使用的角度考虑,我要每次都创建多个Handler太麻烦了,那就还需要一个来整合这些对象的一个处理器,这是一个抽象出来的类,就叫Logger。每个包,模块都可以有自己的logger,可以用独特的名字来标识这个Logger的唯一性。
- 日志消息LogRecord是分级别的,其它处理LogRecord的也都可以根据其级别做不同处理。
- 在logger和handler这两个日志路由位置进行过滤。即增加一个Filter过滤对象到logger和handler。
这样根据需求围绕核心几个类的关系
Logger包含Handler,Handler包含Formatter。都处理LogRecord。Logger和Handler都可以添加Filter对LogRecord进行过滤。
logger的继承
1.就是通过logger的名字的命名层级来查找其parent logger。使用'.'来表示继承关系。没有了通过占位对象占位。
2.logger的继承其实是继承几个方面
2.1. level的继承:子logger写日志的时候,优先使用本身设置的level,如果没有设置,则逐层向上级父logger查询,直到查询到为止。最极端的情况是,使用rootlogger的默认日志级别——WARNING.可以使用logger.getEffectivaLeve()获取有效的等级。
2.2. handler的继承,先将日志对象传递给子logger的所有handler处理,处理完毕,如果孩子的logger的propagate属性设置的是1(TRUE),那么日志record对象会被传播给上级父logger,该父logger的handler都会进行处理,如果父logger的propagate也是1,那么会继续向上。如果子logger的propagate就是0,就不会传播,极端,如果是子logger都没有handler,且不传播,那么就会极端的给logging.lastResort这是一个内置的handler,不会和任何一个logger关联,and acts like StreamHandler which the event description message to the current value of sys.stderr.这个logging.lastResort是没有formatted的,所以我们会看到就message原始格式,输出到标准输出中。
3. 总的来说,其实是逻辑上的继承,只是利用层级关系,可以来使用父logger的level和handler。继承的管理是Manager对象。
logger在逻辑上的继承结构
logger的构建是通过logger的命名创建出的。名字在整个程序中都是唯一的表示。
这样说的设计就可以实现将你自己的日志和使用第三方模块所产生的日志结合在一起。不用在自己的代码中去刻意再写兼容第三方模块产生的日志的处理。都交给logging就可以整合处理了。
Logger 间继承关系的表示:
- 是通过'.'号来表示的。如:'foo' 'foo.bar' 'foo.bar.baz' 'foo'是'foo.bar'的父亲,'foo.bar' 是'foo.bar.baz' 的父亲。'foo'的父亲其实就是'root'logger。
- 这种等级关系的表示方法,非常类似于包和模块之间的表示层级关系的方法。
logging.basicConfig()
该模块级函数是设置root logger.如果root logger已经有handler的化,那么调用函数是没有作用的,以下是源码:
def basicConfig(**kwargs):
_acquireLock()
try:
if len(root.handlers) == 0:
filename = kwargs.get("filename")
if filename:
mode = kwargs.get("filemode", 'a')
hdlr = FileHandler(filename, mode)
else:
stream = kwargs.get("stream")
hdlr = StreamHandler(stream)
fs = kwargs.get("format", BASIC_FORMAT)
dfs = kwargs.get("datefmt", None)
fmt = Formatter(fs, dfs)
hdlr.setFormatter(fmt)
root.addHandler(hdlr)
level = kwargs.get("level")
if level is not None:
root.setLevel(level)
finally:
_releaseLock()
Logger Objects
- Loggers are never instantiated directly,but always through the module-level function logging.getLogger(name).
- logger对象不能直接通过Logger类实例,都是通过模块级函数logging.getLogger(name)调用产生的。
- Mutiple calls to getLogger() with the same name will always return a refernece to the same Logger object.
- Logger的继承关系除了可以继承父级的一些东西如level(getEffectiveLevel())。还可以通过继承关系将LogRecord向上传播。
logger经常使用的方法:
方法 | 用法 | 注释 |
---|---|---|
logger.info() | ||
logger.debug() | ||
logger.setLevel | ||
logger.getEffectiveLevel() | ||
logger.getChild(suffix) | 返回一个子logger,子logger的名字会是父logger名字加上suffix | |
logger.addFilter(filt) | ||
logger.removeFilter(filt) | ||
logger.addHandler(hd) | ||
logger.removeHandler(hd) |
Handler
Handler是一个基础类,其它不同类型的handler都是继承于它。同样,handler也不会直接实例化对象,而是使用它的子类。
常用类型Handler
Handler | 所在模块 | 注释 |
---|---|---|
StreamHandler | logging | |
FileHandler | logging | |
NullHandler | loggging | |
BaseRotatingHandler | logging.handlers | |
RotatingFileHandler | logging.handlers | |
TimedRotatingFileHandler | logging.handlers | |
SocketHandler | logging.handlers |
等等。。。
Handler常用方法:
- Handler.addFilter()
- Handler.removeFilter()
- Handler.setFormatter()
- Handler.setLevel()
Formatter
该类对象是用于对LogRecord进行格式化处理的。
Formtter对象的初始化需要指定日志字符串格式,还可以指定时间格式,和格式化字符串使用的风格。
Fomatter对象是添加到Handler对象中的。
Filter
Filter对象初始化需要指定过滤logger名,只有指定的logger名所产生的LogRecord才会通过该filter,不然会被它过滤掉。logger名过滤是指不会过滤指定logger名及其子logger。
LogRecord
日志内容的载体。
LogRecord的实例都是通过Logger对象的方法实例的
LogRecord的主要分类就是在日志级别上的分类,分为DEBUG,INFO,WARNING,ERROR,CRITICAL
读取logging配置
参考《02. logging模块利用配置加载logger》
还是贴点实践代码吧
import logging
logging.basicConfig(filename='logging_train.log',level=logging.DEBUG)
logger1 = logging.getLogger('1')
logger2 = logger1.getChild('2') # logger2继承于logger1:即会是1.2
logger2.propagate = True # 是否向上传递LogRecord对象
class myfilter(logging.Filter): # 定义自己的Filter类
def __init__(self, s): # 过滤所有不包含指定字符串的日志
self.filter_string = s
def filter(self, red): # logger和handler会调用这个方法判定过滤结果。
if self.filter_string in red.getMessage():
return True
else:
return False
filter1 = myfilter('world')
handler1 = logging.FileHandler('logging_train1.log')
handler2 = logging.StreamHandler()
formatter1 = logging.Formatter('%(asctime)s %(name)s %(threadName)s %(process)s %(levelname)s %(message)s')
handler2.setFormatter(formatter1)
logger1.addHandler(handler1)
logger2.addHandler(handler2)
logger2.addFilter(filter1)
logger2.setLevel(logging.CRITICAL)
logger2.error("hello world!")
logger2.critical("Python is the best all over the world!")