3、Python日志模块:logging模块基础、Logger、Handler、Formatter、Filter详解

日志这东西,说白了就是给程序装个黑匣子。

我刚入行那会儿,觉得日志就是print一下完事。直到有一次线上服务半夜挂了,我爬起来看日志,发现全是print输出的乱糟糟信息,根本分不清哪个是警告、哪个是错误。嗯,从那以后,我再也不敢小看logging模块了。

3.1 logging模块基础:别再用print了

Python自带的logging模块,其实已经够强大了。你不需要装任何第三方库,直接import就能用。

import logging

logging.debug("这是调试信息")
logging.info("这是普通信息")
logging.warning("这是警告信息")
logging.error("这是错误信息")
logging.critical("这是严重错误")

跑一下你会发现,默认只输出了warning及以上级别的日志。为什么?因为logging默认的日志级别是WARNING。我个人习惯在开发阶段直接设成DEBUG,省得漏掉关键线索。

小技巧: 设置日志级别用 logging.basicConfig(level=logging.DEBUG),这样所有级别的日志都能看到。

3.2 Logger:日志的入口,你的专属记录员

Logger是logging模块的核心对象。你可以把它理解成一个「日志记录员」。每个模块或组件,都应该有自己的Logger实例。

import logging

# 创建一个Logger
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

logger.debug("这条日志来自my_app")

我在项目中遇到过一个问题:多个模块共用一个Logger,结果日志全混在一起,排查起来特别痛苦。后来我养成了一个习惯——每个Python文件都单独创建一个Logger,名字就用 __name__

logger = logging.getLogger(__name__)

这样日志里会自动带上模块名,一眼就能看出是哪个文件输出的。

注意: 不要直接使用 logging.root 这个根Logger。它太「粗」了,不适合精细控制。每个模块用自己的Logger,才是正道。

3.3 Handler:日志该去哪儿?

Logger负责产生日志,但日志最终要输出到哪里?这就轮到Handler登场了。

Handler决定了日志的「目的地」。常见的Handler有:

  • StreamHandler:输出到控制台(标准输出)
  • FileHandler:输出到文件
  • RotatingFileHandler:按文件大小滚动
  • TimedRotatingFileHandler:按时间滚动
  • SMTPHandler:通过邮件发送

我举个例子,你一看就明白:

import logging

logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

# 控制台Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# 文件Handler
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG)

# 把Handler加到Logger上
logger.addHandler(console_handler)
logger.addHandler(file_handler)

logger.debug("这条只进文件")
logger.info("控制台和文件都能看到")

你看,一个Logger可以挂多个Handler。每个Handler还能设置不同的级别。这样就能实现:控制台只显示INFO以上,但文件里记录所有DEBUG信息。

核心思想: Logger负责「产生什么」,Handler负责「送到哪里」。两者解耦,灵活组合。

3.4 Formatter:让日志长得好看点

日志光有内容还不够,格式也很重要。Formatter就是干这个的。

默认的日志格式是:级别:Logger名称:消息内容。说实话,太简陋了。我一般会自定义格式:

import logging

formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)

logger = logging.getLogger("my_app")
logger.addHandler(console_handler)
logger.setLevel(logging.DEBUG)

logger.info("这条日志有完整的时间戳和模块名")

常用的格式化字段:

字段 含义 示例
%(asctime)s 时间戳 2025-01-15 14:30:22,123
%(name)s Logger名称 my_app
%(levelname)s 日志级别 INFO
%(message)s 日志内容 用户登录成功
%(filename)s 文件名 app.py
%(lineno)d 行号 42

我曾经踩过一个坑:线上日志没有时间戳,出问题后根本不知道哪个日志是哪个时间点的。后来我强制团队所有项目必须加上 %(asctime)s,再也没出过这种问题。

3.5 Filter:日志的「安检员」

Filter的作用是过滤日志。你可以根据内容、级别、来源等条件,决定哪些日志放行,哪些拦截。

举个例子,我只想记录某个特定模块的日志:

import logging

class ModuleFilter(logging.Filter):
    def filter(self, record):
        # 只放行名为 "my_app" 的Logger
        return record.name == "my_app"

logger = logging.getLogger("my_app")
logger.addFilter(ModuleFilter())

logger.info("这条日志会被放行")

other_logger = logging.getLogger("other_module")
other_logger.info("这条日志会被过滤掉")

Filter在实际项目中用得不多,但一旦用上,往往能解决大问题。我记得有一次,某个第三方库疯狂输出DEBUG日志,把磁盘都写满了。我直接用Filter把它屏蔽掉,世界清静了。

避坑指南: Filter是在日志输出到Handler之前执行的。如果Filter返回False,这条日志根本不会进入Handler。所以Filter的效率很重要,别在里面做耗时操作。

3.6 一张图看懂logging核心架构

说了这么多,我画张图帮你理清关系:

Logger 产生日志消息 Filter 过滤/放行日志 Handler 输出目的地 Formatter 控制台 (StreamHandler) 文件 (FileHandler) 邮件 (SMTPHandler) 日志处理流程:Logger → Filter → Handler → Formatter → 输出

这张图展示了日志从产生到输出的完整链路。Logger产生日志,Filter决定要不要放行,Handler决定送到哪里,Formatter决定长什么样。每一步都可以独立配置,这就是logging模块灵活的地方。

3.7 实战:搭建一个生产级日志系统

说了这么多理论,来点实际的。这是我个人在项目中常用的日志配置模板:

import logging
import logging.handlers
import os

def setup_logger(name, log_dir="logs"):
    """配置一个生产级Logger"""
    # 创建日志目录
    os.makedirs(log_dir, exist_ok=True)
    
    # 创建Logger
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)
    
    # 格式化器
    formatter = logging.Formatter(
        '%(asctime)s | %(name)s | %(levelname)s | %(filename)s:%(lineno)d | %(message)s'
    )
    
    # 控制台Handler - 只输出INFO及以上
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)
    
    # 文件Handler - 按天滚动,保留30天
    file_handler = logging.handlers.TimedRotatingFileHandler(
        filename=os.path.join(log_dir, f"{name}.log"),
        when="midnight",
        interval=1,
        backupCount=30,
        encoding="utf-8"
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)
    
    # 错误文件Handler - 只记录ERROR及以上
    error_handler = logging.handlers.RotatingFileHandler(
        filename=os.path.join(log_dir, f"{name}_error.log"),
        maxBytes=10*1024*1024,  # 10MB
        backupCount=5,
        encoding="utf-8"
    )
    error_handler.setLevel(logging.ERROR)
    error_handler.setFormatter(formatter)
    
    # 添加Handler
    logger.addHandler(console_handler)
    logger.addHandler(file_handler)
    logger.addHandler(error_handler)
    
    return logger

# 使用
logger = setup_logger("my_service")
logger.info("服务启动成功")
logger.error("数据库连接失败", exc_info=True)

这个模板我用了好几年,基本没出过问题。它做到了三点:

  • 控制台只显示INFO以上,开发时不会刷屏
  • 文件按天滚动,不会撑爆磁盘
  • 错误日志单独存一份,方便快速定位问题
记住: 日志不是越多越好,而是越「有用」越好。该记的记全,不该记的别浪费磁盘。

好了,logging模块的核心内容就这些。你想想看,从Logger到Handler,再到Formatter和Filter,其实就是一个流水线。每个环节各司其职,组合起来就能搭建出适合自己项目的日志系统。

公众号:蓝海资料掘金营,微信deep3321