原来这才是 Python logging 模块的正确使用姿势!进阶高级开发必看
2026/8/8 12:08:57 网站建设 项目流程

原来这才是 Python logging 模块的正确使用姿势!进阶高级开发必看

    • logging 模块的核心设计
      • 配置中的 class 字段
    • 全项目一个 logger 错在哪
    • 推荐的写法:配置驱动
      • 两种日志组织方式都是合理的
      • 配置驱动的写法
      • 为什么 propagate 要设为 False
      • 全项目一个 logger 的正确改法
    • 常见坑
      • 1. 重复 handler
      • 2. level 继承的坑
      • 3. 多进程写入同一个文件
      • 4. 性能问题
    • 总结

最近在参与我司的一个新项目的时候,让我负责配置可观测性相关的基础代码,看到其他同事的第一版代码搞了个全项目一起使用的 logger 变量,在所有项目代码内统一引入,进行统一的打印,突然感觉十分难受。

# 同事的第一版代码logger=logging.getLogger("app")# 所有模块都引用这个全局 loggerfromutilsimportlogger logger.info("xxx")

这种写法让我产生了强烈的生理不适——日志真的应该是这么打的吗?于是研究了一下 Python logging 的最佳实践,结果发现了新大陆。

logging 模块的核心设计

在说最佳实践之前,先聊聊 logging 模块是怎么设计的。理解了这个,才能理解为什么很多"常见用法"其实是错误的。

logging 模块有五个核心组件:

1

1

1

*

1

*

Logger

+name: str

+level: int

+handlers: List[Handler]

+propagate: bool

+debug(msg)

+info(msg)

+warning(msg)

+error(msg)

Handler

+level: int

+formatter: Formatter

+emit(record)

Formatter

+fmt: str

+datefmt: str

+format(record)

Filter

+filter(record)

LogRecord

+name: str

+level: int

+message: str

+created: float

日志的流转过程是这样的:

Handler flow

Logger flow

用户代码中的日志调用,例如:logger.info(...)

logger 是否对本次调用 level 启用?

创建 LogRecord

挂载在 logger 上的 filter 是否拒绝该 record?

将 record 传递给当前 logger 的 handlers

当前 logger 的 propagate 是否为 true?

是否存在 parent logger?

将当前 logger 设为 parent logger

停止

层级结构中是否至少有一个 handler?

使用 lastResort handler

record 被传递给 handler

handler 是否对该 record 的 level 启用?

挂载在 handler 上的 filter 是否拒绝该 record?

emit(包含 formatting)

停止

关键点:

  1. Logger 有层级结构:root 是最顶层,app.userapp的子 logger,app.user.auth又是app.user的子 logger
  2. 默认传播机制:子 logger 的日志默认传播到父 logger(propagate=True
  3. Logger 继承父 logger 的 level:如果子 logger 没有设置 level,会从父 logger 继承

配置中的 class 字段

在 YAML 配置中,class字段指定 Handler 的类,参数会传给构造函数:

handlers:console:class:logging.StreamHandlerlevel:INFOformatter:simplestream:ext://sys.stdout# 参数传给构造函数

这里的ext://sys.stdout是 logging 配置的特殊语法,表示引用标准输出流。

全项目一个 logger 错在哪

先看看常见的"全项目一个 logger"是怎么写的:

# utils.pyimportlogging logger=logging.getLogger("app")
# user_service.pyfromutilsimportlogger logger.info("用户登录")# 来自同一个 logger
# order_service.pyfromutilsimportlogger logger.info("创建订单")# 还是同一个 logger

这种写法的问题是:

日志没有区分来源。当你想只看user_service的日志时,没办法过滤。你只能看到"所有模块混在一起"的日志。

耦合问题。所有模块都依赖utils.py,模块间的依赖关系变得混乱。

推荐的写法:配置驱动

两种日志组织方式都是合理的

实际上,logging 的最佳实践有两种常见的组织方式,没有绝对优劣,按需选择即可:

方式写法特点
每模块独立 loggerlogger = logging.getLogger(__name__)无脑方便,利用默认传播机制配置简单
子系统级别 loggerlogger = logging.getLogger("app.user")按业务分组控制灵活,但需要额外设计

每模块独立 logger的好处:由于 logger 默认传播和继承父 logger,只要配置顶级 logger(app)就能控制所有子 logger,配置并不复杂。很多项目(比如 Flask、Django)默认就是这么用的。

子系统级别 logger的好处:按业务领域分组控制很方便,比如可以把app.user的日志单独输出到用户相关日志文件。但代码需要额外的设计和维护。

业界现状:常用库里两种用法都有。比如 uvicorn 用的是子系统级别,而很多小型项目直接用每模块独立 logger。没有一定之规,按需选择即可。

配置驱动的写法

# logging.yamlversion:1disable_existing_loggers:falseformatters:default:format:'%(asctime)s - %(name)s - %(levelname)s - %(message)s'detailed:format:'%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'handlers:console:class:logging.StreamHandlerlevel:INFOformatter:defaultfile:class:logging.handlers.RotatingFileHandlerlevel:DEBUGformatter:detailedfilename:app.logmaxBytes:10485760# 10MBbackupCount:5loggers:app:# 顶级 loggerlevel:DEBUGhandlers:[console,file]propagate:false# 配置了 handler 后建议设为 Falseapp.user:level:DEBUGhandlers:[file]propagate:falseroot:level:WARNINGhandlers:[console]

加载配置:

importlogging.configimportyamlwithopen('logging.yaml','r')asf:config=yaml.safe_load(f)logging.config.dictConfig(config)

为什么 propagate 要设为 False

当一个 logger 配置了 handler 后,建议把propagate设为False。否则会出现重复日志:

# 如果 propagate=True(默认)app.info("msg")# 会打印两次# 第一次:app 的 handler 打印# 第二次:root 的 handler 打印(因为传播到 root 了)

全项目一个 logger 的正确改法

回到开头的问题,正确的做法是每个模块用自己的 logger:

# utils.pyimportlogging logger=logging.getLogger(__name__)logger.info("utils loaded")
# user_service.pyimportlogging logger=logging.getLogger(__name__)# "user_service"logger.info("用户登录")
# order_service.pyimportlogging logger=logging.getLogger(__name__)# "order_service"logger.info("创建订单")

配置起来也很简单,只要配置顶级 logger 就行:

root:level:INFOhandlers:[console,file]

所有子 logger(user_serviceorder_service)的日志都会自动传播到 root logger 被处理。

常见坑

1. 重复 handler

如果父 logger 和子 logger 都配置了 handler,且propagate=True,日志会打印多次。

解决:要么子 logger 的propagate=False,要么子 logger 不配 handler。

2. level 继承的坑

子 logger 没有设置 level 时,会从父 logger 继承。如果父 logger 设置了level=DEBUG,子 logger 默认也是 DEBUG。

解决:明确设置每个 logger 的 level。

3. 多进程写入同一个文件

使用RotatingFileHandlerTimedRotatingFileHandler时,多进程写入可能导致日志损坏。

解决:使用ConcurrentRotatingFileHandler或日志收集服务(如 Filebeat)。

4. 性能问题

日志字符串拼接在DEBUGlevel 时也会执行,即使这条日志不会被记录:

logger.debug(f"用户数据:{long_string}")# 字符串拼接总是会执行

解决:使用懒加载:

logger.debug("用户数据: %s",long_string)# 仅在 DEBUG 开启时执行拼接

总结

logging 模块的设计其实很优雅:层级结构 + 传播机制 + handler 解耦

全项目一个 logger 的做法,是没有理解这套设计的结果。正确做法是让每个模块用自己的 logger,利用默认的传播机制,配置集中在根 logger。

至于选择"每模块独立 logger"还是"子系统级别 logger",取决于项目规模和个人偏好。前者简单无脑,后者控制灵活。没有绝对的好坏之分。

关键的一点是:把日志配置抽离成配置文件(YAML 或 dictConfig),而不是在代码里硬编码basicConfig。这样改了配置不用改代码,也方便在不同环境使用不同配置。


参考文档

  • Python Logging HOWTO
  • Python logging 模块文档

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询