# 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 状态**: 提议中
**下一步**: 等待团队评审和批准