Python Logging

日志能够记录程序中问题信息,用户通过它来检查错误发生的原因。Python logging 模块是一个日志记录的模块。logging 模块的工作流程为:

记录器产生日志信息,并将日志信息封装成 LogRecord 对象,接下来检查日志的级别是否达到处理的级别。如果未达到,则丢弃该日志信息。如果达到,则将其送入到过滤器中,如果通过过滤器,则将其送到处理器中。

在处理器中也会检查日志的级别,如果级别符合则将其送入到过滤器中,如果通过过滤器,则将日志信息送入到格式器中进行字符串格式化,最后送入到指定的设备中,例如:标准输出、标准错误、文件、网络等等。

在这个过程中重要的组件共有以下四个:

  1. 记录器:暴露了应用程序代码直接使用的接口
  2. 处理器:日志记录(由记录器创建)发送到适当的目标
  3. 过滤器:提供了更精细的附加功能,用于确定要输出的日志记录
  4. 格式器:指定最终输出中日志记录的样式

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
未经允许不得转载:一亩三分地 » Python Logging
评论 (0)

8 + 2 =