日志框架入门:Python logging 模块基础、配置 Logger、Handler 与 Formatter
说实话,很多开发者在项目初期对日志的态度就是「print 大法好」。我早年也这么干过,直到有一次线上排查问题,看着满屏的 print 输出,连哪个模块打的都分不清……嗯,从那以后我就老老实实用 logging 了。
Python 自带的 logging 模块,其实已经够强大了。你不需要装第三方库,就能搭出一套完整的日志体系。今天我们就把它掰开揉碎,看看 Logger、Handler、Formatter 到底怎么配合。
为什么不用 print?
print 的问题很明显:
- 没法控制输出级别——线上不能关掉调试信息
- 没有时间戳、模块名等上下文
- 输出目标单一——要么终端,要么文件,不能同时
- 多线程环境下容易乱
说白了,print 适合临时调试,logging 才是生产环境该用的东西。
logging 模块的三驾马车
我习惯把 logging 的核心组件分成三个角色:
| 组件 | 职责 | 类比 |
|---|---|---|
| Logger | 日志入口,决定是否记录、记录什么级别 | 公司的前台接待 |
| Handler | 日志输出目的地(文件、终端、网络等) | 快递员,负责把日志送到指定地点 |
| Formatter | 日志内容的排版格式 | 快递单的填写格式 |
它们的关系很简单:Logger 收到日志消息,交给一个或多个 Handler,Handler 再按 Formatter 的格式输出。
核心原则:Logger 负责「要不要记」,Handler 负责「记到哪」,Formatter 负责「记成什么样」。各司其职,别混在一起。
快速上手:最简单的配置
先来个最基础的例子,感受一下:
import logging
logging.basicConfig(level=logging.INFO)
logging.debug("这条不会显示")
logging.info("系统启动成功")
logging.warning("磁盘空间不足")
输出:
INFO:root:系统启动成功
WARNING:root:磁盘空间不足
你看,debug 级别的消息被过滤掉了。这就是级别控制的好处——生产环境设成 INFO,调试时改成 DEBUG,不用改代码。
小技巧:我一般会在开发环境用 level=logging.DEBUG,线上用 level=logging.WARNING。这样既能保证开发时看到足够细节,又不会让线上日志刷爆磁盘。
Logger 的命名与层级
很多人直接 logging.getLogger() 拿到 root logger 就用。我个人建议:每个模块创建自己的 logger。
# 在模块顶部
logger = logging.getLogger(__name__)
为什么?因为 __name__ 会带上包路径。比如你的模块是 app.service.user,那 logger 名就是 app.service.user。这样在日志里一眼就能看出是哪个模块打的。
Logger 的命名还支持层级继承。举个例子:
app的 logger 级别设为WARNINGapp.service的 logger 没设级别,默认继承app的WARNINGapp.service.user可以单独设为DEBUG,覆盖父级
这种层级设计,让你能精细控制每个模块的日志级别。我在微服务项目里经常用这个特性——核心模块开 DEBUG,其他模块保持 INFO。
Handler:日志该去哪?
Handler 决定了日志的最终归宿。常用的有这些:
| Handler 类型 | 输出目标 | 适用场景 |
|---|---|---|
StreamHandler |
终端(stdout/stderr) | 开发调试、容器日志 |
FileHandler |
单个文件 | 简单应用、小规模日志 |
RotatingFileHandler |
按大小轮转的文件 | 生产环境,防止日志撑爆磁盘 |
TimedRotatingFileHandler |
按时间轮转的文件 | 需要按天/小时归档的场景 |
SocketHandler |
网络 socket | 集中式日志收集 |
我遇到过最典型的坑:线上用 FileHandler 没配轮转,结果日志文件涨到几十 GB,服务器磁盘直接打满。后来我养成了习惯——生产环境永远用 RotatingFileHandler。
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("my_app")
handler = RotatingFileHandler(
"app.log",
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
logger.addHandler(handler)
这样每个日志文件最大 10MB,保留最近 5 个文件,旧的自动删除。
Formatter:日志长什么样?
Formatter 就是日志的「模板」。你可以自定义时间格式、添加模块名、行号等信息。
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
常用的格式化字段:
| 字段 | 含义 | 示例 |
|---|---|---|
%(asctime)s |
时间戳 | 2025-01-15 14:30:22 |
%(name)s |
Logger 名称 | app.service.user |
%(levelname)s |
日志级别 | INFO / ERROR |
%(message)s |
日志内容 | 用户登录成功 |
%(filename)s |
文件名 | user_service.py |
%(lineno)d |
行号 | 42 |
注意:时间格式默认是 ISO 格式,如果你想要更可读的格式,可以传 datefmt 参数:Formatter(fmt, datefmt="%Y-%m-%d %H:%M:%S")。我曾经因为没配时间格式,日志里全是毫秒级的时间戳,排查问题时看得眼睛疼。
完整配置示例
把上面这些串起来,一个生产可用的配置大概长这样:
import logging
from logging.handlers import RotatingFileHandler
# 创建 logger
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)
# 文件 handler(记录所有级别)
file_handler = RotatingFileHandler(
"app.log",
maxBytes=10*1024*1024,
backupCount=5
)
file_handler.setLevel(logging.DEBUG)
# 终端 handler(只记录 WARNING 及以上)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
# 统一的格式
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 注册 handler
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# 使用
logger.info("服务启动")
logger.warning("连接池即将耗尽")
logger.error("数据库连接失败")
这样配置后,终端只显示警告和错误,文件里则记录了所有调试信息。排查问题时翻文件,日常监控看终端,两不耽误。
知识体系总览
下面这张图,把 logging 的核心流程和组件关系画清楚了:
避坑指南
最后分享几个我踩过的坑:
- 重复日志:如果你多次调用
basicConfig或重复addHandler,日志会重复输出。我一般只在程序入口配置一次。 - 日志级别混淆:Logger 和 Handler 都有
setLevel。Logger 的级别是「总闸」,Handler 的级别是「分闸」。只有两者都通过,日志才会输出。 - 忘记关闭文件:虽然 Python 会在程序退出时自动关闭,但长时间运行的服务,建议在合适时机调用
handler.close()。 - 日志内容泄露敏感信息:别把密码、token 直接打出来。我曾经见过同事把数据库密码打在 ERROR 日志里……嗯,后来被安全团队约谈了。