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
15 KiB
RFC 201: 日志系统迁移 - 从 loguru 到标准库 logging
状态: 提议中
创建日期: 2026-01-29
作者: System
类型: 重构
概述
将项目中所有使用 loguru 的代码迁移到 Python 标准库 logging,以符合 Jointo 技术栈规范。
背景
当前问题
- 技术栈不一致: 项目技术栈规范要求使用标准库
logging,但实际代码中大量使用了第三方库loguru - 依赖冗余:
loguru增加了不必要的外部依赖 - 维护成本: 团队需要同时维护两套日志系统的知识
- 文档不一致: 新文档已按标准库
logging编写,但现有代码仍使用loguru
影响范围
通过代码扫描,发现 23 个文件使用了 loguru:
服务层 (5 个文件):
app/services/attachment_service.pyapp/services/payment_service.pyapp/services/payment/alipay_payment.pyapp/services/payment/wechat_payment.pyapp/services/recharge_service.py
API 层 (2 个文件):
app/api/v1/recharge.pyapp/api/v1/attachments.py
仓储层 (2 个文件):
app/repositories/recharge_repository.pyapp/repositories/attachment_repository.py
任务层 (5 个文件):
app/tasks/maintenance_tasks.pyapp/tasks/sms_tasks.pyapp/tasks/ai_tasks.pyapp/tasks/recharge_tasks.pyapp/tasks/export_tasks.py
核心模块 (3 个文件):
app/core/database.pyapp/core/cache.pyapp/core/logging.py⚠️ 关键文件
迁移脚本 (6 个文件):
app/migrations/011_sms_service_tables.pyapp/migrations/003_project_tables.pyapp/migrations/001_uuid_migration.pyapp/migrations/002_folder_enhancement.pyapp/migrations/010_recharge_service_tables.py
中间件 (1 个文件):
app/middleware/logging.py
目标
- ✅ 统一日志系统: 全项目使用标准库
logging - ✅ 保持功能: 保留
loguru的核心功能(结构化日志、日志轮转、彩色输出) - ✅ 零停机迁移: 分批迁移,不影响现有功能
- ✅ 提升可维护性: 减少外部依赖,降低学习成本
设计方案
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 logginglogger = logging.getLogger(__name__) |
logging 需要显式创建 logger |
| 格式化 | f-string: f"msg {var}" |
%-formatting: "msg %s", var |
logging 推荐使用 % 格式化 |
| 异常信息 | 自动捕获 | exc_info=True |
logging 需要显式指定 |
| 彩色输出 | 内置 | 需要自定义 Formatter | 已在配置中实现 |
| 日志轮转 | 内置 | TimedRotatingFileHandler |
已在配置中实现 |
3. 迁移计划
阶段 1: 核心模块(优先级:最高)
目标: 建立标准日志配置,为其他模块提供基础
文件:
- ✅
app/core/logging.py- 重写日志配置模块 app/core/database.py- 数据库连接日志app/core/cache.py- 缓存操作日志app/middleware/logging.py- 请求日志中间件
预计时间: 2 小时
阶段 2: 服务层(优先级:高)
目标: 迁移业务逻辑层的日志
文件:
app/services/payment_service.pyapp/services/payment/wechat_payment.pyapp/services/payment/alipay_payment.pyapp/services/recharge_service.pyapp/services/attachment_service.py
预计时间: 3 小时
阶段 3: API 和仓储层(优先级:中)
目标: 迁移接口层和数据访问层
文件:
app/api/v1/recharge.pyapp/api/v1/attachments.pyapp/repositories/recharge_repository.pyapp/repositories/attachment_repository.py
预计时间: 2 小时
阶段 4: 任务层(优先级:中)
目标: 迁移异步任务的日志
文件:
app/tasks/ai_tasks.pyapp/tasks/export_tasks.pyapp/tasks/maintenance_tasks.pyapp/tasks/recharge_tasks.pyapp/tasks/sms_tasks.py
预计时间: 2 小时
阶段 5: 迁移脚本(优先级:低)
目标: 迁移数据库迁移脚本的日志
文件:
app/migrations/001_uuid_migration.pyapp/migrations/002_folder_enhancement.pyapp/migrations/003_project_tables.pyapp/migrations/010_recharge_service_tables.pyapp/migrations/011_sms_service_tables.py
预计时间: 1 小时
阶段 6: 清理和验证(优先级:最高)
目标: 移除 loguru 依赖,验证迁移完整性
任务:
- 从
requirements.txt移除loguru==0.7.2 - 全局搜索确认无遗漏的
loguru引用 - 运行测试套件验证功能
- 更新相关文档
预计时间: 1 小时
实施步骤
Step 1: 创建新的日志配置模块
# 备份现有文件
cp server/app/core/logging.py server/app/core/logging.py.bak
# 实现新的日志配置(见上文设计方案)
Step 2: 分批迁移代码
每个阶段的迁移步骤:
-
备份文件
cp <file>.py <file>.py.bak -
替换导入语句
# 删除 from loguru import logger # 添加 import logging logger = logging.getLogger(__name__) -
替换日志调用
- f-string → %-formatting
- 添加
exc_info=True到错误日志
-
测试验证
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
风险评估
高风险
-
日志格式变化: 可能影响日志解析工具
- 缓解: 保持相似的日志格式
- 回滚: 保留备份文件
-
性能影响: logging 性能可能不如 loguru
- 缓解: 使用异步日志处理器
- 监控: 观察应用性能指标
中风险
-
遗漏文件: 可能遗漏某些使用 loguru 的文件
- 缓解: 使用全局搜索工具
- 验证: 运行完整测试套件
-
第三方库兼容性: 某些库可能依赖 loguru
- 缓解: 检查依赖树
- 测试: 集成测试验证
低风险
- 开发体验: 开发者需要适应新的日志 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
成功标准
- ✅ 所有文件不再使用
loguru - ✅
requirements.txt中移除loguru依赖 - ✅ 所有测试通过
- ✅ 日志功能正常(控制台输出、文件轮转、彩色显示)
- ✅ 应用性能无明显下降
- ✅ 文档已更新
后续工作
- 监控优化: 集成 ELK/Loki 等日志聚合系统
- 性能调优: 根据实际使用情况优化日志配置
- 文档完善: 编写日志最佳实践指南
- 团队培训: 分享标准库 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 状态: 提议中
下一步: 等待团队评审和批准