2. Python日志基础:logging模块核心概念

日志这东西,说白了就是程序的「黑匣子」。

我刚做运维那会儿,线上服务出了问题,第一反应就是翻日志。但那时候的日志写得乱七八糟,有的用print,有的自己写文件,查个问题跟大海捞针似的。后来我学乖了——统一用Python的logging模块,这才算真正入了门。

2.1 为什么非要用logging?

你可能会问:我直接用print不香吗?

嗯,小脚本确实可以。但生产环境里,print有几个硬伤:

  • 没法控制输出级别——调试信息跟错误信息混在一起
  • 没法灵活配置——想同时输出到文件和终端?得自己写两套逻辑
  • 没法格式化——时间戳、进程ID、行号,这些信息print写起来太累

logging模块就是来解决这些问题的。它把日志这件事抽象成了几个核心组件,你只需要搭积木一样组合起来就行。

2.2 四大核心组件

logging模块有四个核心概念:LoggerHandlerFormatterFilter

我习惯把它们比作一个「日志流水线」:

组件 角色 一句话理解
Logger 日志记录器 你代码里调用的那个对象,比如 logger.info()
Handler 日志处理器 决定日志去哪——文件、终端、网络
Formatter 日志格式化器 决定日志长什么样——时间戳、级别、消息
Filter 日志过滤器 决定哪些日志能过,哪些不能过

2.3 Logger:你的日志入口

Logger是你直接打交道的对象。我一般这样创建:

import logging

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

这里有个坑——getLogger传的参数是logger的名字,不是日志文件路径。名字可以随便起,但建议用模块名,比如__name__

我的习惯:每个模块都创建一个独立的logger,名字就用__name__。这样日志里能清楚看到是哪个模块输出的,排查问题快很多。

2.4 Handler:日志去哪了?

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

  • StreamHandler——输出到终端(标准输出)
  • FileHandler——输出到文件
  • RotatingFileHandler——按文件大小滚动
  • TimedRotatingFileHandler——按时间滚动

举个例子,我想同时输出到终端和文件:

import logging

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

# 终端处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# 文件处理器
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.DEBUG)

# 把处理器加到logger上
logger.addHandler(console_handler)
logger.addHandler(file_handler)
我曾经踩过的坑:给logger设置了级别,又给handler设置了级别。结果logger的级别是DEBUG,handler的级别是INFO,那DEBUG级别的日志就被handler拦住了。记住——日志要过两关:logger级别和handler级别,取两者中更严格的。

2.5 Formatter:日志长什么样?

Formatter就是日志的「化妆师」。我常用的格式:

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

常用的格式化字段:

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

Formatter要绑定到Handler上:

console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

2.6 Filter:精准控制日志

Filter用得相对少,但关键时刻很管用。比如我只想记录某个模块的ERROR日志:

class MyFilter(logging.Filter):
    def filter(self, record):
        return record.name == 'my_app' and record.levelno == logging.ERROR

handler.addFilter(MyFilter())

你想想看,如果日志量特别大,用Filter可以精准筛选,避免磁盘被撑爆。

2.7 日志级别:从DEBUG到CRITICAL

Python定义了五个级别,从低到高:

级别 数值 什么时候用?
DEBUG 10 调试信息,开发时用
INFO 20 正常操作信息,比如「用户登录」
WARNING 30 警告,不影响运行但需要注意
ERROR 40 错误,功能不可用
CRITICAL 50 严重错误,程序可能崩溃

级别是「向上包含」的。比如你设置级别为WARNING,那WARNING、ERROR、CRITICAL都会记录,DEBUG和INFO不会。

生产环境建议:线上一般设INFO或WARNING级别。DEBUG日志量太大,容易把磁盘写满。我见过一个案例,有人把DEBUG日志开到了线上,一天写了50GB,直接把磁盘撑爆了。

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

最后,给你看一个我常用的配置模板:

import logging
from logging.handlers import RotatingFileHandler

def setup_logger(name, log_file='app.log', level=logging.INFO):
    logger = logging.getLogger(name)
    logger.setLevel(level)
    
    # 文件处理器,按10MB滚动,保留5个备份
    file_handler = RotatingFileHandler(
        log_file, maxBytes=10*1024*1024, backupCount=5
    )
    file_handler.setLevel(level)
    
    # 终端处理器
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.WARNING)
    
    # 格式化器
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)
    
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    
    return logger

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

这个配置里,文件记录所有INFO及以上日志,终端只显示WARNING及以上。文件按大小滚动,不会无限增长。

嗯,logging模块的基础就这些。说白了就是四个组件搭积木——Logger是入口,Handler是出口,Formatter是造型,Filter是安检。你把这四个搞明白了,日志这块就算入门了。

下一章我们聊聊怎么把这些基础应用到实际监控场景中,到时候会更有意思。