日志系统

21 阅读5分钟

深入理解 Python 日志系统:从基础到生产实践

在 Python 开发中,日志系统是最基础也最重要的工具之一。无论是调试问题、监控应用状态,还是审计用户行为,日志都扮演着不可或缺的角色。本文将深入探讨 Python 日志系统的核心概念、最佳实践以及生产环境中的应用。


一、为什么需要日志?

1.1 日志的价值

  • 问题诊断:当应用出现异常时,日志是第一时间获取现场信息的窗口
  • 性能监控:通过日志可以追踪关键操作的执行时间
  • 安全审计:记录用户操作,满足合规要求
  • 业务分析:从日志中提取业务指标和趋势

1.2 日志 vs print

很多初学者习惯使用 print 进行调试,但生产环境中应该使用日志系统:

# ❌ 不推荐
print(f"用户 {user_id} 登录成功")

# ✅ 推荐
import logging
logging.info(f"用户 {user_id} 登录成功")

日志系统的优势:

  • 可配置的输出级别(DEBUG, INFO, WARNING, ERROR, CRITICAL)
  • 支持多输出目标(控制台、文件、邮件等)
  • 可自定义格式
  • 可动态调整日志级别无需重启应用

二、日志系统核心概念

2.1 Logger、Handler、Formatter

Python 日志系统由三个核心组件构成:

import logging

# 1. Logger - 日志记录器
logger = logging.getLogger(__name__)

# 2. Handler - 处理器,决定日志输出到哪里
console_handler = logging.StreamHandler()
file_handler = logging.FileHandler('app.log')

# 3. Formatter - 格式化器,决定日志长什么样
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# 配置
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.setLevel(logging.INFO)

# 使用
logger.info("应用启动成功")
logger.error("发生错误")

2.2 日志级别

级别数值用途
DEBUG10详细的调试信息
INFO20一般信息
WARNING30警告信息
ERROR40错误信息
CRITICAL50严重错误

三、实际代码示例

3.1 基础日志配置

import logging
import os
from logging.handlers import RotatingFileHandler

def setup_logger(name, log_file=None, level=logging.INFO):
    """
    配置日志系统
    
    Args:
        name: 日志记录器名称
        log_file: 日志文件路径
        level: 日志级别
    
    Returns:
        配置好的 logger 对象
    """
    logger = logging.getLogger(name)
    logger.setLevel(level)
    
    # 避免重复添加 handler
    if logger.handlers:
        return logger
    
    # 创建 formatter
    formatter = logging.Formatter(
        '%(asctime)s | %(name)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    
    # 控制台 handler
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)
    
    # 文件 handler(如果指定)
    if log_file:
        # 确保日志目录存在
        os.makedirs(os.path.dirname(log_file), exist_ok=True)
        
        # 使用 RotatingFileHandler,日志文件超过 10MB 自动轮转
        file_handler = RotatingFileHandler(
            log_file,
            maxBytes=10*1024*1024,  # 10MB
            backupCount=5           # 保留 5 个备份文件
        )
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)
    
    return logger

# 使用示例
logger = setup_logger('my_app', 'logs/app.log')
logger.info("应用初始化完成")

3.2 异常日志记录

def process_user_data(user_id, data):
    """处理用户数据"""
    logger = logging.getLogger('user_processor')
    
    try:
        logger.info(f"开始处理用户 {user_id} 的数据")
        
        # 模拟处理逻辑
        if not data:
            raise ValueError("数据不能为空")
        
        result = transform_data(data)
        logger.info(f"用户 {user_id} 数据处理成功,结果:{result}")
        return result
        
    except ValueError as e:
        logger.warning(f"用户 {user_id} 数据验证失败:{e}")
        raise
        
    except Exception as e:
        # 记录完整的异常堆栈
        logger.error(f"处理用户 {user_id} 数据时发生异常", exc_info=True)
        raise

def transform_data(data):
    """转换数据"""
    return data.upper()

# 测试
if __name__ == "__main__":
    setup_logger('test', 'logs/test.log')
    try:
        process_user_data(123, "")
    except:
        pass

3.3 结构化日志

对于机器可读的日志,可以使用结构化格式(如 JSON):

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,
            'message': record.getMessage(),
            'module': record.module,
            'function': record.funcName,
            'line': record.lineno,
        }
        
        # 添加额外字段
        if hasattr(record, 'user_id'):
            log_record['user_id'] = record.user_id
        
        if hasattr(record, 'request_id'):
            log_record['request_id'] = record.request_id
        
        if record.exc_info:
            log_record['exception'] = self.formatException(record.exc_info)
        
        return json.dumps(log_record, ensure_ascii=False)

# 使用
logger = logging.getLogger('structured_app')
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)

# 记录带额外字段的日志
logger.info("用户操作", extra={'user_id': 123, 'request_id': 'req-456'})

3.4 日志上下文管理

使用上下文管理器来自动记录请求上下文:

from contextlib import contextmanager
import uuid

@contextmanager
def request_context(request_id=None):
    """请求上下文管理器"""
    if request_id is None:
        request_id = str(uuid.uuid4())[:8]
    
    # 创建子 logger 并添加上下文
    logger = logging.getLogger('request')
    old_handlers = logger.handlers.copy()
    
    # 添加带上下文的 formatter
    class ContextFormatter(logging.Formatter):
        def format(self, record):
            record.request_id = request_id
            return super().format(record)
    
    # 使用
    logger.info(f"请求开始", extra={'request_id': request_id})
    try:
        yield request_id
        logger.info(f"请求完成", extra={'request_id': request_id})
    except Exception as e:
        logger.error(f"请求失败:{e}", extra={'request_id': request_id}, exc_info=True)
        raise

# 使用示例
with request_context() as req_id:
    print(f"处理请求 {req_id}")

四、生产环境最佳实践

4.1 日志配置管理

使用配置文件管理日志:

# logging_config.py
LOGGING_CONFIG = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'default': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        },
        'detailed': {
            'format': '%(asctime)s | %(name)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s',
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'INFO',
            'formatter': 'default',
        },
        'file': {
            'class': 'logging.handlers.RotatingFileHandler',
            'level': 'DEBUG',
            'formatter': 'detailed',
            'filename': 'logs/app.log',
            'maxBytes': 10485760,
            'backupCount': 5,
        },
        'error_file': {
            'class': 'logging.handlers.RotatingFileHandler',
            'level': 'ERROR',
            'formatter': 'detailed',
            'filename': 'logs/error.log',
            'maxBytes': 10485760,
            'backupCount': 5,
        },
    },
    'loggers': {
        '': {  # root logger
            'handlers': ['console', 'file', 'error_file'],
            'level': 'DEBUG',
            'propagate': True,
        },
        'uvicorn': {
            'handlers': ['console'],
            'level': 'INFO',
            'propagate': False,
        },
    },
}

# 在应用启动时加载
import logging.config
logging.config.dictConfig(LOGGING_CONFIG)

4.2 敏感信息脱敏

import re

class SensitiveDataFilter(logging.Filter):
    """敏感信息过滤器"""
    
    PATTERNS = [
        (r'password['"]?\s*[:=]\s*["']?[^"\s]+', 'password=***'),
        (r'api_key['"]?\s*[:=]\s*["']?[^"\s]+', 'api_key=***'),
        (r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', '****-****-****-****'),  # 信用卡号
        (r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Z|a-z]{2,}\b', '[EMAIL]'),  # 邮箱
    ]
    
    def filter(self, record):
        message = record.getMessage()
        for pattern, replacement in self.PATTERNS:
            message = re.sub(pattern, replacement, message, flags=re.IGNORECASE)
        record.msg = message
        return True

# 使用
logger = logging.getLogger('secure_app')
logger.addFilter(SensitiveDataFilter())
logger.info("用户登录,password=secret123, email=user@example.com")
# 输出:用户登录,password=***, email=[EMAIL]

五、总结

Python 日志系统是一个强大而灵活的工具。掌握以下要点:

  1. 选择合适的日志级别:DEBUG 用于开发,INFO 用于一般操作,WARNING/ERROR 用于问题
  2. 配置多个输出:控制台用于实时查看,文件用于持久化
  3. 使用结构化日志:便于日志分析和监控
  4. 敏感信息脱敏:保护用户隐私
  5. 日志轮转:避免日志文件过大

记住:好的日志系统能让你在问题发生时快速定位原因,是生产环境不可或缺的工具。