文章目录

Python logging模块的Logger层级传播与Handler配置

发布于 2026-04-27 23:27:22 · 浏览 5 次 · 评论 0 条

Python logging模块的Logger层级传播与Handler配置

Python标准库中的 logging 模块是一个功能强大的日志系统,但很多开发者在使用时常遇到日志重复打印、配置混乱等问题。掌握Logger的层级结构与传播机制,以及Handler的正确配置方式,是构建清晰日志系统的关键。


理解Logger层级架构

Logger对象采用树形层级结构,命名规则类似于Python包的路径,使用点号(.)分隔。

  1. 认识层级关系
    当使用 logging.getLogger(name) 获取Logger时,名称的点号分割决定了父子关系。例如,getLogger('a.b') 获取的Logger是 getLogger('a') 的子Logger。

    • Root Logger:这是所有Logger的祖先,默认的Logger。如果不指定名称或名称为空,获取的就是Root Logger。
    • 父/子 Logger:子Logger会自动继承父Logger的配置(如Level、Handler等),除非子Logger单独进行了设置。
  2. 获取Logger对象
    在代码中,通常以当前模块名作为Logger名称,利用层级结构管理日志。

    import logging
    
    # 获取名为 'my_app' 的Logger (通常作为父Logger)
    parent_logger = logging.getLogger('my_app')
    
    # 获取名为 'my_app.utils' 的Logger (子Logger)
    child_logger = logging.getLogger('my_app.utils')
  3. 设置日志级别
    Logger有一个“有效等级”的概念。如果子Logger没有显式设置Level,它将使用父Logger的Level;如果父Logger也没设置,则向上追溯直到Root Logger(Root Logger默认级别为 WARNING)。

    • 设置:使用 logger.setLevel(logging.DEBUG) 方法指定最低日志级别。

掌握日志传播机制

默认情况下,子Logger处理完日志记录后,会将该记录传递给父Logger,父Logger再传递给它的父Logger,直到Root Logger。这种机制称为传播。

  1. 理解传播流程
    当调用 child_logger.info('message') 时:

    • 日志记录首先被 child_logger 处理(如果它有Handler,则输出)。
    • 随后,日志记录传递给 parent_logger(如果它有Handler,再次输出)。
    • 最后传递给 root logger(如果有Handler,又一次输出)。
  2. 解决重复日志问题
    如果父子Logger都配置了输出到控制台的Handler,同一条日志会被打印多次。这是最常见的困扰。

    • 关闭传播:如果不希望子Logger的日志向上传递,设置 propagate 属性为 False
    child_logger.propagate = False
  3. 查看传播逻辑图
    下图展示了日志从子Logger生成后,经过Handler输出并向上层传播的过程。

    graph TD A["生成日志记录"] --> B["子Logger"] B --> C["子Handler 1"] C --> D["输出 (文件/控制台)"] B -- propagate=True --> E["父Logger"] E --> F["父Handler 2"] F --> G["输出 (控制台)"] E -- propagate=True --> H["Root Logger"] H --> I["默认Handler"] I --> J["输出"]

精准配置Handler

Handler负责将日志记录发送到指定的目的地(如控制台、文件、网络)。一个Logger可以关联多个Handler。

  1. 选择Handler类型
    根据输出需求选择合适的Handler类:

    • StreamHandler:输出到流(如 sys.stdoutsys.stderr),常用于控制台输出。
    • FileHandler:输出到指定文件。
    • RotatingFileHandler:输出到文件,并支持当文件达到指定大小时自动回滚(备份)。
    • TimeRotatingFileHandler:按时间间隔自动切分日志文件。
  2. 配置Handler步骤
    配置Handler通常包含三个核心动作:实例化、设置Formatter、设置Level。

    • 创建 Handler对象。
    • 设置 日志格式。
    • 设置 处理级别。
    • 添加 到Logger。
    import logging
    
    logger = logging.getLogger('my_app')
    logger.setLevel(logging.DEBUG) # Logger总体级别设为DEBUG
    
    # 创建一个StreamHandler
    console_handler = logging.StreamHandler()
    # 设置Handler级别为INFO,只处理INFO及以上级别的日志
    console_handler.setLevel(logging.INFO)
    
    # 设置Formatter
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    console_handler.setFormatter(formatter)
    
    # 将Handler添加到Logger
    logger.addHandler(console_handler)
  3. 理解Logger与Handler的级别过滤
    日志最终是否输出,取决于两道关卡:Logger级别和Handler级别。

    • 第一关:Logger级别。如果日志级别低于Logger的设置级别,直接丢弃,不会传递给任何Handler。
    • 第二关:Handler级别。日志通过Logger过滤后,传递给Handler。如果日志级别低于该Handler的设置级别,该Handler不输出它。

    注意:只有当消息级别 大于或等于 Logger和Handler两者的设定级别时,日志才会被最终输出。


使用Formatter定义输出格式

Formatter对象决定了日志记录的最终外观。

  1. 常用格式属性
    在格式字符串中,可以使用 %() 风格的占位符。
属性名称 格式 说明
asctime %(asctime)s 日志发生的时间,默认格式为 2003-07-08 16:49:45,896
name %(name)s Logger的名称
levelname %(levelname)s 文本形式的日志级别(INFO, DEBUG等)
message %(message)s 用户输出的日志消息
filename %(filename)s 包含路径的文件名
lineno %(lineno)d 调用日志输出函数的源代码行号
  1. 自定义时间格式
    使用 datefmt 参数可以指定时间的输出格式,规则同 time.strftime()

    formatter = logging.Formatter(
        fmt='%(asctime)s [%(levelname)s] %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

综合实战案例

以下是一个完整的配置示例:创建一个父Logger配置文件输出(记录所有DEBUG级别日志),和一个子Logger配置控制台输出(仅显示INFO及以上日志),并演示如何避免重复输出。

  1. 编写 配置代码。

    import logging
    import logging.handlers
    
    # 1. 配置父Logger 'app'
    parent_logger = logging.getLogger('app')
    parent_logger.setLevel(logging.DEBUG) # 父Logger捕获所有级别的日志
    
    # 2. 配置父Handler (FileHandler,记录DEBUG及以上)
    file_handler = logging.FileHandler('app.log', mode='a', encoding='utf-8')
    file_handler.setLevel(logging.DEBUG)
    file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(funcName)s - %(message)s')
    file_handler.setFormatter(file_formatter)
    parent_logger.addHandler(file_handler)
    
    # 3. 配置子Logger 'app.module'
    child_logger = logging.getLogger('app.module')
    # child_logger未设置Level,继承父Logger的DEBUG级别
    # child_logger未设置Handler,默认会将日志传递给父Logger的Handler
    
    # 4. 给子Logger添加独立的控制台Handler (StreamHandler,仅显示INFO及以上)
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_formatter = logging.Formatter('%(levelname)s: %(message)s')
    console_handler.setFormatter(console_formatter)
    child_logger.addHandler(console_handler)
    
    # 5. 关闭子Logger的传播
    # 如果不设置这行,DEBUG日志会由child捕获,传给parent写入文件;
    # 同时INFO日志会被child捕获(显示控制台)并传给parent(写入文件)。
    # 若只需child单独处理控制台,不希望父级重复处理,可取消下面注释:
    # child_logger.propagate = False
    
    # 测试日志输出
    child_logger.debug('这是一条调试信息') # 只会写入文件
    child_logger.info('这是一条普通信息')   # 写入文件,且控制台输出
    child_logger.warning('这是一条警告信息') # 写入文件,且控制台输出
  2. 运行 脚本。
    执行上述代码后,观察控制台输出与生成的 app.log 文件内容。

    • 控制台仅显示 INFOWARNING
    • 文件中包含 DEBUGINFOWARNING
  3. 调整 参数。
    试着修改 child_logger.propagate = False,再次运行,观察文件中是否还包含INFO和WARNING日志(这将不再包含,因为传播被切断)。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文