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 给球贴上漂亮的标签。
完整的入门范例
把上面零散的内容拼成一个完整例子,你就知道实际项目里怎么写了:
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 的三大组件你现在心里应该有数了。接下来就可以用它搭建一套适合自己的日志体系了。