Python日志模块:logging模块入门、基本配置、Handler与Formatter
日志这东西,说白了就是程序的「黑匣子」。
我刚做运维那会儿,线上服务一超时,第一反应就是 SSH 上去翻日志。但很多团队的日志写得乱七八糟——有的全打到了标准输出,有的连时间戳都没有,排查问题全靠猜。后来我强制团队统一用 Python 的 logging 模块,才算是把「黑匣子」变成了「仪表盘」。
这一章,我们就把它彻底搞明白。
为什么非得用 logging,而不是 print?
很多新手喜欢用 print() 调试,这没问题。但到了生产环境,print 的短板就暴露了:
- 没有级别:你分不清哪些是 INFO,哪些是 ERROR。
- 没有目的地控制:要么全打屏幕,要么全打文件,不能同时来。
- 没有格式化能力:时间戳、进程号、行号,每次都得自己拼字符串。
- 性能差:
print是同步阻塞的,高并发下会拖慢业务。
而 logging 模块,就是专门解决这些问题的。它内置了日志级别、处理器(Handler)、格式化器(Formatter),而且线程安全。
一句话总结:print 是调试工具,logging 是运维工具。线上环境,请用 logging。
快速入门:三行代码跑起来
先别管什么 Handler、Formatter,我们先让日志「出声」。
import logging
logging.warning("磁盘使用率超过 80%")
logging.error("连接超时,重试第 3 次失败")
运行一下,你会看到类似这样的输出:
WARNING:root:磁盘使用率超过 80%
ERROR:root:连接超时,重试第 3 次失败
默认情况下,logging 只输出 WARNING 及以上级别的日志。DEBUG 和 INFO 是不会打印的。这是很多新手踩的第一个坑——「我明明写了 logging.info(),为什么没输出?」
小技巧:刚接触时,可以先调低日志级别来观察全部输出:logging.basicConfig(level=logging.DEBUG)。
日志级别:从 DEBUG 到 CRITICAL
Python 定义了 5 个标准级别,从低到高:
| 级别 | 数值 | 典型场景 |
|---|---|---|
| DEBUG | 10 | 开发调试,比如 SQL 语句、变量值 |
| INFO | 20 | 正常流程,比如「服务启动成功」「请求到达」 |
| WARNING | 30 | 潜在问题,比如「磁盘剩余空间不足 10%」 |
| ERROR | 40 | 功能异常,比如「数据库连接失败」 |
| CRITICAL | 50 | 严重故障,比如「服务无法启动」 |
我个人习惯是:开发环境开到 DEBUG,生产环境至少开到 WARNING。为什么呢?因为生产环境日志量太大,开 DEBUG 会把磁盘撑爆。我曾经见过一个团队,线上开了 DEBUG 日志,一天就写了 200GB,直接把磁盘打满了。
基本配置:basicConfig 的威力
basicConfig() 是最简单的配置方式。它适合小型脚本和快速原型,但大型项目我建议你跳过它,直接上 Handler。
先看一个常用配置:
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
filename='/var/log/myapp.log',
filemode='a'
)
logging.info("应用启动完成")
这里几个参数,我解释一下:
level:全局日志级别,低于这个级别的日志会被忽略。format:日志格式,后面会细讲。datefmt:时间格式,默认是 ISO 格式,可读性差。filename:输出到文件,不指定则输出到控制台。filemode:写文件模式,'a'是追加,'w'是覆盖。
注意:basicConfig() 只能调用一次。第二次调用是无效的。如果你需要动态调整配置,请使用 Handler 方式。
Handler:日志该去哪儿?
Handler 决定了日志的「目的地」。你可以把日志同时写到文件、控制台、甚至通过 HTTP 发送到远程日志中心。
常用的 Handler 有这些:
| Handler 名称 | 用途 | 适用场景 |
|---|---|---|
| StreamHandler | 输出到控制台(stdout/stderr) | 开发调试、Docker 容器 |
| FileHandler | 输出到文件 | 单机部署、日志持久化 |
| RotatingFileHandler | 按文件大小滚动 | 避免单个日志文件过大 |
| TimedRotatingFileHandler | 按时间滚动(每天/每小时) | 日志归档、定期清理 |
| HTTPHandler | 通过 HTTP POST 发送 | 集中式日志收集 |
来看一个实战例子:同时输出到控制台和文件,并且文件按天滚动。
import logging
from logging.handlers import TimedRotatingFileHandler
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
# 控制台 Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 文件 Handler,每天滚动,保留 7 天
file_handler = TimedRotatingFileHandler(
filename="/var/log/myapp.log",
when="midnight",
interval=1,
backupCount=7
)
file_handler.setLevel(logging.DEBUG)
# 给 logger 添加 Handler
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.info("双通道日志配置完成")
这里有个细节:logger.setLevel() 设置的是全局门槛,而每个 Handler 还可以单独设置级别。比如上面,控制台只输出 INFO 以上,但文件可以记录 DEBUG 级别的所有细节。这在排查问题时非常有用——平时看控制台只关注关键信息,出问题时去翻文件看完整日志。
Formatter:让日志变得可读
Formatter 就是日志的「排版师」。没有格式化的日志,就像没有标点符号的文章——看得人头疼。
常用的格式字段:
| 字段 | 含义 | 示例 |
|---|---|---|
%(asctime)s |
时间戳 | 2025-01-15 14:30:22 |
%(name)s |
Logger 名称 | myapp |
%(levelname)s |
日志级别 | ERROR |
%(message)s |
日志内容 | 连接超时 |
%(filename)s |
文件名 | server.py |
%(lineno)d |
行号 | 42 |
%(process)d |
进程 ID | 12345 |
%(thread)d |
线程 ID | 67890 |
我个人最常用的格式是:
formatter = logging.Formatter(
'%(asctime)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
输出效果:
2025-01-15 14:30:22 | INFO | server.py:42 | 服务启动成功
2025-01-15 14:30:25 | ERROR | client.py:18 | 连接超时,重试第 3 次失败
注意 %(levelname)-8s 这里的 -8,它表示左对齐并占 8 个字符宽度。这样不同级别的日志在视觉上能对齐,扫一眼就能定位到 ERROR。
核心逻辑:一张图看懂
下面这张图,展示了 Logger、Handler、Formatter 三者的协作关系:
避坑指南:我踩过的几个坑
讲完了基础,分享几个真实教训。
坑一:重复日志
我曾经在项目中看到日志每条都打印两遍。排查了半天,发现是多次调用了 basicConfig() 和 addHandler(),导致同一个 Handler 被添加了多次。解决办法:每次添加 Handler 前,先清空旧的:
logger.handlers.clear()
坑二:日志不输出
新手最容易犯的错——用 logging.info() 但没设置级别。默认级别是 WARNING,INFO 当然不输出。要么调低全局级别,要么用 logging.basicConfig(level=logging.INFO)。
坑三:日志文件权限
线上环境,日志文件通常由 root 用户创建,但应用以普通用户运行。结果就是「Permission denied」。我的习惯是:在部署脚本里提前创建好日志目录,并设置正确的属主。
重要提醒:生产环境不要用 basicConfig() 配置日志。它太死板,无法灵活控制多个 Handler。请使用 getLogger() + Handler 的方式,或者直接用 dictConfig() 从配置文件加载。
小结
这一章我们覆盖了 logging 模块的核心:
- 为什么不用 print,而用 logging
- 5 个日志级别及其使用场景
- basicConfig 快速配置
- Handler 控制日志目的地
- Formatter 控制日志格式
- 一张图理清 Logger → Handler → Formatter 的协作关系