# 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 配置文件结构 ```python # 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 配置项 ```python # 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): ```python from loguru import logger logger.info(f"用户 {user_id} 创建订单 {order_no}") logger.error(f"订单创建失败: {error}") ``` **替换后** (logging): ```python 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: 创建新的日志配置模块 ```bash # 备份现有文件 cp server/app/core/logging.py server/app/core/logging.py.bak # 实现新的日志配置(见上文设计方案) ``` ### Step 2: 分批迁移代码 每个阶段的迁移步骤: 1. **备份文件** ```bash cp .py .py.bak ``` 2. **替换导入语句** ```python # 删除 from loguru import logger # 添加 import logging logger = logging.getLogger(__name__) ``` 3. **替换日志调用** - f-string → %-formatting - 添加 `exc_info=True` 到错误日志 4. **测试验证** ```bash docker exec jointo-server-app pytest tests/unit/.py -v ``` ### Step 3: 更新依赖 ```bash # 编辑 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: 全局验证 ```bash # 搜索残留的 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 - **缓解**: 提供迁移指南和示例 - **培训**: 团队分享会 --- ## 回滚计划 如果迁移出现严重问题,可以快速回滚: ```bash # 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 的使用技巧 --- ## 参考资料 - [Python logging 官方文档](https://docs.python.org/3/library/logging.html) - [Python logging cookbook](https://docs.python.org/3/howto/logging-cookbook.html) - [Jointo 技术栈规范 - 后端开发](/.claude/skills/jointo-tech-stack/references/backend.md) --- ## 附录 ### 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. 常见日志模式 ```python 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 状态**: 提议中 **下一步**: 等待团队评审和批准