Python logging 模块怎么用?为什么不建议直接用 print 打日志?
简化版
logging 是 Python 标准日志框架,支持日志级别、Logger 层级、Handler 输出目标、Formatter 格式化和 Filter 过滤。相比 print,它能控制级别、输出到文件或日志系统、带时间和上下文、在生产环境统一管理。
详细版
print 适合临时调试,不适合生产日志。生产日志需要区分 DEBUG、INFO、WARNING、ERROR,需要写入文件、标准输出、日志平台,还要带请求 id、模块名、异常堆栈等上下文。logging 正是为这些需求设计的。
常见写法是每个模块创建自己的 logger:
import logging
logger = logging.getLogger(__name__)
def pay(order_id: str):
logger.info("pay order %s", order_id)
应用入口统一配置 handler、格式和级别;库代码只获取 logger,不随意 basicConfig()。面试回答要强调:logger 负责产生日志,handler 负责输出到哪里,formatter 负责长什么样,level 负责过滤。
完整版教学
一、为什么 print 不适合生产日志
print 只能把文本写到标准输出,缺少级别、来源、时间、堆栈和输出路由。线上系统需要知道“什么时候、哪个模块、哪个请求、发生了什么、严重程度如何”。这些信息靠散落的 print 很难统一。
例如同样一条错误:
error
和结构化一点的日志:
2026-07-31 12:00:01 ERROR payment.service request_id=abc order_id=1001 timeout
后者才能支持检索、告警和排障。日志不是给当下的你看一眼,而是给未来排查问题的人留证据。
二、logging 的核心对象
logging 有四个核心概念:Logger、Handler、Formatter、Filter。Logger 是业务代码拿来调用的对象,Handler 决定输出到控制台、文件或网络,Formatter 决定格式,Filter 决定是否进一步过滤或补上下文。
| 组件 | 作用 | 例子 |
|---|---|---|
| Logger | 产生日志事件 | logging.getLogger(__name__) |
| Handler | 输出目标 | 控制台、文件、Syslog |
| Formatter | 输出格式 | 时间、级别、模块名 |
| Filter | 过滤/补充字段 | request_id、tenant |
一个 logger 可以挂多个 handler。比如 ERROR 日志写文件,INFO 日志写标准输出,关键错误再发到告警系统。
三、日志级别怎么选
日志级别表达重要程度。DEBUG 给开发排查细节,INFO 记录关键业务路径,WARNING 表示异常但可恢复,ERROR 表示当前操作失败,CRITICAL 表示系统级严重问题。
DEBUG < INFO < WARNING < ERROR < CRITICAL
数字化理解:一个每秒 1000 次请求的接口,如果每次都打 5 条 INFO,一天就是 4.32 亿条日志,成本和检索压力都很高。日志级别不是越低越好,要根据排障价值和成本取舍。
四、为什么推荐 getLogger(__name__)
__name__ 通常是模块路径,比如 app.payment.service。用它创建 logger,可以形成层级结构,应用入口能统一控制某个包或模块的日志级别。
logger = logging.getLogger(__name__)
app
app.payment
app.payment.service
如果支付模块太吵,可以单独把 app.payment 调成 WARNING,而不影响其他模块。相比所有地方都用 root logger,这种层级更适合大型项目。
库代码只拿 logger 记录事件,配置权应该交给应用入口。
五、异常日志怎么打才有用
捕获异常时不要只写异常字符串,否则堆栈丢失。logger.exception() 会在 except 块里自动带上当前异常堆栈;或者用 logger.error(..., exc_info=True)。
try:
charge(order)
except TimeoutError:
logger.exception("charge timeout, order_id=%s", order.id)
raise
错误日志还要带业务上下文。只有 timeout 不够,至少要有 order_id、user_id、request_id、外部服务名等。否则排查时只能在海量日志里猜。
六、生产配置要注意什么
Web 服务通常把日志输出到标准输出,由容器或平台采集。传统部署可能写滚动文件。无论哪种方式,都要避免重复 handler、重复打印、日志文件无限增长、敏感信息泄露。
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
复杂项目更适合用 dictConfig 统一配置。密码、token、身份证、手机号等敏感字段要脱敏;日志既是排障工具,也是安全资产。
七、常见误区与追问
- 误区:logging 只是高级版 print。 它是日志事件系统,支持级别、层级、输出目标、格式和过滤。
- 误区:库代码应该自己配置 logging。 库应只创建 logger,最终格式和 handler 交给应用决定。
- 误区:异常日志写
str(e)就够了。 没有堆栈很难定位,应使用logger.exception或exc_info=True。 - 追问:为什么日志会重复打印? 常见原因是父子 logger 传播和多个 handler 重复挂载。
- 追问:生产中日志输出到文件还是 stdout? 容器化环境通常 stdout,由平台采集;传统部署可用滚动文件。
- 追问:日志里能不能打印请求参数? 可以但要脱敏,避免泄露 token、密码和隐私数据。
- 追问:如何给日志加 request_id? 可用 Filter、LoggerAdapter、contextvars 或框架中间件注入上下文。
八、加强记忆
logging 记成“事件、级别、去向、格式、上下文”。业务代码用 getLogger(__name__) 产生日志事件,应用入口统一配置 handler 和 formatter;异常用 logger.exception 保留堆栈;生产日志要考虑成本、重复、脱敏和 request_id。这样回答,比简单说“print 不能控制级别”厚得多。