日志能够记录程序中问题信息,用户通过它来检查错误发生的原因。Python logging 模块是一个日志记录的模块。logging 模块的工作流程为:
记录器产生日志信息,并将日志信息封装成 LogRecord 对象,接下来检查日志的级别是否达到处理的级别。如果未达到,则丢弃该日志信息。如果达到,则将其送入到过滤器中,如果通过过滤器,则将其送到处理器中。
在处理器中也会检查日志的级别,如果级别符合则将其送入到过滤器中,如果通过过滤器,则将日志信息送入到格式器中进行字符串格式化,最后送入到指定的设备中,例如:标准输出、标准错误、文件、网络等等。
在这个过程中重要的组件共有以下四个:
-
记录器:暴露了应用程序代码直接使用的接口
-
处理器:日志记录(由记录器创建)发送到适当的目标
-
过滤器:提供了更精细的附加功能,用于确定要输出的日志记录
-
格式器:指定最终输出中日志记录的样式
-
记录器 {#title-0} =================
记录器暴露了应用程序代码直接使用的接口。在使用这些接口之前,我们得先要了解日志的级别,不同的级别代表了这条日志的重要程度,在输出时会收到级别的限制。比如:我们设置输出的日志级别为 ERROR,那么当你输出的日志是 ERROR 以下的级别时,该日志信息将被过滤掉,不会被输出。
数字越大,该信息的重要性就越大,忽略该信息会给整个软件系统带来的安全隐患就越大。**注意:**在设置日志级别时,WARN 和 WARNING 是相同的,FATAL 和 CRITICAL 是相同的。
注意:NOTSET 意指不设置,按照父 Logger 级别来过滤日志
常用的直接使用的接口有:
import logging
def test01():
# 1. 创建日志对象
logger = logging.getLogger()
# 2. 默认日志级别
print('日志级别:', logger.level)
# 3. 设置日志级别
logger.setLevel(logging.DEBUG)
# 3. 打印不同日志
logger.debug(msg='debug 日志')
logger.info(msg='info 日志')
logger.warning(msg='warning 日志')
logger.error(msg='error 日志')
logger.critical(msg='critical 日志')
if __name__ == '__main__':
test01()
程序的输出结果:
日志级别: 30
warning 日志
error 日志
critical 日志
- 处理器 {#title-1} =================
在上面的例子中,我们发现我们分别调用了 5 个级别的日志接口来打印日志信息,但是只显示出了 warning、error、critical 级别的信息,其他的信息并没有显示。
为什么没有输出?
首先,我们已经将记录器默认的日志级别由 WARNING 修改为 DEBUG 级别。所以,记录器并不存在日志级别的限制。
这里就引出一个 Handler 的概念。处理器的作用是将日志记录发送到指定的目标。那么,很有可能是目标设备不接受该级别的信息。我们先看下常用的处理器有哪些?
FileHandler 用于将日志记录输出到文件中
StreamHandler 用于将日志记录输出到标准输出、或者标准错误中
logging 模块默认使用的是 StreamHandler,并且设置了该 Handler 的日志级别为 WARNING。我们可以通过修改默认 Handler 的日志级别来实现所有级别日志的输出,如下代码所示:
# 未添加任何 Handler 时使用的默认 Handler
# _defaultLastResort = _StderrHandler(WARNING)
# lastResort = _defaultLastResort
def test02():
# 1. 创建日志对象
logger = logging.getLogger()
# 2. 修改记录器日志级别
logger.setLevel(logging.DEBUG)
# 3. 修改默认处理器日志级别
logging.lastResort.setLevel(logging.DEBUG)
# 4. 打印不同日志
logger.debug(msg='debug 日志')
logger.info(msg='info 日志')
logger.warning(msg='warning 日志')
logger.error(msg='error 日志')
logger.critical(msg='critical 日志')
_StderrHandler 时 StreamHandler 的子类,并且默认绑定了 WARNING 日志级别,如下源码所示:
class _StderrHandler(StreamHandler):
"""
This class is like a StreamHandler using sys.stderr, but always uses
whatever sys.stderr is currently set to rather than the value of
sys.stderr at handler construction time.
"""
def __init__(self, level=NOTSET):
"""
Initialize the handler.
"""
Handler.__init__(self, level)
@property
def stream(self):
return sys.stderr
_defaultLastResort = _StderrHandler(WARNING)
# lastResort 使用的默认处理器
lastResort = _defaultLastResort
这里要注意标准错误和标准输出的区别:标准输出是带缓冲区的,标准错误是不带缓冲区。我们知道缓冲区分为块缓冲区、行缓冲区,这里指的是行缓冲区。文件读写使用的是块缓冲区。即:标准输出碰到换行符或者缓冲区满的时候才会进行输出。
处理器的使用方法示例
我们在构建 StreamHandler 时可以指定 stream=stdout,使处理器绑定标准输出。指定 stream=stdout 或者 stream=stderr 使处理器绑定标准错误。
另外,关于日志输出设备。它可以输出到标准的输入和输出、也可以输出到文件、同理也可以输出到网络 (需要自定义 Handler 类),下面演示下用法:
import logging
import sys
def test03():
# 1. 获得 root 日志对象
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# 2. 绑定多个处理器
handler1 = logging.StreamHandler(sys.stderr) # 关联标准错误
handler1.setLevel(logging.INFO)
logger.addHandler(handler1)
handler2 = logging.StreamHandler(sys.stdout) # 关联标准输出
handler2.setLevel(logging.INFO)
logger.addHandler(handler2)
handler3 = logging.FileHandler(filename='demo.log') # 关联文件输出
handler3.setLevel(logging.INFO)
logger.addHandler(handler3)
# 4. 打印不同级别日志
logger.debug(msg='debug 日志')
logger.info(msg='info 日志')
logger.warning(msg='warning 日志')
logger.error(msg='error 日志')
logger.critical(msg='critical 日志')
if __name__ == '__main__':
test03()
程序执行结果:
info 日志
warning 日志
error 日志
critical 日志
info 日志
warning 日志
error 日志
critical 日志
还有一部分输出到了 demo.log 日志文件中。
层级结构下的处理器调用规则
如果记录器有存在层级结构,子记录器不存在处理器时,会向上追溯搜索父记录器的处理器,如果父记录器存在处理器,则调用该处理器。请看下面源码实现 (Logger 类):
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
- 过滤器 {#title-2} =================
Logger 和 Handler 都继承了 Filterer 类,两者都可以添加过滤器,并且日志记录传递时,会经过两次过滤。一次是 Logger 处理日志记录时会根据自身添加的过滤器列表过滤一次,如下 Logger handle 方法的实现:
def handle(self, record):
"""
Call the handlers for the specified record.
This method is used for unpickled records received from a socket, as
well as those created locally. Logger-level filtering is applied.
"""
if (not self.disabled) and self.filter(record): # 执行过滤
self.callHandlers(record) # 调用处理器
一次是日志消息传递到 Handler 时会也会再次根据自身添加的过滤器列表过滤一次,然后最终输出到指定设备,如下 Handler handle 方法的实现:
def handle(self, record):
"""
Conditionally emit the specified logging record.
Emission depends on filters which may have been added to the handler.
Wrap the actual emission of the record with acquisition/release of
the I/O thread lock. Returns whether the filter passed the record for
emission.
"""
rv = self.filter(record) # 执行过滤
if rv:
self.acquire()
try:
self.emit(record) # 输出到设备
finally:
self.release()
return rv
自定义过滤器:
import logging
import sys
class CustonLoggerFilter(object):
def __init__(self):
pass
def filter(self, record):
print('日志的记录器:', record.name)
print('日志描述信息:', record.msg)
print('日志发出文件:', record.filename)
print('日志创建时间:', record.created)
print('日志创建时间:', record.created)
print('日志发出函数:', record.funcName)
print('日志发出行号:', record.lineno)
print('日志发出级别:', record.levelno)
print('日志发出模块:', record.module)
print('-' * 30)
return True
def test04():
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# 记录器添加自定义过滤器
filter = CustonLoggerFilter()
logger.addFilter(filter)
# 处理器添加自定义过滤器
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
handler.addFilter(filter)
logger.addHandler(handler)
logger.debug(msg='debug 日志')
if __name__ == '__main__':
test04()
程序执行结果:
日志的记录器: root
日志描述信息: debug 日志
日志发出文件: 22-日志模块.py
日志创建时间: 1667240610.3610618
日志创建时间: 1667240610.3610618
日志发出函数: test04
日志发出行号: 107
日志发出级别: 10
日志发出模块: 22-日志模块
------------------------------
日志的记录器: root
日志描述信息: debug 日志
日志发出文件: 22-日志模块.py
日志创建时间: 1667240610.3610618
日志创建时间: 1667240610.3610618
日志发出函数: test04
日志发出行号: 107
日志发出级别: 10
日志发出模块: 22-日志模块
------------------------------
debug 日志
- 格式器 {#title-3} =================
格式器的作用是用来指定最终输出中日志记录的样式。格式器用在处理器上,这是因为处理器是用于输出。
%(name)s Name of the logger (logging channel)
%(levelno)s Numeric logging level for the message (DEBUG, INFO,
WARNING, ERROR, CRITICAL)
%(levelname)s Text logging level for the message ("DEBUG", "INFO",
"WARNING", "ERROR", "CRITICAL")
%(pathname)s Full pathname of the source file where the logging
call was issued (if available)
%(filename)s Filename portion of pathname
%(module)s Module (name portion of filename)
%(lineno)d Source line number where the logging call was issued
(if available)
%(funcName)s Function name
%(created)f Time when the LogRecord was created (time.time()
return value)
%(asctime)s Textual time when the LogRecord was created
%(msecs)d Millisecond portion of the creation time
%(relativeCreated)d Time in milliseconds when the LogRecord was created,
relative to the time the logging module was loaded
(typically at application startup time)
%(thread)d Thread ID (if available)
%(threadName)s Thread name (if available)
%(process)d Process ID (if available)
%(message)s The result of record.getMessage(), computed just as
the record is emitted
示例代码:
def test05():
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(logging.DEBUG)
# 构建输出格式器
formatter = logging.Formatter('[%(levelname)s] "%(message)s" in %(pathname)s',
datefmt='%Y-%m-%d %H:%M:%S')
# 格式器设置到输出器
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.debug(msg='日志信息')
if __name__ == '__main__':
test05()
程序执行结果:
[DEBUG] "日志信息" in /Users/meng/Desktop/面试题/22-日志模块.py