You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

15 KiB

RFC 201: 日志系统迁移 - 从 loguru 到标准库 logging

状态: 提议中
创建日期: 2026-01-29
作者: System
类型: 重构


概述

将项目中所有使用 loguru 的代码迁移到 Python 标准库 logging,以符合 Jointo 技术栈规范。

背景

当前问题

  1. 技术栈不一致: 项目技术栈规范要求使用标准库 logging,但实际代码中大量使用了第三方库 loguru
  2. 依赖冗余: loguru 增加了不必要的外部依赖
  3. 维护成本: 团队需要同时维护两套日志系统的知识
  4. 文档不一致: 新文档已按标准库 logging 编写,但现有代码仍使用 loguru

影响范围

通过代码扫描,发现 23 个文件使用了 loguru

服务层 (5 个文件):

  • app/services/attachment_service.py
  • app/services/payment_service.py
  • app/services/payment/alipay_payment.py
  • app/services/payment/wechat_payment.py
  • app/services/recharge_service.py

API 层 (2 个文件):

  • app/api/v1/recharge.py
  • app/api/v1/attachments.py

仓储层 (2 个文件):

  • app/repositories/recharge_repository.py
  • app/repositories/attachment_repository.py

任务层 (5 个文件):

  • app/tasks/maintenance_tasks.py
  • app/tasks/sms_tasks.py
  • app/tasks/ai_tasks.py
  • app/tasks/recharge_tasks.py
  • app/tasks/export_tasks.py

核心模块 (3 个文件):

  • app/core/database.py
  • app/core/cache.py
  • app/core/logging.py ⚠️ 关键文件

迁移脚本 (6 个文件):

  • app/migrations/011_sms_service_tables.py
  • app/migrations/003_project_tables.py
  • app/migrations/001_uuid_migration.py
  • app/migrations/002_folder_enhancement.py
  • app/migrations/010_recharge_service_tables.py

中间件 (1 个文件):

  • app/middleware/logging.py

目标

  1. 统一日志系统: 全项目使用标准库 logging
  2. 保持功能: 保留 loguru 的核心功能(结构化日志、日志轮转、彩色输出)
  3. 零停机迁移: 分批迁移,不影响现有功能
  4. 提升可维护性: 减少外部依赖,降低学习成本

设计方案

1. 日志配置设计

1.1 配置文件结构

# app/core/logging.py
import logging
import logging.handlers
import sys
from pathlib import Path
from typing import Optional
from app.core.config import get_settings

settings = get_settings()


class ColoredFormatter(logging.Formatter):
    """彩色日志格式化器(开发环境)"""
    
    COLORS = {
        'DEBUG': '\033[36m',      # 青色
        'INFO': '\033[32m',       # 绿色
        'WARNING': '\033[33m',    # 黄色
        'ERROR': '\033[31m',      # 红色
        'CRITICAL': '\033[35m',   # 紫色
    }
    RESET = '\033[0m'
    
    def format(self, record):
        log_color = self.COLORS.get(record.levelname, self.RESET)
        record.levelname = f"{log_color}{record.levelname}{self.RESET}"
        return super().format(record)


class StructuredFormatter(logging.Formatter):
    """结构化日志格式化器(生产环境)"""
    
    def format(self, record):
        # 添加额外的上下文信息
        if not hasattr(record, 'request_id'):
            record.request_id = '-'
        if not hasattr(record, 'user_id'):
            record.user_id = '-'
        return super().format(record)


def setup_logging(
    log_level: str = "INFO",
    log_dir: Optional[Path] = None,
    enable_console: bool = True,
    enable_file: bool = True,
    enable_color: bool = True
) -> None:
    """配置日志系统
    
    Args:
        log_level: 日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL)
        log_dir: 日志文件目录
        enable_console: 是否启用控制台输出
        enable_file: 是否启用文件输出
        enable_color: 是否启用彩色输出(仅控制台)
    """
    # 获取根日志记录器
    root_logger = logging.getLogger()
    root_logger.setLevel(getattr(logging, log_level.upper()))
    
    # 清除现有处理器
    root_logger.handlers.clear()
    
    # 日志格式
    console_format = (
        "%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s"
    )
    file_format = (
        "%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | "
        "request_id=%(request_id)s | user_id=%(user_id)s | %(message)s"
    )
    
    # 控制台处理器
    if enable_console:
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setLevel(logging.DEBUG)
        
        if enable_color:
            console_formatter = ColoredFormatter(console_format)
        else:
            console_formatter = logging.Formatter(console_format)
        
        console_handler.setFormatter(console_formatter)
        root_logger.addHandler(console_handler)
    
    # 文件处理器
    if enable_file and log_dir:
        log_dir = Path(log_dir)
        log_dir.mkdir(parents=True, exist_ok=True)
        
        # 应用日志(所有级别)
        app_handler = logging.handlers.TimedRotatingFileHandler(
            filename=log_dir / "app.log",
            when="midnight",
            interval=1,
            backupCount=30,
            encoding="utf-8"
        )
        app_handler.setLevel(logging.DEBUG)
        app_handler.setFormatter(StructuredFormatter(file_format))
        root_logger.addHandler(app_handler)
        
        # 错误日志(仅 ERROR 及以上)
        error_handler = logging.handlers.TimedRotatingFileHandler(
            filename=log_dir / "error.log",
            when="midnight",
            interval=1,
            backupCount=90,
            encoding="utf-8"
        )
        error_handler.setLevel(logging.ERROR)
        error_handler.setFormatter(StructuredFormatter(file_format))
        root_logger.addHandler(error_handler)
    
    # 设置第三方库日志级别
    logging.getLogger("uvicorn").setLevel(logging.INFO)
    logging.getLogger("sqlalchemy").setLevel(logging.WARNING)
    logging.getLogger("asyncpg").setLevel(logging.WARNING)
    logging.getLogger("celery").setLevel(logging.INFO)


def get_logger(name: str) -> logging.Logger:
    """获取日志记录器
    
    Args:
        name: 日志记录器名称(通常使用 __name__)
        
    Returns:
        logging.Logger: 日志记录器实例
        
    Example:
        >>> logger = get_logger(__name__)
        >>> logger.info("Application started")
    """
    return logging.getLogger(name)


# 初始化日志系统
setup_logging(
    log_level=settings.LOG_LEVEL,
    log_dir=Path(settings.LOG_DIR) if settings.LOG_DIR else None,
    enable_console=True,
    enable_file=True,
    enable_color=settings.ENVIRONMENT == "development"
)

1.2 配置项

# app/core/config.py
from pydantic_settings import BaseSettings
from pydantic import Field

class Settings(BaseSettings):
    # 日志配置
    LOG_LEVEL: str = Field(default="INFO", description="日志级别")
    LOG_DIR: str = Field(default="logs", description="日志文件目录")
    
    # 环境
    ENVIRONMENT: str = Field(default="development", description="运行环境")
    
    model_config = {
        "env_file": ".env",
        "extra": "forbid"
    }

2. 迁移策略

2.1 代码替换模式

替换前 (loguru):

from loguru import logger

logger.info(f"用户 {user_id} 创建订单 {order_no}")
logger.error(f"订单创建失败: {error}")

替换后 (logging):

import logging

logger = logging.getLogger(__name__)

logger.info("用户 %s 创建订单 %s", user_id, order_no)
logger.error("订单创建失败: %s", error, exc_info=True)

2.2 关键差异

功能 loguru logging 说明
导入 from loguru import logger import logging
logger = logging.getLogger(__name__)
logging 需要显式创建 logger
格式化 f-string: f"msg {var}" %-formatting: "msg %s", var logging 推荐使用 % 格式化
异常信息 自动捕获 exc_info=True logging 需要显式指定
彩色输出 内置 需要自定义 Formatter 已在配置中实现
日志轮转 内置 TimedRotatingFileHandler 已在配置中实现

3. 迁移计划

阶段 1: 核心模块(优先级:最高)

目标: 建立标准日志配置,为其他模块提供基础

文件:

  1. app/core/logging.py - 重写日志配置模块
  2. app/core/database.py - 数据库连接日志
  3. app/core/cache.py - 缓存操作日志
  4. app/middleware/logging.py - 请求日志中间件

预计时间: 2 小时

阶段 2: 服务层(优先级:高)

目标: 迁移业务逻辑层的日志

文件:

  1. app/services/payment_service.py
  2. app/services/payment/wechat_payment.py
  3. app/services/payment/alipay_payment.py
  4. app/services/recharge_service.py
  5. app/services/attachment_service.py

预计时间: 3 小时

阶段 3: API 和仓储层(优先级:中)

目标: 迁移接口层和数据访问层

文件:

  1. app/api/v1/recharge.py
  2. app/api/v1/attachments.py
  3. app/repositories/recharge_repository.py
  4. app/repositories/attachment_repository.py

预计时间: 2 小时

阶段 4: 任务层(优先级:中)

目标: 迁移异步任务的日志

文件:

  1. app/tasks/ai_tasks.py
  2. app/tasks/export_tasks.py
  3. app/tasks/maintenance_tasks.py
  4. app/tasks/recharge_tasks.py
  5. app/tasks/sms_tasks.py

预计时间: 2 小时

阶段 5: 迁移脚本(优先级:低)

目标: 迁移数据库迁移脚本的日志

文件:

  1. app/migrations/001_uuid_migration.py
  2. app/migrations/002_folder_enhancement.py
  3. app/migrations/003_project_tables.py
  4. app/migrations/010_recharge_service_tables.py
  5. app/migrations/011_sms_service_tables.py

预计时间: 1 小时

阶段 6: 清理和验证(优先级:最高)

目标: 移除 loguru 依赖,验证迁移完整性

任务:

  1. requirements.txt 移除 loguru==0.7.2
  2. 全局搜索确认无遗漏的 loguru 引用
  3. 运行测试套件验证功能
  4. 更新相关文档

预计时间: 1 小时


实施步骤

Step 1: 创建新的日志配置模块

# 备份现有文件
cp server/app/core/logging.py server/app/core/logging.py.bak

# 实现新的日志配置(见上文设计方案)

Step 2: 分批迁移代码

每个阶段的迁移步骤:

  1. 备份文件

    cp <file>.py <file>.py.bak
    
  2. 替换导入语句

    # 删除
    from loguru import logger
    
    # 添加
    import logging
    logger = logging.getLogger(__name__)
    
  3. 替换日志调用

    • f-string → %-formatting
    • 添加 exc_info=True 到错误日志
  4. 测试验证

    docker exec jointo-server-app pytest tests/unit/<test_file>.py -v
    

Step 3: 更新依赖

# 编辑 requirements.txt
# 移除: loguru==0.7.2

# 重新构建容器
docker-compose -f server/docker-compose.yml build app
docker-compose -f server/docker-compose.yml up -d

Step 4: 全局验证

# 搜索残留的 loguru 引用
grep -r "from loguru import" server/app/
grep -r "import loguru" server/app/

# 运行完整测试套件
docker exec jointo-server-app pytest tests/ -v

# 检查日志输出
docker exec jointo-server-app tail -f logs/app.log

风险评估

高风险

  1. 日志格式变化: 可能影响日志解析工具

    • 缓解: 保持相似的日志格式
    • 回滚: 保留备份文件
  2. 性能影响: logging 性能可能不如 loguru

    • 缓解: 使用异步日志处理器
    • 监控: 观察应用性能指标

中风险

  1. 遗漏文件: 可能遗漏某些使用 loguru 的文件

    • 缓解: 使用全局搜索工具
    • 验证: 运行完整测试套件
  2. 第三方库兼容性: 某些库可能依赖 loguru

    • 缓解: 检查依赖树
    • 测试: 集成测试验证

低风险

  1. 开发体验: 开发者需要适应新的日志 API
    • 缓解: 提供迁移指南和示例
    • 培训: 团队分享会

回滚计划

如果迁移出现严重问题,可以快速回滚:

# 1. 恢复备份文件
for file in $(find server/app -name "*.py.bak"); do
    mv "$file" "${file%.bak}"
done

# 2. 恢复 requirements.txt
git checkout server/requirements.txt

# 3. 重新构建容器
docker-compose -f server/docker-compose.yml build app
docker-compose -f server/docker-compose.yml up -d

成功标准

  1. 所有文件不再使用 loguru
  2. requirements.txt 中移除 loguru 依赖
  3. 所有测试通过
  4. 日志功能正常(控制台输出、文件轮转、彩色显示)
  5. 应用性能无明显下降
  6. 文档已更新

后续工作

  1. 监控优化: 集成 ELK/Loki 等日志聚合系统
  2. 性能调优: 根据实际使用情况优化日志配置
  3. 文档完善: 编写日志最佳实践指南
  4. 团队培训: 分享标准库 logging 的使用技巧

参考资料


附录

A. 日志级别使用指南

级别 使用场景 示例
DEBUG 详细的调试信息 logger.debug("SQL: %s", query)
INFO 常规信息 logger.info("用户 %s 登录成功", user_id)
WARNING 警告信息 logger.warning("缓存未命中: key=%s", key)
ERROR 错误信息 logger.error("订单创建失败", exc_info=True)
CRITICAL 严重错误 logger.critical("数据库连接失败")

B. 常见日志模式

import logging

logger = logging.getLogger(__name__)

# 1. 基本日志
logger.info("应用启动")

# 2. 带参数的日志
logger.info("用户 %s 创建订单 %s", user_id, order_no)

# 3. 异常日志
try:
    result = risky_operation()
except Exception as e:
    logger.error("操作失败: %s", str(e), exc_info=True)

# 4. 结构化日志(使用 extra)
logger.info(
    "订单创建成功",
    extra={
        "request_id": request_id,
        "user_id": user_id,
        "order_no": order_no
    }
)

# 5. 条件日志
if logger.isEnabledFor(logging.DEBUG):
    logger.debug("详细数据: %s", expensive_operation())

C. 迁移检查清单

  • 阶段 1: 核心模块迁移完成
  • 阶段 2: 服务层迁移完成
  • 阶段 3: API 和仓储层迁移完成
  • 阶段 4: 任务层迁移完成
  • 阶段 5: 迁移脚本迁移完成
  • 阶段 6: 清理和验证完成
  • 全局搜索确认无 loguru 残留
  • 测试套件全部通过
  • 日志文件正常生成和轮转
  • 彩色输出正常工作
  • 性能测试通过
  • 文档已更新
  • 团队已通知

RFC 状态: 提议中
下一步: 等待团队评审和批准