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 三者的协作关系:

Python logging 核心架构 Logger 级别过滤 (level) StreamHandler FileHandler RotatingFileHandler Formatter (格式) 控制台 日志文件 滚动归档 Logger 负责接收日志 → 级别过滤 → 分发给多个 Handler → 每个 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 的协作关系