第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.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是筛子。下一章咱们聊聊日志的配置管理——怎么用配置文件优雅地搞定这一切,而不是在代码里硬编码。