logging 结构
简单分析
logging.debug/info/warning/error
这些顶层函数,其源代码结构为:(以info为例子,其他类似)
# logging/__init__.py 2140
def info(msg, *args, **kwargs):
if len(root.handlers) == 0:
basicConfig()
root.info(msg, *args, **kwargs)
- 判断
root.handlers
长度为0来进行初始化basicConfig
- 然后调用了
root
对应的方法
这个 root
实际上是一个名称为 root
的 Logger
子类实例,在 logging
模块导入时便创建存在
Logger.__init__()
中对其成员变量进行赋值:self.handlers = []
# logging/__init__.py 1778
class RootLogger(Logger):
def __init__(self, level):
"""
Initialize the logger with the name "root".
"""
Logger.__init__(self, "root", level)
def __reduce__(self):
return getLogger, ()
# logging/__init__.py 1935
root = RootLogger(WARNING)
Logger.root = root
Logger.manager = Manager(Logger.root)
basicConfig
函数是用于对 logging
的配置
-
在上面没有传入任何参数,那么
root.handlers
会被初始化为仅包含StreamHandler
的列表stream = kwargs.pop("stream", None) h = StreamHandler(stream)
-
如果指定文件名,那么会被初始化为仅包含
FileHandler
的列表:filename = kwargs.pop("filename", None) mode = kwargs.pop("filemode", 'a') if filename: if 'b' in mode: errors = None else: encoding = io.text_encoding(encoding) h = FileHandler(filename, mode, encoding=encoding, errors=errors)
- 如果指定了
handlers
参数,那么会被初始化为该handlers
- 如果指定了
当首次调用 logging.xxx
时,basicConfig
就会被调用;
随后调用 root.xxx
,实际上是 Loggger
提供的方法:
# logging/__init__.py#class Logger
def info(self, msg, *args, **kwargs):
# 检查是否输出 INFO 级别的日志
if self.isEnabledFor(INFO):
self._log(INFO, msg, args, **kwargs)
进入 _log
方法:
- 对传入的参数进行检查处理
- 生成
LogRecord
实例,然后交由handle
方法处理
# logging/__init__.py#class Logger 1600
def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False,
stacklevel=1):
....
record = self.makeRecord(self.name, level, fn, lno, msg, args,
exc_info, func, extra, sinfo)
self.handle(record)
进入 handle
方法:
# logging/__init__.py#class Logger 1626
def handle(self, record):
# 该 logger 非禁用状态且过滤后任存在
if (not self.disabled) and self.filter(record):
self.callHandlers(record)
进入 callHandlers
方法:
- 从
while
循环中可以看到,不断向上查找c = c.parent
,每次交由父Logger的handlers
处理 c.propagate
表示是否将日志向上传递- 如果不能找到
handler
进行处理,则会交由lastResort
进行处理
# logging/__init__.py#class Logger 1626
def callHandlers(self, record):
c = self
found = 0
while c:
for hdlr in c.handlers:
found = found + 1
if record.levelno >= hdlr.level:
hdlr.handle(record)
if not c.propagate:
c = None #break out
else:
c = c.parent
if (found == 0):
if lastResort:
if record.levelno >= lastResort.level:
lastResort.handle(record)
elif raiseExceptions and not self.manager.emittedNoHandlerWarning:
sys.stderr.write("No handlers could be found for logger"
" \"%s\"\n" % self.name)
self.manager.emittedNoHandlerWarning = True
# logging/__init__.py 1243
# 如果没有任何的 handler 可以处理,那么会交由该 Handler 处理
_defaultLastResort = _StderrHandler(WARNING)
lastResort = _defaultLastResort
从这部分代码可以意识到
logger
是一个层级结构(树形结构)
logging.getLogger
该方法用于获取 name
对应的 logger
实例
传入一个 name
参数,用于设置 logger
的名称
- 如果
name == None
或name == "root"
或name
不是str
类型时,返回root
实例(上面提到root
在模块导入时便创建存在了) - 否则交由
Logger.manager
创建
# logging/__init__.py 2071
def getLogger(name=None):
"""
Return a logger with the specified name, creating it if necessary.
If no name is specified, return the root logger.
"""
if not name or isinstance(name, str) and name == root.name:
return root
return Logger.manager.getLogger(name)
Logger.manager
是 logging.Manager
的实例:
-
根据文档描述:通过一个指定的
name
(用.
划分的层级结构)来获得logger
实例- 层级结构:举个例子
a.b.c
是a.b
的子级,而后者是前者的父级 - 对于每层的
name
获取对应的实例logger[name]
- 如果是没有创建而有
PlaceHolder
占位(这种情况属于子级已经创建,但是父级没有创建)则将其创建 - 如果没有创建也没有
PlaceHolder
则直接创建
- 如果是没有创建而有
- 层级结构:举个例子
-
PlaceHolder
:维护着其所有子级的name
,当创建logger
时会将子级的logger.parent
指向该实例.# logging/__init__.py 1249 class PlaceHolder(object): def __init__(self, alogger): self.loggerMap = { alogger : None } def append(self, alogger): if alogger not in self.loggerMap: self.loggerMap[alogger] = None
# logging/__init__.py#Manager 1315
def getLogger(self, name):
rv = None
if not isinstance(name, str):
raise TypeError('A logger name must be a string')
_acquireLock()
try:
# self.looggerDict类似缓存,记录着所有已经创建的Logger/PlaceHolder实例
if name in self.loggerDict:
rv = self.loggerDict[name]
# PlaceHodler占位需要创建一个logger实例替换
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
_fixupParents
用于创建当前子级的父级logger
- 逐层向上查找:比如
a.b.c
先查找a.b
,然后是a
def _fixupParents(self, alogger):
name = alogger.name
i = name.rfind(".")
rv = None
while (i > 0) and not rv:
substr = name[:i]
# 父级 logger 尚未创建且没有对应的PlaceHolder,使用 PlaceHolder 占位
if substr not in self.loggerDict:
self.loggerDict[substr] = PlaceHolder(alogger)
else:
obj = self.loggerDict[substr]
# 父级已经创建
if isinstance(obj, Logger):
rv = obj
else:
# 父级尚未创建
assert isinstance(obj, PlaceHolder)
obj.append(alogger)
i = name.rfind(".", 0, i - 1)
# 该子级没有已经创建好的 logger,则设置为 root
if not rv:
rv = self.root
alogger.parent = rv
总结
-
每个 Logger 实例维护着一个
parent
实例,对应着其父级的logger
,类似一个树形结构-
如果对应父级尚未创建则会用
PlaceHolder
占位 -
所有
logger
向上查找最终会到root
-
-
每个
name
对应的logger
都是单例,在Logging.manager.loggerDict
中存放; -
在输出日志时,先将日志信息封装成一个
LogRecord
实例,然后交由所有 Logger 实例中的handlers
进行处理; -
模块初始化流程:
- 模块在载入时会实例化一个name为
root
的 Logger 实例,此时root.handlers
是没有任何Handler
实例; - 在 第一次
logging.xxx
前如果没有手动执行basicConfig
函数,那么会执行一次,执行后root.handlers
有一个StreamHandler
实例,将日志输出至sys.stderr
; - 如果已经执行过
basicConfig
函数,那么会根据传参进行设置- 指定了
filename
,则root.handlers = [FileHandler]
- 指定了
handlers
,则root.handlers = handlers
- 上面都没有指定,则
root.handlers = [StreamHandler]
- 指定了
- 模块在载入时会实例化一个name为
-
日志输出流程:
先封装为 LogRecord 实例,然后向上查找父级并交由其
handlers
处理(从当前 logger 到
root
的路径上所有 handlers 都会处理一遍)如果找不到
handlers
处理,那么最后会交给lastResort
处理注意保证输出到控制台的Handler只有一个,否则就会产生多次输出,就比如下面的例子:
logger = logging.getLogger('example') logger.addHandler(logging.StreamHandler()) logging.basicConfig() # 此时 logging.root有一个StreamHandler,而 logger也有一个StreamHandler,就会导致输出两次 logger.warning("warn“)
LogRecord经过 Handler处理时:
- 先判断是否大于等于其日志等级
- 经过
Handler#filter
过滤 Handler#format
将 LogRecord 实例按照指定的格式转化为字符串Hander#emit
将格式化后的字符串输出到指定的位置(StreamHandler
为sys.stderr
,FileHandler
则为指定的filename
)