日志框架入门: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 级别设为 WARNING
  • app.service 的 logger 没设级别,默认继承 app 的 WARNING
  • app.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 的核心流程和组件关系画清楚了:

Python Logging 核心架构 Logger 级别过滤 (DEBUG/INFO/...) Handler 列表 StreamHandler 终端输出 RotatingFileHandler 文件轮转 SocketHandler 网络发送 Formatter 时间 / 级别 / 模块 / 消息 终端 日志文件 远程服务器

避坑指南

最后分享几个我踩过的坑:

  • 重复日志:如果你多次调用 basicConfig 或重复 addHandler,日志会重复输出。我一般只在程序入口配置一次。
  • 日志级别混淆:Logger 和 Handler 都有 setLevel。Logger 的级别是「总闸」,Handler 的级别是「分闸」。只有两者都通过,日志才会输出。
  • 忘记关闭文件:虽然 Python 会在程序退出时自动关闭,但长时间运行的服务,建议在合适时机调用 handler.close()。
  • 日志内容泄露敏感信息:别把密码、token 直接打出来。我曾经见过同事把数据库密码打在 ERROR 日志里……嗯,后来被安全团队约谈了。