2、Python日志模块:logging模块基础、Handler与Formatter、日志级别与过滤

好,咱们正式开始聊Python的logging模块。说实话,我见过太多团队在日志这块翻车了——要么是日志满天飞,磁盘撑爆;要么是出了线上事故,翻遍日志啥也找不到。说白了,日志写得好不好,直接决定了你半夜被叫起来排查问题的效率。

我个人习惯,不管项目大小,第一件事就是把日志体系搭好。今天咱们就把它彻底讲透。

2.1 logging模块基础:从零开始

Python自带的logging模块,其实已经很强大了。你不需要装任何第三方库,就能搞定90%的日志需求。

先看一个最简单的例子:

import logging

logging.debug("这是debug信息")
logging.info("这是info信息")
logging.warning("这是warning信息")
logging.error("这是error信息")
logging.critical("这是critical信息")

你运行一下试试?你会发现,只有warning及以上的日志被打印出来了。为什么?因为默认的日志级别是WARNING。嗯,这里要注意,初学者经常在这踩坑——写了debug日志,结果啥也没看到,还以为代码没执行。

小技巧: 开发阶段我建议直接设置成DEBUG级别,别省这点输出。等上线了再调高。

2.2 日志级别:五个等级,各有用途

Python定义了五个标准日志级别,从低到高分别是:

级别 数值 什么时候用
DEBUG 10 调试信息,比如变量值、函数入口出口
INFO 20 正常流程信息,比如服务启动、请求到达
WARNING 30 潜在问题,比如磁盘快满了、重试操作
ERROR 40 出错了但还能继续跑,比如数据库连接失败
CRITICAL 50 系统要挂了,比如内存耗尽、核心服务宕机

我在项目中遇到过一种情况:有人把所有日志都写成ERROR级别。结果报警系统天天响,最后大家直接无视了所有ERROR日志。这其实是个管理问题——日志级别不是随便选的,它代表了严重程度。

你可以通过setLevel()来控制日志输出门槛:

import logging

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

logger.debug("这条会输出,因为级别够了")

2.3 Handler:日志该去哪?

日志不能光在控制台看吧?你得把它写到文件里、发到远程服务器、甚至推送到消息队列。Handler就是干这个的。

常见的Handler有这些:

  • StreamHandler:输出到控制台
  • FileHandler:写入文件
  • RotatingFileHandler:按文件大小滚动
  • TimedRotatingFileHandler:按时间滚动
  • SMTPHandler:发邮件(慎用,容易把邮箱塞爆)

来看一个实际例子:

import logging
from logging.handlers import RotatingFileHandler

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

# 控制台输出
console = logging.StreamHandler()
console.setLevel(logging.INFO)

# 文件输出,每个文件5MB,保留3个备份
file_handler = RotatingFileHandler(
    "app.log", maxBytes=5*1024*1024, backupCount=3
)
file_handler.setLevel(logging.DEBUG)

logger.addHandler(console)
logger.addHandler(file_handler)
避坑指南: 我曾经在生产环境见过一个服务,日志文件写到了100GB还没滚动。结果磁盘满了,服务直接挂了。所以,生产环境一定要用RotatingFileHandler或TimedRotatingFileHandler,别偷懒。

2.4 Formatter:日志长啥样?

日志的格式,说白了就是一条字符串。但这条字符串里包含什么信息,差别很大。

我常用的格式模板:

formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
)

常用的格式化字段:

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

把Formatter挂到Handler上:

formatter = logging.Formatter(
    '%(asctime)s | %(levelname)-8s | %(message)s'
)
console.setFormatter(formatter)
file_handler.setFormatter(formatter)

你想想看,如果没有时间戳和行号,线上出了错你都不知道是哪一秒、哪一行代码出的问题。所以,Formatter不是装饰,是刚需

2.5 日志过滤:只抓你想要的

有时候,你不想看到所有日志。比如,第三方库的debug信息太吵了,或者你只想关注某个模块的ERROR日志。

Filter就是干这个的:

import logging

class SensitiveFilter(logging.Filter):
    def filter(self, record):
        # 过滤掉包含密码的日志
        return "password" not in record.getMessage()

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

更常见的场景是按模块过滤:

# 只让my_app模块输出DEBUG,其他模块保持WARNING
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

# 第三方库的日志,保持安静
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
经验之谈: 我在做微服务日志聚合时,经常用Filter来标记不同服务的日志。比如给每个服务加一个service_name字段,这样在ELK里就能快速筛选。这个技巧,后面讲日志聚合时会详细展开。

2.6 完整示例:一个生产级的日志配置

最后,给你一个我常用的模板。直接拿去用,改改路径就行:

import logging
import logging.handlers
import os

def setup_logger(name, log_dir="logs"):
    os.makedirs(log_dir, exist_ok=True)
    
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)
    
    # 格式
    formatter = logging.Formatter(
        '%(asctime)s | %(name)s | %(levelname)-8s | '
        '%(filename)s:%(lineno)d | %(message)s'
    )
    
    # 控制台:只输出INFO及以上
    console = logging.StreamHandler()
    console.setLevel(logging.INFO)
    console.setFormatter(formatter)
    
    # 文件:输出所有级别,按天滚动,保留30天
    file_handler = logging.handlers.TimedRotatingFileHandler(
        os.path.join(log_dir, f"{name}.log"),
        when="midnight",
        interval=1,
        backupCount=30
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)
    
    # 错误日志单独存一份
    error_handler = logging.handlers.RotatingFileHandler(
        os.path.join(log_dir, f"{name}_error.log"),
        maxBytes=10*1024*1024,
        backupCount=5
    )
    error_handler.setLevel(logging.ERROR)
    error_handler.setFormatter(formatter)
    
    logger.addHandler(console)
    logger.addHandler(file_handler)
    logger.addHandler(error_handler)
    
    return logger

# 使用
logger = setup_logger("order_service")
logger.info("订单服务启动成功")
logger.error("数据库连接超时,重试中...")

这个配置,我用了好几年。控制台看个大概,文件里查细节,错误日志单独追踪。三个Handler各司其职,互不干扰。

好了,logging模块的基础就这些。下一节咱们聊多进程环境下的日志问题——那才是真正考验功底的地方。