第4章:logging模块深入:Logger、Handler、Formatter、Filter四大组件详解

说实话,很多开发者用了好几年logging,还停留在logging.info()这种初级阶段。一旦系统复杂起来,日志要么满天飞,要么该记的没记到。我当年接手过一个老项目,日志文件一天能写20GB,但出问题时愣是找不到关键线索。后来我才明白——没搞懂logging的四大组件,迟早要踩坑。

Python的logging模块,说白了就是一套生产日志的流水线。它由四个核心角色组成:Logger(记录器)、Handler(处理器)、Formatter(格式化器)、Filter(过滤器)。咱们一个一个拆开看。

4.1 Logger:日志的入口与门卫

Logger是你写代码时直接调用的那个对象。比如logger.info("用户登录成功")。但它的职责远不止记录消息——它还负责判断这条日志该不该记。

每个Logger都有一个日志级别。默认是WARNING,也就是说,低于WARNING的日志(DEBUG、INFO)会被直接丢弃。嗯,这里要注意:很多人以为调了logger.info()就一定会输出,其实不一定。

关键点:Logger的级别是“准入门槛”。只有达到这个级别的日志,才会往下传给Handler。

Logger之间还有层级关系,用点号分隔。比如app.module是app的子Logger。子Logger默认会把日志传播给父Logger。我在项目中遇到过一个问题:明明只配了根Logger的Handler,子模块的日志却重复输出了。原因就是传播机制在作怪。

import logging

# 创建Logger
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)

# 子Logger
child_logger = logging.getLogger('myapp.module')
child_logger.setLevel(logging.INFO)

# 默认情况下,child_logger的日志会传播给'myapp'
# 如果不想传播,可以关闭
child_logger.propagate = False

个人习惯:我一般只在根Logger上配Handler,子Logger只设级别。这样结构清晰,不容易乱。

4.2 Handler:日志的出口与目的地

Handler决定了日志最终去哪。是写文件?打印到控制台?还是通过网络发出去?每种场景对应不同的Handler。

Python内置了十几种Handler,常用的有:

Handler名称 用途 适用场景
StreamHandler 输出到控制台 开发调试、Docker容器
FileHandler 写入单个文件 简单应用、小规模日志
RotatingFileHandler 按大小滚动文件 生产环境,防止磁盘写满
TimedRotatingFileHandler 按时间滚动文件 需要按天/小时归档的场景
SMTPHandler 邮件发送 错误告警通知

一个Logger可以挂多个Handler。比如开发时同时输出到控制台和文件,各司其职。每个Handler也可以有自己的级别,这给了你更细的控制粒度。

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)

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

# 文件Handler,按大小滚动,保留5个备份
file_handler = RotatingFileHandler(
    'app.log', maxBytes=10*1024*1024, backupCount=5
)
file_handler.setLevel(logging.DEBUG)

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

我曾经踩过的坑:给同一个Logger重复添加了相同的Handler,结果日志一条变两条。排查了半天才发现是初始化代码被调了两次。建议在添加Handler前先检查是否已存在。

4.3 Formatter:日志的排版师

Formatter负责把日志记录(LogRecord)转换成字符串。说白了就是决定日志长什么样。默认格式很简单,但生产环境通常需要自定义。

Formatter使用%风格的格式化字符串,常用的占位符有:

  • %(asctime)s — 时间戳
  • %(name)s — Logger名称
  • %(levelname)s — 日志级别
  • %(message)s — 日志内容
  • %(filename)s — 文件名
  • %(lineno)d — 行号
  • %(funcName)s — 函数名
formatter = logging.Formatter(
    '%(asctime)s | %(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# 应用到Handler
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

你想想看,没有Formatter的日志长什么样?WARNING:root:something happened。有了Formatter,你能一眼看出时间、位置、级别。排查问题时,这行信息可能就是救命稻草。

我的建议:开发环境用彩色输出(可以配合colorlog库),生产环境用JSON格式。JSON格式方便日志收集系统(如ELK)解析。我最近的项目就在用python-json-logger,效果不错。

4.4 Filter:日志的精准筛子

Filter是四大组件里最容易被忽略的。它的作用是在日志到达Handler之前,决定要不要放行。级别只能做粗粒度过滤,Filter可以做细粒度控制。

比如,你只想记录某个特定用户的请求日志,或者只记录错误码大于500的异常。这时候Filter就派上用场了。

import logging

class SensitiveFilter(logging.Filter):
    """过滤掉包含敏感信息的日志"""
    def filter(self, record):
        # 如果日志消息包含'password',就丢弃
        return 'password' not in record.getMessage()

class ErrorThresholdFilter(logging.Filter):
    """只记录错误码大于500的日志"""
    def __init__(self, threshold=500):
        super().__init__()
        self.threshold = threshold
    
    def filter(self, record):
        # 假设我们在日志中传入了error_code参数
        error_code = getattr(record, 'error_code', 0)
        return error_code > self.threshold

# 使用Filter
logger = logging.getLogger('myapp')
sensitive_filter = SensitiveFilter()
logger.addFilter(sensitive_filter)

# 也可以加到Handler上
error_handler = logging.StreamHandler()
error_handler.addFilter(ErrorThresholdFilter(500))

为什么会需要Filter?我举个例子。有一次线上系统疯狂打印调试日志,把磁盘撑爆了。后来发现是某个第三方库的Logger级别设得太低。用Filter可以精准拦截特定Logger或特定关键词的日志,比改源码优雅得多。

核心思路:Logger做准入,Handler做输出,Formatter做排版,Filter做筛选。四者配合,才能构建一套健壮的日志系统。

4.5 四大组件的协作流程

咱们用一张图来理解它们是怎么配合的:

Logger 级别判断 + 传播 Filter 精准筛选 Handler 输出目的地 Formatter 格式化输出 文件 / 控制台 / 网络 日志消息流向 Logger → Filter → Handler → Formatter → 输出 注意: • 一个Logger可挂多个Handler • 每个Handler可独立设置Formatter和Filter • Filter可加在Logger或Handler上

流程其实很简单:你的代码调用logger.info(),Logger先判断级别是否达标。达标后,经过Filter筛选,再交给Handler。Handler拿到日志后,用Formatter格式化,最后写到目标位置。每个环节各司其职,互不干扰。

4.6 实战:构建一个生产级日志配置

说了这么多,咱们来点实际的。下面是我个人常用的生产环境日志配置模板:

import logging
import logging.handlers
import json
from datetime import datetime

class JsonFormatter(logging.Formatter):
    """输出JSON格式的日志,方便日志收集系统解析"""
    def format(self, record):
        log_record = {
            'timestamp': datetime.utcnow().isoformat(),
            'level': record.levelname,
            'logger': record.name,
            'module': record.module,
            'function': record.funcName,
            'line': record.lineno,
            'message': record.getMessage(),
        }
        # 如果有异常信息,也加上
        if record.exc_info and record.exc_info[0]:
            log_record['exception'] = self.formatException(record.exc_info)
        return json.dumps(log_record, ensure_ascii=False)

def setup_logging():
    logger = logging.getLogger('myapp')
    logger.setLevel(logging.DEBUG)
    
    # 控制台输出:只输出INFO及以上
    console = logging.StreamHandler()
    console.setLevel(logging.INFO)
    console.setFormatter(logging.Formatter(
        '%(asctime)s [%(levelname)s] %(message)s'
    ))
    
    # 文件输出:按天滚动,保留30天
    file_handler = logging.handlers.TimedRotatingFileHandler(
        'logs/app.log', when='midnight', backupCount=30
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(JsonFormatter())
    
    # 错误单独输出:只记录ERROR及以上
    error_handler = logging.handlers.RotatingFileHandler(
        'logs/error.log', maxBytes=50*1024*1024, backupCount=10
    )
    error_handler.setLevel(logging.ERROR)
    error_handler.setFormatter(JsonFormatter())
    
    # 添加敏感信息过滤器
    class PasswordFilter(logging.Filter):
        def filter(self, record):
            msg = record.getMessage()
            return 'password' not in msg.lower() and '' not in msg.lower()
    
    logger.addFilter(PasswordFilter())
    
    # 挂载所有Handler
    logger.addHandler(console)
    logger.addHandler(file_handler)
    logger.addHandler(error_handler)
    
    return logger

# 使用
logger = setup_logging()
logger.info("系统启动成功")
logger.error("数据库连接超时", exc_info=True)

避坑指南:我曾经把日志文件路径写成了相对路径,结果部署到不同环境时日志散落各处。建议用路径,或者通过配置文件传入。另外,别忘了处理日志目录不存在的情况——os.makedirs可以帮你。

好了,四大组件就讲到这里。记住它们的职责:Logger是入口,Handler是出口,Formatter是排版,Filter是筛子。下一章咱们聊聊日志的配置管理——怎么用配置文件优雅地搞定这一切,而不是在代码里硬编码。