2. Python日志模块基础:logging模块核心组件与基本配置

日志这东西,说白了就是系统的「黑匣子」。我在做量化交易系统时,有一次凌晨三点策略突然停止运行,要不是日志记录得完整,根本不可能在十分钟内定位到问题——原来是某个交易所的API接口返回格式变了。嗯,从那以后,我对日志的重视程度直接拉满。

Python自带的logging模块,别看它是标准库,功能一点都不弱。很多新手喜欢用print打日志,我建议你趁早改掉这个习惯。为什么?因为print输出到控制台就没了,而logging可以同时写文件、发邮件、输出到终端,还能按级别过滤。你想想看,生产环境里谁会盯着控制台看?

2.1 核心组件:Logger、Handler、Formatter、Filter

logging模块有四个核心组件,我习惯把它们比作一个「日志流水线」:

  • Logger:日志的入口,你代码里调用的logger.info()就是它
  • Handler:决定日志去哪,文件、控制台、网络,都靠它
  • Formatter:决定日志长什么样,时间、级别、消息,格式你说了算
  • Filter:决定哪些日志能过,哪些要拦,精细控制

我曾经见过一个团队,所有日志都往根Logger上怼,结果日志文件一个月涨到200GB。这就是典型的「只用Logger,不管Handler」的后果。

核心关系图:日志从产生到输出的完整流程

Logger 日志入口 Filter 过滤/筛选 Handler 输出目的地 Formatter 格式化输出 日志消息流:Logger → Filter → Handler → Formatter → 输出 一个Logger可以绑定多个Handler,每个Handler可以有自己的Formatter 📋 日志处理流水线

2.2 Logger:日志的起点

Logger是你要直接打交道的对象。我个人习惯给每个模块创建一个独立的Logger,而不是用全局的根Logger。

import logging

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

# 记录日志
logger.debug('订单参数校验开始')
logger.info('订单已提交,订单号: %s', order_id)
logger.warning('订单执行延迟,当前延迟: %.2f秒', delay)
logger.error('订单执行失败,错误码: %d', error_code)
logger.critical('系统内存不足,无法继续执行')

这里有个坑:setLevel设置的是Logger自己的级别,但最终生效的是Logger级别和Handler级别的「交集」——取两者中更严格的那个。我曾经调试了半天,发现Logger设了DEBUG,Handler设了WARNING,结果DEBUG日志死活不输出。嗯,这个细节很容易忽略。

命名建议:用__name__作为Logger名称,这样能自动继承模块的包路径。比如logger = logging.getLogger(__name__),日志里会显示quant_system.order_manager,定位问题非常方便。

2.3 Handler:日志去哪,它说了算

Handler决定了日志的「归宿」。常用的Handler有这么几种:

Handler类型 输出目标 适用场景
StreamHandler 控制台(stdout/stderr) 开发调试、本地运行
FileHandler 单个文件 简单日志记录
RotatingFileHandler 按大小轮转的文件 生产环境,防止日志撑爆磁盘
TimedRotatingFileHandler 按时间轮转的文件 需要按天/小时归档的场景
SMTPHandler 邮件 错误告警通知

我在量化系统里最常用的是TimedRotatingFileHandler。为什么?因为交易日志每天几百MB,如果不按天切割,一个月后一个文件几个GB,编辑器都打不开。而且按天切割还有个好处——方便回溯某一天的交易记录。

from logging.handlers import TimedRotatingFileHandler

# 按天轮转,保留30天
handler = TimedRotatingFileHandler(
    filename='logs/trading.log',
    when='midnight',    # 每天午夜切割
    interval=1,         # 间隔1天
    backupCount=30,     # 保留30个备份
    encoding='utf-8'
)
handler.setLevel(logging.INFO)

注意backupCount不是越大越好。我曾经有个同事设了365,结果一年后磁盘满了才发现。建议根据磁盘空间和日志量合理设置,一般量化系统保留30-90天就够了。

2.4 Formatter:日志长什么样

Formatter就是日志的「化妆师」。同样的信息,格式不同,可读性天差地别。

# 常用格式
formatter = logging.Formatter(
    fmt='%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# 输出示例:
# 2024-01-15 14:30:22 | INFO     | quant_system.order_manager | 订单已提交

我个人习惯的格式包含四个要素:时间、级别、模块名、消息。为什么?因为排查问题时,你首先要知道「什么时候发生的」「严重程度如何」「哪个模块出的」「具体是什么」。缺一个都可能让你多花半小时。

常用的格式化字段:

  • %(asctime)s:时间,配合datefmt自定义格式
  • %(levelname)s:日志级别,建议用%-8s左对齐固定宽度
  • %(name)s:Logger名称,定位模块
  • %(message)s:日志内容
  • %(filename)s:文件名,调试时有用
  • %(lineno)d:行号,精准定位代码位置

2.5 Filter:精细控制日志输出

Filter用得相对少,但某些场景下特别好用。比如我只想记录某个特定订单的日志,或者过滤掉某些敏感信息。

class SensitiveDataFilter(logging.Filter):
    """过滤掉包含敏感信息的日志"""
    def filter(self, record):
        # 如果日志消息包含密码字段,直接过滤
        if 'password' in record.getMessage().lower():
            return False
        return True

# 使用
logger.addFilter(SensitiveDataFilter())

我曾经在日志里不小心打印了用户的API密钥,还好是测试环境。从那以后,我所有生产环境的Logger都加了一个敏感信息过滤器。你想想看,如果日志泄露了交易所的API Key,后果不堪设想。

2.6 基本配置:快速上手

对于小型项目,用basicConfig一把梭就够了。但注意,它只能调用一次,后续调用无效。

import logging

# 快速配置
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s | %(levelname)-8s | %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
    filename='app.log',
    filemode='a'  # 追加模式
)

# 直接使用根Logger
logging.info('系统启动成功')

但说实话,basicConfig只适合脚本和简单应用。真正的量化系统,我建议用更灵活的配置方式——手动创建Logger、Handler、Formatter,然后组装起来。

import logging
from logging.handlers import RotatingFileHandler

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

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

# 创建文件Handler(按大小轮转)
file_handler = RotatingFileHandler(
    'logs/system.log',
    maxBytes=10*1024*1024,  # 10MB
    backupCount=5,
    encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)

# 创建Formatter
formatter = logging.Formatter(
    '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s'
)

# 组装
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

logger.addHandler(console_handler)
logger.addHandler(file_handler)

# 使用
logger.info('系统初始化完成')
logger.debug('加载配置文件: config.yaml')

最佳实践:开发环境用控制台输出INFO及以上级别,生产环境用文件记录DEBUG及以上级别。这样开发时不会被大量DEBUG日志刷屏,生产环境又能保留完整的调试信息。

2.7 避坑指南

最后分享几个我踩过的坑:

  • 重复日志:每次调用basicConfigaddHandler都会添加新的Handler,如果不检查,日志会重复输出。我习惯在添加Handler前先判断logger.handlers是否为空。
  • 编码问题:中文日志在Windows上经常乱码。记得在FileHandler里指定encoding='utf-8'
  • 性能问题:DEBUG级别的日志如果包含字符串格式化,即使不输出也会执行。用logger.debug('耗时操作: %s', expensive_func())logger.debug(f'耗时操作: {expensive_func()}')更高效,因为前者只在需要输出时才计算。
  • 日志泄露:永远不要在日志里打印密码、密钥、个人隐私信息。加Filter是个好习惯。

嗯,logging模块的基础就这些。掌握了这四个核心组件,你就能搭建出适合量化系统的日志体系了。下一节我们会聊怎么把这些组件组织成一个完整的日志配置,以及如何应对生产环境的各种挑战。


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