原来这才是 Python logging 模块的正确使用姿势!进阶高级开发必看
最近在参与我司的一个新项目的时候,让我负责配置可观测性相关的基础代码,看到其他同事的第一版代码搞了个全项目一起使用的 logger 变量,在所有项目代码内统一引入,进行统一的打印,突然感觉十分难受。
# 同事的第一版代码
logger = logging.getLogger("app")
# 所有模块都引用这个全局 logger
from utils import logger
logger.info("xxx")
这种写法让我产生了强烈的生理不适——日志真的应该是这么打的吗?于是研究了一下 Python logging 的最佳实践,结果发现了新大陆。
logging 模块的核心设计
在说最佳实践之前,先聊聊 logging 模块是怎么设计的。理解了这个,才能理解为什么很多"常见用法"其实是错误的。
logging 模块有五个核心组件:
日志的流转过程是这样的:
关键点:
- Logger 有层级结构:root 是最顶层,
app.user是app的子 logger,app.user.auth又是app.user的子 logger - 默认传播机制:子 logger 的日志默认传播到父 logger(
propagate=True) - Logger 继承父 logger 的 level:如果子 logger 没有设置 level,会从父 logger 继承
配置中的 class 字段
在 YAML 配置中,class 字段指定 Handler 的类,参数会传给构造函数:
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: simple
stream: ext://sys.stdout # 参数传给构造函数
这里的 ext://sys.stdout 是 logging 配置的特殊语法,表示引用标准输出流。
全项目一个 logger 错在哪
先看看常见的"全项目一个 logger"是怎么写的:
# utils.py
import logging
logger = logging.getLogger("app")
# user_service.py
from utils import logger
logger.info("用户登录") # 来自同一个 logger
# order_service.py
from utils import logger
logger.info("创建订单") # 还是同一个 logger
这种写法的问题是:
日志没有区分来源。当你想只看 user_service 的日志时,没办法过滤。你只能看到"所有模块混在一起"的日志。
耦合问题。所有模块都依赖 utils.py,模块间的依赖关系变得混乱。
推荐的写法:配置驱动
两种日志组织方式都是合理的
实际上,logging 的最佳实践有两种常见的组织方式,没有绝对优劣,按需选择即可:
| 方式 | 写法 | 特点 |
|---|---|---|
| 每模块独立 logger | logger = logging.getLogger(__name__) | 无脑方便,利用默认传播机制配置简单 |
| 子系统级别 logger | logger = logging.getLogger("app.user") | 按业务分组控制灵活,但需要额外设计 |
每模块独立 logger 的好处:由于 logger 默认传播和继承父 logger,只要配置顶级 logger(app)就能控制所有子 logger,配置并不复杂。很多项目(比如 Flask、Django)默认就是这么用的。
子系统级别 logger 的好处:按业务领域分组控制很方便,比如可以把 app.user 的日志单独输出到用户相关日志文件。但代码需要额外的设计和维护。
业界现状:常用库里两种用法都有。比如 uvicorn 用的是子系统级别,而很多小型项目直接用每模块独立 logger。没有一定之规,按需选择即可。
配置驱动的写法
# logging.yaml
version: 1
disable_existing_loggers: false
formatters:
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.StreamHandler
level: INFO
formatter: default
file:
class: logging.handlers.RotatingFileHandler
level: DEBUG
formatter: detailed
filename: app.log
maxBytes: 10485760 # 10MB
backupCount: 5
loggers:
app: # 顶级 logger
level: DEBUG
handlers: [console, file]
propagate: false # 配置了 handler 后建议设为 False
app.user:
level: DEBUG
handlers: [file]
propagate: false
root:
level: WARNING
handlers: [console]
加载配置:
import logging.config
import yaml
with open('logging.yaml', 'r') as f:
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.py
import logging
logger = logging.getLogger(__name__)
logger.info("utils loaded")
# user_service.py
import logging
logger = logging.getLogger(__name__) # "user_service"
logger.info("用户登录")
# order_service.py
import logging
logger = logging.getLogger(__name__) # "order_service"
logger.info("创建订单")
配置起来也很简单,只要配置顶级 logger 就行:
root:
level: INFO
handlers: [console, file]
所有子 logger(user_service、order_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. 多进程写入同一个文件
使用 RotatingFileHandler 或 TimedRotatingFileHandler 时,多进程写入可能导致日志损坏。
解决:使用 ConcurrentRotatingFileHandler 或日志收集服务(如 Filebeat)。
4. 性能问题
日志字符串拼接在 DEBUG level 时也会执行,即使这条日志不会被记录:
logger.debug(f"用户数据: {expensive_function()}") # expensive_function 总是会执行
解决:使用懒加载:
logger.debug("用户数据: %s", expensive_function()) # 仅在 DEBUG 开启时执行
总结
logging 模块的设计其实很优雅:层级结构 + 传播机制 + handler 解耦。
全项目一个 logger 的做法,是没有理解这套设计的结果。正确做法是让每个模块用自己的 logger,利用默认的传播机制,配置集中在根 logger。
至于选择"每模块独立 logger"还是"子系统级别 logger",取决于项目规模和个人偏好。前者简单无脑,后者控制灵活。没有绝对的好坏之分。
关键的一点是:把日志配置抽离成配置文件(YAML 或 dictConfig),而不是在代码里硬编码 basicConfig。这样改了配置不用改代码,也方便在不同环境使用不同配置。
参考文档:

17万+

被折叠的 条评论
为什么被折叠?



