Python logging 模块入门:基本使用与三大组件

日志这东西,写小脚本时觉得没必要,真上了生产环境,你就知道它多重要了。我记得刚带团队那会儿,线上一个服务突然挂掉,翻遍控制台只有一堆 print,排查了整整一下午——自那以后,我再也不写裸 print 了。

Python 内置的 logging 模块,说白了就是帮你统一管理日志的工具。你不需要自己搞文件锁、轮转、格式化,它全包了。今天我们就把它最核心的三个东西搞清楚:Logger、Handler、Formatter。

先从最简单的例子开始

我习惯先跑个最小的 demo,感受一下它长什么样:

import logging

logging.basicConfig(level=logging.INFO)
logging.info("服务启动成功")
logging.warning("磁盘空间不足")

输出:

INFO:root:服务启动成功
WARNING:root:磁盘空间不足

嗯,就这么几行,日志就出来了。basicConfig 是快速配置入口,但真正灵活的东西藏在后面。

小提示:默认 logger 名字叫 root,实际项目中建议用 getLogger(__name__) 创建专属 logger,这样日志里能看出是哪个模块打的。

Logger——日志的入口与过滤器

Logger 就是你代码里用来产生日志的对象。每个 logger 都有个 级别(level),低于这个级别的日志就不会被处理。级别从低到高:DEBUG → INFO → WARNING → ERROR → CRITICAL。

我在项目里遇到过一种情况:开发时打开了 DEBUG,上线前忘记关掉,结果日志文件每小时涨几个 G。后来我们强制规定——每个 logger 必须显式设置级别,不能依赖默认值。

import logging

logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)   # 所有级别都收

logger.debug("数据库连接建立")
logger.info("请求处理完成")
logger.error("数据库连接失败")

你想想看,如果这里 setLevel(logging.WARNING),那 DEBUG 和 INFO 就直接被丢弃了,根本不会往下传到 handler。

Handler——日志的真正出口

Logger 只管判断要不要打,真正把日志写到哪,是 Handler 的活。常见的 handler 有:

  • StreamHandler — 输出到控制台(stderr)
  • FileHandler — 输出到文件
  • RotatingFileHandler — 按大小自动轮转文件
  • TimedRotatingFileHandler — 按时间自动轮转

一个 logger 可以挂多个 handler。举个例子,让日志同时打印到屏幕和文件:

import logging

logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

# 控制台 handler
console = logging.StreamHandler()
console.setLevel(logging.INFO)
logger.addHandler(console)

# 文件 handler
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG)
logger.addHandler(file_handler)

logger.debug("这条不会显示在控制台,但会写入文件")
logger.info("两边都能看到")

我曾经踩过的坑:重复添加同一个 handler 会导致日志重复打印。比如在函数里每次调用都 addHandler 一次,结果一条日志输出两遍。解决方案是加一个 if not logger.handlers: 的判断,或者用 logging.getLogger 的缓存特性在模块级别创建。

Formatter——日志的排版师

没格式化的日志长这样:2025-03-20 10:15:30,123 - my_app - INFO - 服务启动。你可以自定义它的样子:

formatter = logging.Formatter(
    fmt="%(asctime)s | %(name)s | %(levelname)-8s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
console.setFormatter(formatter)

常用占位符:

占位符 含义 示例输出
%(asctime)s 时间戳 2025-03-20 10:15:30
%(name)s logger 名称 my_app
%(levelname)s 日志级别 INFO
%(message)s 日志正文 服务启动
%(lineno)d 行号 42

我个人习惯把时间格式调成 %Y-%m-%d %H:%M:%S,去掉毫秒,看起来清爽很多。另外 %(levelname)-8s 表示左对齐占 8 个字符,对齐后日志看上去更整齐。

一张图看清三者的关系

Logger 是源头,Handler 是管道,Formatter 是包装。你可以想象成:Logger 做了个日志消息的“球”,Handler 决定往哪个方向扔,Formatter 给球贴上漂亮的标签。

Logger-Handler-Formatter 工作流 Logger StreamHandler FileHandler RotatingFileHandler Formatter 输出到控制台 / 文件 / 轮转文件

完整的入门范例

把上面零散的内容拼成一个完整例子,你就知道实际项目里怎么写了:

import logging

# 1. 创建 logger
logger = logging.getLogger("payment_service")
logger.setLevel(logging.DEBUG)

# 2. 创建 formatter
formatter = logging.Formatter(
    fmt="%(asctime)s | %(name)s | %(levelname)-8s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

# 3. 创建 handler 并绑定 formatter
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(formatter)

file = logging.FileHandler("payment.log")
file.setLevel(logging.DEBUG)
file.setFormatter(formatter)

# 4. 将 handler 添加到 logger
logger.addHandler(console)
logger.addHandler(file)

# 5. 使用
logger.info("订单创建成功,订单号:12345")
logger.error("支付接口超时,重试第 2 次")

核心要点:Logger 负责“是否记录”,Handler 负责“写到哪里”,Formatter 负责“写成什么样子”。三者协同,缺一不可。

常见对比:StreamHandler vs FileHandler

特性 StreamHandler FileHandler
输出目标 sys.stderr(默认) 磁盘文件
持久化 否,重启即丢失 是,长期保留
适用场景 开发调试、容器 stdout 生产审计、错误追溯
性能影响 低(无 I/O 写盘) 较高(频繁写入需注意)
常见组合 搭配 DEBUG 级别 搭配 WARNING 及以上级别

避坑指南

  • 重复日志 — 每次调用 getLogger 拿到的是同一个对象,但如果你在多个地方重复 addHandler,消息就会翻倍。解决办法:模块顶部一次性配置,或者用 logger.handlers.clear()。
  • 日志乱码 — FileHandler 默认编码跟随系统,Windows 下可能是 gbk。显式指定 FileHandler("app.log", encoding="utf-8"),省心。
  • 忘记 setLevel — logger 和 handler 都有级别,默认是 WARNING。如果你写了 logger.debug(...) 却看不到输出,先检查这两个级别是不是都放开了。

好了,logging 的三大组件你现在心里应该有数了。接下来就可以用它搭建一套适合自己的日志体系了。