Python logging模块的Logger层级传播与Handler配置
Python标准库中的 logging 模块是一个功能强大的日志系统,但很多开发者在使用时常遇到日志重复打印、配置混乱等问题。掌握Logger的层级结构与传播机制,以及Handler的正确配置方式,是构建清晰日志系统的关键。
理解Logger层级架构
Logger对象采用树形层级结构,命名规则类似于Python包的路径,使用点号(.)分隔。
-
认识层级关系
当使用logging.getLogger(name)获取Logger时,名称的点号分割决定了父子关系。例如,getLogger('a.b')获取的Logger是getLogger('a')的子Logger。- Root Logger:这是所有Logger的祖先,默认的Logger。如果不指定名称或名称为空,获取的就是Root Logger。
- 父/子 Logger:子Logger会自动继承父Logger的配置(如Level、Handler等),除非子Logger单独进行了设置。
-
获取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') -
设置日志级别
Logger有一个“有效等级”的概念。如果子Logger没有显式设置Level,它将使用父Logger的Level;如果父Logger也没设置,则向上追溯直到Root Logger(Root Logger默认级别为WARNING)。- 设置:使用
logger.setLevel(logging.DEBUG)方法指定最低日志级别。
- 设置:使用
掌握日志传播机制
默认情况下,子Logger处理完日志记录后,会将该记录传递给父Logger,父Logger再传递给它的父Logger,直到Root Logger。这种机制称为传播。
-
理解传播流程
当调用child_logger.info('message')时:- 日志记录首先被
child_logger处理(如果它有Handler,则输出)。 - 随后,日志记录传递给
parent_logger(如果它有Handler,再次输出)。 - 最后传递给
root logger(如果有Handler,又一次输出)。
- 日志记录首先被
-
解决重复日志问题
如果父子Logger都配置了输出到控制台的Handler,同一条日志会被打印多次。这是最常见的困扰。- 关闭传播:如果不希望子Logger的日志向上传递,设置
propagate属性为False。
child_logger.propagate = False - 关闭传播:如果不希望子Logger的日志向上传递,设置
-
查看传播逻辑图
下图展示了日志从子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。
-
选择Handler类型
根据输出需求选择合适的Handler类:StreamHandler:输出到流(如sys.stdout或sys.stderr),常用于控制台输出。FileHandler:输出到指定文件。RotatingFileHandler:输出到文件,并支持当文件达到指定大小时自动回滚(备份)。TimeRotatingFileHandler:按时间间隔自动切分日志文件。
-
配置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) -
理解Logger与Handler的级别过滤
日志最终是否输出,取决于两道关卡:Logger级别和Handler级别。- 第一关:Logger级别。如果日志级别低于Logger的设置级别,直接丢弃,不会传递给任何Handler。
- 第二关:Handler级别。日志通过Logger过滤后,传递给Handler。如果日志级别低于该Handler的设置级别,该Handler不输出它。
注意:只有当消息级别 大于或等于 Logger和Handler两者的设定级别时,日志才会被最终输出。
使用Formatter定义输出格式
Formatter对象决定了日志记录的最终外观。
- 常用格式属性
在格式字符串中,可以使用%()风格的占位符。
| 属性名称 | 格式 | 说明 |
|---|---|---|
| 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 | 调用日志输出函数的源代码行号 |
-
自定义时间格式
使用datefmt参数可以指定时间的输出格式,规则同time.strftime()。formatter = logging.Formatter( fmt='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' )
综合实战案例
以下是一个完整的配置示例:创建一个父Logger配置文件输出(记录所有DEBUG级别日志),和一个子Logger配置控制台输出(仅显示INFO及以上日志),并演示如何避免重复输出。
-
编写 配置代码。
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('这是一条警告信息') # 写入文件,且控制台输出 -
运行 脚本。
执行上述代码后,观察控制台输出与生成的app.log文件内容。- 控制台仅显示
INFO和WARNING。 - 文件中包含
DEBUG、INFO和WARNING。
- 控制台仅显示
-
调整 参数。
试着修改child_logger.propagate = False,再次运行,观察文件中是否还包含INFO和WARNING日志(这将不再包含,因为传播被切断)。

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