万字长文 - Python 日志记录器logging 百科全书 - 高级配置之 日志分层-LMLPHP

万字长文 - Python 日志记录器logging 百科全书 - 高级配置之 日志分层

前言

在 Python 的logging模块中,它不仅提供了基础的日志功能,还拥有一系列高级配置选项来满足复杂应用的日志管理需求。

说到logging 模块的高级配置,必须提及日志分层logging.config配置日志异步操作等关键功能。它们每一项都为开发者提供了强大的调试和监控环境,对于构建可维护和高效的日志系统至关重要。

在接下来的三篇logging高级配置 文章中,我将为读者朋友们介绍 Python logging 模块中的三个高级配置的具体应用:日志分层logging.config 以及 日志异步操作,探讨它们如何优化日志处理流程,并提升应用的整体性能。

本文将首先聚焦于 logging 模块中的日志分层概念,解析其如何使开发者能够构建具有层级结构的日志记录系统,并高效地管理和过滤日志信息。

知识点📖📖

导入模块

import logging
import logging.handlers


文章脉络:


理解日志分层

作用

日志分层在复杂应用程序中提供了高度的组织性和灵活性,允许针对不同模块或组件进行细致的日志管理,从而优化调试、维护效率,并增强整体日志系统的性能和可读性。


将 Python 的 logging 模块中的日志分层类比为 Python 中的类继承(父类和子类关系),这样可以有助于理解日志记录器(logger)之间的继承和行为传播机制。

  1. 继承:在类的继承中,子类继承父类的属性和方法。类似地,在日志分层中,子记录器会继承父记录器的配置,如日志级别和关联的处理器(handlers)。

  2. 重写和自定义:就像子类可以重写或扩展父类的方法和属性,子记录器也可以有自己的日志级别和处理器,甚至可以完全覆盖父记录器的设置。

  3. 层级关系:正如类可以有多层继承关系,日志记录器也可以形成层级结构,允许复杂和细致的控制。

  4. 传播行为:在类继承中,子类的行为可以反映父类的特征,而在日志分层中,日志消息的传播(默认情况下)会从子记录器向上到父记录器,除非显式地将 propagate 属性设置为 False

需要注意的是,这只是一个比喻,实际的实现细节和概念上仍有区别。例如,类继承是面向对象编程中的一个核心概念,涉及到更广泛的编程模式,而日志分层主要是关于信息传递和处理的策略。

简单总结如下:


疑惑

在学习了前面的 万字长文 - Python 日志记录器logging 百科全书 之 日志过滤 和 初步了解到日志分层的作用之后,可能会有读者朋友们有疑问:

以下是我在深入了解日志分层 的作用之后的回答:

  • 单个日志记录器配合多个处理器:在这种情况下,我们可以根据日志的类型(如错误、警告、信息等)将日志定向到不同的处理器(如文件、控制台等)。这种方法在应用结构相对简单,或者当日志需求主要围绕不同类型的日志处理时很有效。
  • 使用分层多个日志记录器和处理器:这种方法允许按模块或组件分别记录日志。每个模块或组件可以有其自定义的日志级别和处理器。这种方式适用于更复杂的应用程序,其中不同部分可能有不同的日志需求。

分层日志在大型和复杂的应用程序中提供了更好的组织性和灵活性,使得对不同模块或组件的日志管理更加精细和高效。


应用场景

日志分层的应用场景主要体现在大型、多模块的应用程序和微服务架构中,以及多团队协作的开发项目里,它帮助各个模块独立地控制日志记录,简化问题追踪和调试,提高维护效率。

  1. 大型应用程序:在大型应用程序(如电子商务平台、企业级软件等)中,应用通常分为多个模块或组件。每个组件可以有自己的日志记录器,这些记录器可以根据组件的具体需求进行配置。
  2. 多团队开发:在多团队协作的项目中,每个团队可能负责应用的不同部分。日志分层允许每个团队为其负责的部分单独配置日志。
  3. 微服务架构:在微服务架构中,每个微服务可以配置自己的日志记录器。这样可以确保每个服务的日志信息都是独立和清晰的。
  4. 调试和维护:在应用程序的维护和调试过程中,可以针对出现问题的特定模块调整日志级别,从而获取更详细的日志信息。
  5. 统一基础配置:在一个复杂的应用中,各个组件可能有不同的日志需求。使用分层结构,可以在顶层记录器上定义通用的日志策略,然后根据需要为特定子记录器定制日志行为。这种方式使得日志系统更易于维护和更新。

日志分层的要点

命名规范和核心概念

以下内容来自官方文档:


getLogger() 返回对具有指定名称的记录器实例的引用(如果已提供),或者如果没有则返回 root 。名称是以句点分隔的层次结构。多次调用 getLogger()具有相同的名称将返回对同一记录器对象的引用。在分层列表中较低的记录器是列表中较高的记录器的子项。例如,给定一个名为 foo 的记录器,名称为 foo.bar 、 和 foo.bam 的记录器都是 foo 子项。

记录器具有 有效等级 的概念。如果未在记录器上显式设置级别,则使用其父记录器的级别作为其有效级别。如果父记录器没有明确的级别设置,则检查 父级。依此类推,搜索所有上级元素,直到找到明确设置的级别。根记录器始终具有明确的级别配置(默认情况下为 WARNING )。在决定是否处理事件时,记录器的有效级别用于确定事件是否传递给记录器相关的处理器。

子记录器将消息传播到与其父级记录器关联的处理器。因此,不必为应用程序使用的所有记录器定义和配置处理器。一般为顶级记录器配置处理器,再根据需要创建子记录器就足够了。(但是,你可以通过将记录器的 propagate 属性设置为 False 来关闭传播。)


我提炼如下,简要说明了关于日志分层中的日志记录器行为和层次结构的核心概念:

  • 记录器实例引用:使用getLogger()函数可以获取具有特定名称的记录器实例。如果使用相同的名称多次调用getLogger(),将返回同一个记录器对象的引用。

  • 命名规范:记录器(logger)的名称通常遵循点号分隔的层级结构,类似于 Python 包和模块的命名方式。例如,'foo.database' 表示 foo 下的 database 子模块的记录器。

  • 层级关系:记录器的层级结构通过命名来实现。在这个层级中,一个记录器可以是另一个记录器的子项。例如,如果有一个名为 'foo' 的记录器,那么 'foo.bar''foo.bar.baz' 就是它的子记录器。

  • 有效级别:如果调用 getLogger() 时不提供名称,将返回根记录器。根记录器是所有记录器的最顶层父记录器,它的默认级别为 WARNING

  • 消息传播:子记录器的日志消息会传播到其父记录器的处理器,除非设置了propagate属性为False。通常只需为顶级记录器配置处理器,子记录器可以继承这些处理器。

这种层级结构和命名规范为大型和复杂的应用程序提供了一种高效和灵活的日志管理方式。通过恰当地命名和配置记录器,可以轻松地管理应用程序的不同部分所生成的日志,确保日志信息的清晰和有序。


命名不规范的问题

在 Python 的 logging 模块中,如果顶级日志记录器的命名与子记录器的命名不遵循统一的层级结构,会出现以下几个问题:

  1. 继承失效:日志记录器之间的层级关系是通过它们的命名来确定的。如果顶级记录器和子记录器的命名不遵循统一的层级前缀,子记录器将无法正确继承顶级记录器的配置(如处理器、级别等)。这意味着我们需要为每个子记录器单独配置处理器和其他设置,增加了配置复杂性。
  2. 日志传播问题:通常,日志消息会从子记录器传播到父记录器。如果子记录器的命名不遵循正确的层级结构,这种传播可能无法发生,导致日志消息不会被预期的父记录器(和其关联的处理器)处理。
  3. 可维护性和可读性降低:在大型项目中,清晰和一致的日志记录器命名极为重要。不遵循统一的命名规范会使代码难以理解和维护,特别是在涉及多个开发者和模块的情况下。

为了避免这些问题,所以需要使用一致的层级命名约定。例如,顶级日志记录器命名为 'foo',那么子记录器应该以 'foo.' 作为前缀,如 'foo.bar''foo.baz' 等,以确保正确的继承和日志消息传播。

什么时候使用 propagate

可能会有读者朋友们有疑问:

其实要回答这个疑问,就应该重新审视日志分层的作用和应用场景,以及为什么即使在某些情况下禁用了传播,日志分层仍然是有价值的。

使用 logger.propagate = False 的情况,

  1. 避免重复日志记录:当在不希望某个子记录器(logger)的日志被其父记录器也处理时。例如,当已经为子记录器配置了特定的处理器(handler)并且不希望同样的日志消息再次由更高级别的记录器处理,这时设置 propagate = False 可以防止日志消息向上传播。

  2. 特定日志处理:当我们希望对某个模块或组件的日志进行特殊处理,与应用程序的其他部分区别开来。例如,我们可能有一个记录安全相关日志的记录器,我们不希望这些日志被常规的应用程序日志处理器处理。

  3. 独立日志流:在构建库或框架时,可能希望库的日志独立于使用该库的应用程序的日志系统。在这种情况下,可以为库设置一个专用的记录器,并设置 propagate = False,以防止库的日志消息污染或干扰主应用程序的日志。

  4. 性能考虑:在一些性能敏感的应用中,防止不必要的日志传播可以减少一些处理开销,特别是当有大量的日志消息和复杂的日志处理器配置时。

即使在上面的这些情况下,分层结构依然有助于维护清晰的组织架构。我们可以为特定的模块创建专用的子记录器,给予它独立的处理器和格式化器,而不必在每个模块中创建和配置新的独立记录器。


示例代码

代码:

# -*- coding: utf-8 -*-

import logging
import logging.handlers
import sys

import requests

# 全局基础配置, 日志格式化配置
formatter = logging.Formatter('%(levelname)-7s - %(asctime)s - %(name)s - %(message)s')


class RemoteLogHandler(logging.Handler):
    """自定义远程处理器"""

    def __init__(self, remote_url, logger):
        super().__init__()
        self.remote_url = remote_url
        self.error_logger = logger
        self.setFormatter(formatter)

    def emit(self, record):
        # 发送日志记录到远程服务器
        log_entry = self.format(record)  # 格式化日志记录
        try:
            response = requests.post(self.remote_url, data=log_entry)
            response.raise_for_status()
        except Exception as e:
            record.msg = f"Original message: {record.msg}, Failed to send log to remote: {str(e)}"
            print('error_logger 等级是 ', self.error_logger.level)
            self.error_logger.handle(record)


def setup_logger(*handlers, name, level=logging.INFO):
    """
    用于设置特定记录器的函数,支持多个处理器.

    Args:
        *handlers(logging.Handler): 日志处理器
        name(str): 日志记录器名称
        level(int): 日志等级

    Returns:
        日志记录器.
    """
    logger = logging.getLogger(name)
    logger.setLevel(level)
    for handler in handlers:
        handler.setFormatter(formatter)
        logger.addHandler(handler)
    return logger


def setup_file_handler(filename, level=logging.DEBUG):
    """
    setup file handler

    Args:
        filename(str): 日志文件的名称
        level(int): 日志处理器的级别, 默认为logging.DEBUG

    Returns:

    """
    file_handler = logging.FileHandler(filename=filename, delay=True)
    file_handler.setLevel(level=level)
    file_handler.setFormatter(formatter)
    return file_handler


def setup_stream_handler(stream=sys.stdout, level=logging.INFO):
    stream_handler = logging.StreamHandler(stream=stream)
    stream_handler.setLevel(level=level)
    stream_handler.setFormatter(formatter)
    return stream_handler


def create_remote_log_handler(url, logger, level=logging.INFO):
    remote_handler = RemoteLogHandler(url, logger)
    remote_handler.setLevel(level=level)
    remote_handler.setFormatter(formatter)
    return remote_handler


if __name__ == '__main__':
    # 顶层日志记录器
    # 处理器配置, 文件处理器 和 控制台处理器
    file_handler_global = setup_file_handler(filename='ecommerce_global.log')
    stream_handler_global = setup_stream_handler(stream=sys.stdout)
    global_logger = setup_logger(
        file_handler_global, stream_handler_global, name='ecommerce', level=logging.DEBUG
    )

    # 创建错误记录器和处理器
    error_file_handler = setup_file_handler(filename='error.log', level=logging.ERROR)
    error_logger = setup_logger(error_file_handler, name='error', level=logging.WARNING)

    # 实例化RemoteLogHandler
    remote_handler_global = create_remote_log_handler(
        url='http://127.0.0.1:5000/submit_log', logger=error_logger, level=logging.ERROR
    )

    # 订单处理系统记录器和处理器
    order_file_handler = setup_file_handler('orders.log', level=logging.WARNING)
    order_logger = setup_logger(
        order_file_handler, remote_handler_global, name='ecommerce.orders', level=logging.INFO
    )

    # 支付系统记录器和处理器
    payment_file_handler = setup_file_handler(filename='payments.log', level=logging.ERROR)
    payment_logger = setup_logger(
        payment_file_handler, remote_handler_global, name='ecommerce.payments', level=logging.WARNING
    )
    #
    # 测试打印日志
    global_logger.info('Global logger configured')
    order_logger.warning('Order logger configured')
    payment_logger.error('Payment logger configured')

运行效果

如果程序没有出错的话,可以看到会创建orders.log, payments.logecommerce_global.log 三份日志文件,内容分别如下所示:

  • 可以看到 ecommerce_global.log 文件,打印的日志是包含orders.logpayments.log的。

万字长文 - Python 日志记录器logging 百科全书 - 高级配置之 日志分层-LMLPHP

使用propagate

添加以下两行代码,

  • 设置子记录器的日志消息不传播到父记录器
order_logger.propagate = False
payment_logger.propagate = False

再次运行结果如下:

  • 可以看到没有重复日志记录啦。

万字长文 - Python 日志记录器logging 百科全书 - 高级配置之 日志分层-LMLPHP

代码释义

这份代码是一个复杂的日志系统的实现,适用于需要将日志信息记录到不同位置(如文件、控制台、远程服务器)的应用程序。展示了如何在 Python 中使用 logging 模块来创建一个分层且灵活的日志处理架构。

  1. 自定义远程日志处理器 (RemoteLogHandler)

    • 定义了一个自定义的日志处理器,用于将日志消息发送到远程服务器。
    • 如果发送失败,它会使用另一个配置的日志记录器(error_logger)来处理这些日志消息。
  2. 灵活的日志配置函数

    • 提供了多个辅助函数(setup_logger, setup_file_handler, setup_stream_handler, create_remote_log_handler),使得创建和配置日志记录器和处理器更加灵活和简洁。
  3. 分层日志记录器

    • 创建了多个日志记录器,每个记录器代表应用程序的不同部分(如全局、订单处理系统、支付系统)。
    • 每个记录器可以有自己的处理器和日志级别。

日志分层的体现

  1. 不同功能的日志记录器

    • global_logger 用于全局日志记录。
    • order_logger 专门用于订单处理系统的日志。
    • payment_logger 专门用于支付系统的日志。
  2. 层级命名

    • 日志记录器的命名体现了应用程序的层级结构。例如,ecommerce.ordersecommerce.payments 表示这些记录器是 ecommerce 的子模块。
  3. 日志传播

    • 在这个例子中,如果 propagate 属性没有被设置为 False,那么日志消息会从子记录器传播到父记录器。这意味着 order_loggerpayment_logger 的日志也可能被 global_logger 所处理,除非显式地关闭了传播。

实际应用

这个日志系统适用于大型或模块化的应用程序,其中需要对不同部分的日志进行精细控制。通过这种方法,我们可以确保不同部分的日志被适当地记录和处理,同时保持日志系统的整洁和可维护性。这对于故障排查、性能监控和安全分析等方面非常有用。

总体来说,这个代码示例展示了一个结构化的日志系统,它既灵活又能够适应不同的日志需求,非常适合复杂的应用场景。

总结🎈🎈

本文详细介绍了 Python logging 模块中的日志分层功能,强调了其在构建复杂应用程序中的重要性。以下是文章的主要要点总结:

  1. 日志分层的作用与优势
    • 提高组织性:允许开发者为不同模块或组件设置独立的子记录器。
    • 继承与覆盖:子记录器可以继承父记录器的配置,同时具备自定义设置的能力。
    • 易于问题追踪:通过层级结构,方便快速定位问题所在模块。
  2. 命名规范与层级关系
    • 通过点号分隔的命名规范,确保记录器之间的层级关系清晰明确。
    • 层级结构提供了继承和消息传播的机制,简化了配置并增加了灵活性。
  3. 传播行为与性能优化
    • 默认情况下,子记录器的日志会传播到父记录器,可通过设置 propagate 属性进行控制。
    • 正确使用日志分层可以减少不必要的日志输出,从而优化性能。
  4. 应用场景
    • 日志分层特别适用于大型、多模块应用程序和微服务架构。
    • 有助于多团队协作的项目中的日志管理和维护。
  5. 实用代码示例
    • 文章通过实际代码展示了如何设置和使用分层日志记录器。
    • 包括自定义处理器和日志记录器的配置方法,增强了文章的实用性和可操作性。

总体来说,这篇文章为理解和应用 Python 的 logging 模块提供了深入的指导,特别是在构建需要细粒度日志管理的复杂应用时。文章的结构清晰,通过逐步深入的方式,使得读者朋友容易跟进和理解。

后话

本次分享到此结束,

see you~~🏹🏹

11-21 06:39