日志能够记录程序中问题信息,用户通过它来检查错误发生的原因。Python logging 模块是一个日志记录的模块。logging 模块的工作流程为:
记录器产生日志信息,并将日志信息封装成 LogRecord 对象,接下来检查日志的级别是否达到处理的级别。如果未达到,则丢弃该日志信息。如果达到,则将其送入到过滤器中,如果通过过滤器,则将其送到处理器中。
在处理器中也会检查日志的级别,如果级别符合则将其送入到过滤器中,如果通过过滤器,则将日志信息送入到格式器中进行字符串格式化,最后送入到指定的设备中,例如:标准输出、标准错误、文件、网络等等。
在这个过程中重要的组件共有以下四个:
- 记录器:暴露了应用程序代码直接使用的接口
- 处理器:日志记录(由记录器创建)发送到适当的目标
- 过滤器:提供了更精细的附加功能,用于确定要输出的日志记录
- 格式器:指定最终输出中日志记录的样式
1. 记录器
记录器暴露了应用程序代码直接使用的接口。在使用这些接口之前,我们得先要了解日志的级别,不同的级别代表了这条日志的重要程度,在输出时会收到级别的限制。比如:我们设置输出的日志级别为 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 日志
2. 处理器
在上面的例子中,我们发现我们分别调用了 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
3. 过滤器
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 日志
4. 格式器
格式器的作用是用来指定最终输出中日志记录的样式。格式器用在处理器上,这是因为处理器是用于输出。
%(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