413 lines
16 KiB
Markdown
413 lines
16 KiB
Markdown
# 生产级日志系统设计方案
|
||
|
||
> 版本:v1.0
|
||
> 日期:2026-08-17
|
||
> 状态:待评审
|
||
|
||
---
|
||
|
||
## 一、现状分析
|
||
|
||
### 1.1 当前日志架构
|
||
|
||
```
|
||
┌─────────────┐
|
||
│ FastAPI │ ← 请求入口
|
||
└──────┬──────┘
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Logging │ ← Python logging 模块
|
||
│ Middleware │
|
||
└──────┬──────┘
|
||
│
|
||
├──────────────────┬──────────────────┐
|
||
▼ ▼
|
||
┌─────────────┐ ┌─────────────┐
|
||
│ Console │ │ File │ ← 输出目标
|
||
│ (开发环境) │ │ (JSON格式) │
|
||
└─────────────┘ └─────────────┘
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ audit_logs │ ← 审计日志表
|
||
│ (PostgreSQL) │
|
||
└─────────────┘
|
||
```
|
||
|
||
### 1.2 现有组件
|
||
|
||
| 组件 | 文件路径 | 功能 |
|
||
|------|----------|------|
|
||
| `logging.py` | `backend/app/core/` | 日志配置、JSON 格式化、按日期/大小轮转 |
|
||
| `platform_store.py` | `backend/app/db/` | `record_audit()` 审计日志写入 |
|
||
| `002_governance.sql` | `backend/app/db/sql/` | `audit_logs` 表结构 |
|
||
|
||
### 1.3 存在的问题
|
||
|
||
| 问题 | 影响 | 严重程度 |
|
||
|------|------|----------|
|
||
| **无结构化日志分级** | DEBUG/INFO/WARNING/ERROR 全部混在一起,无法按级别过滤查看 | 🔴 高 |
|
||
| **无请求链路追踪** | 一个请求从进入到返回经过哪些服务/函数,无法串联 | 🔴 高 |
|
||
| **审计日志与业务耦合** | 各模块手动调用 `record_audit()`,容易遗漏 | 🟡 中 |
|
||
| **无敏感数据脱敏** | 用户 token、密码等可能明文记录 | 🔴 高 |
|
||
| **无日志聚合查询** | 无法按用户/时间范围/操作类型快速检索 | 🟡 中 |
|
||
| **无告警通知** | 系统异常无法主动推送通知 | 🟡 中 |
|
||
| **日志文件无归档策略** | 只有简单的过期删除,无压缩归档 | 🟢 低 |
|
||
|
||
---
|
||
|
||
## 二、设计目标
|
||
|
||
### 2.1 核心原则
|
||
|
||
1. **结构化** - 日志有固定 schema,便于机器解析和查询
|
||
2. **可追溯** - 每个请求有唯一 ID,可串联完整调用链路
|
||
3. **分级输出** - 不同环境输出不同级别,生产环境不输出 DEBUG
|
||
4. **安全合规** - 敏感数据自动脱敏(token、密码、手机号等)
|
||
5. **高性能** - 日志写入不影响业务接口性能(异步写入)
|
||
6. **可观测** - 支持快速检索、统计、告警
|
||
|
||
### 2.2 日志分级标准
|
||
|
||
| 级别 | 使用场景 | 示例 | 生产环境 |
|
||
|------|----------|------|:--------:|
|
||
| **DEBUG** | 开发调试 | 变量值、SQL 语句、完整堆栈 | ❌ 不输出 |
|
||
| **INFO** | 正常流程记录 | 任务创建成功、用户登录 | ✅ 记录 |
|
||
| **WARNING** | 可恢复异常 | 重试操作、参数校验失败、资源不足 | ✅ 记录 |
|
||
| **ERROR** | 需要人工介入 | 数据库连接失败、第三方 API 超时 | ✅ 记录 + 告警 |
|
||
| **CRITICAL** | 系统不可用 | 磁盘满、主节点宕机 | ✅ 记录 + 立即告警 |
|
||
|
||
---
|
||
|
||
## 三、技术方案
|
||
|
||
### 3.1 整体架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ 应用层 (Application Layer) │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||
│ │ 数据集管理 │ │ 微调训练 │ │ 模型推理 │ │ 用户认证 │ ... │
|
||
│ └─────┬────┘ └─────┬────┘ └─────┬────┘ └─────┬────┘ │
|
||
│ │ │ │ │ │
|
||
│ └────────────┴───────────┴──────────┘ │
|
||
│ ▼ │
|
||
│ ┌──────────────┐ │
|
||
│ │ Structured │ ← 结构化日志中间件 │
|
||
│ │ Logger │ │
|
||
│ └──────┬───────┘ │
|
||
│ │ │
|
||
│ ┌────────────┬────────────┬─────────────┐ │
|
||
│ ▼ ▼ ▼ │ │
|
||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
|
||
│ │ Console │ │ File │ │ 审计DB │ │ 告警 │ │
|
||
│ │ (开发) │ │ (JSON) │ │ (PG) │ │(可选) │ │
|
||
│ └──────────┘ └──────────┘ └──────────┘ └────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 可观测层 (Observability) │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||
│ │ Grafana │ │ Kibana │ │ PagerDuty │ ... │
|
||
│ │ (查询) │ │ (分析) │ │ (告警) │ │
|
||
│ └───────────┘ └───────────┘ └───────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.2 日志 Schema 设计
|
||
|
||
#### 3.2.1 应用日志 (app.log)
|
||
|
||
```json
|
||
{
|
||
"timestamp": "2026-08-17T10:30:00.000Z",
|
||
"level": "INFO",
|
||
"trace_id": "req-abc123",
|
||
"parent_span_id": "span-xyz789", // OpenTelemetry Span
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/dataset-manage",
|
||
"client_ip": "192.168.1.100",
|
||
"user_agent": "Mozilla/5.0...",
|
||
"user_id": "u_admin"
|
||
},
|
||
"module": "dataset.router",
|
||
"function": "create_dataset",
|
||
"message": "数据集创建成功",
|
||
"extra": {
|
||
"dataset_id": "ds_abc123",
|
||
"dataset_name": "训练数据"
|
||
},
|
||
"duration_ms": 125,
|
||
"status_code": 200,
|
||
"error": null
|
||
}
|
||
```
|
||
|
||
#### 3.2.2 审计日志 (audit_logs 表)
|
||
|
||
```sql
|
||
-- 已有表结构(保持不变)
|
||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||
id TEXT PRIMARY KEY,
|
||
tenant_id TEXT,
|
||
project_id TEXT,
|
||
actor_id TEXT, -- 操作人
|
||
action TEXT, -- 操作类型: create/delete/update/acl.set/login...
|
||
target_type TEXT, -- 资源类型: dataset/model/fine-tune/user...
|
||
target_id TEXT, -- 资源 ID
|
||
detail TEXT, -- 详细信息 JSON
|
||
client_ip TEXT, -- 客户端 IP
|
||
time TEXT, -- 操作时间
|
||
|
||
-- 新增字段
|
||
trace_id TEXT, -- 关联应用日志的请求追踪 ID
|
||
request_method TEXT, -- HTTP 方法
|
||
request_path TEXT, -- 请求路径
|
||
status_code INTEGER, -- 响应状态码
|
||
duration_ms REAL, -- 耗时(ms)
|
||
extra JSONB -- 扩展信息
|
||
);
|
||
|
||
-- 新增索引
|
||
CREATE INDEX IF NOT EXISTS idx_audit_trace ON audit_logs(trace_id);
|
||
CREATE INDEX IF NOT EXISTS idx_audit_actor_time ON audit_logs(actor_id, time);
|
||
```
|
||
|
||
### 3.3 日志中间件设计
|
||
|
||
```python
|
||
# backend/app/core/logging.py 新增
|
||
|
||
class StructuredLogger:
|
||
"""结构化日志记录器"""
|
||
|
||
def __init__(self, name: str):
|
||
self.logger = logging.getLogger(name)
|
||
self.trace_id = context_var.get("trace_id")
|
||
|
||
def info(self, msg: str, **kwargs):
|
||
self._log("INFO", msg, **kwargs)
|
||
|
||
def warning(self, msg: str, **kwargs):
|
||
self._log("WARNING", msg, **kwargs)
|
||
|
||
def error(self, msg: str, **kwargs):
|
||
self._log("ERROR", msg, **kwargs)
|
||
|
||
def _log(self, level: str, msg: str,
|
||
user_id: str = None,
|
||
target_type: str = None,
|
||
target_id: str = None,
|
||
duration_ms: float = None,
|
||
status_code: int = None,
|
||
error: Exception = None,
|
||
**extra):
|
||
"""统一日志记录方法"""
|
||
log_entry = {
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"level": level,
|
||
"trace_id": self.trace_id.get(),
|
||
"request": {
|
||
"user_id": user_id or current_user_id(),
|
||
"client_ip": client_ip(),
|
||
# ...
|
||
},
|
||
"module": calling_module,
|
||
"message": msg,
|
||
"target": {
|
||
"type": target_type,
|
||
"id": target_id,
|
||
},
|
||
"extra": extra,
|
||
"duration_ms": duration_ms,
|
||
"error": format_exception(error) if error else None,
|
||
}
|
||
|
||
# 1. 写入控制台/文件
|
||
self.logger.log(level, json.dumps(log_entry))
|
||
|
||
# 2. 异步写入审计表(如果需要)
|
||
if level in ("WARNING", "ERROR", "CRITICAL"):
|
||
async_write_audit(log_entry)
|
||
```
|
||
|
||
### 3.4 装饰器模式(推荐)
|
||
|
||
使用 Python 裁饰器自动记录,避免手动调用:
|
||
|
||
```python
|
||
# backend/app/core/log_decorator.py
|
||
|
||
def audit_log(action: str, target_type: str = ""):
|
||
"""审计日志装饰器"""
|
||
def decorator(func):
|
||
@wraps(func)
|
||
async def wrapper(*args, **kwargs):
|
||
result = await func(*args, **kwargs)
|
||
|
||
# 自动记录审计日志
|
||
record_audit(
|
||
action=action,
|
||
target_type=target_type,
|
||
target_id=kwargs.get('id') or result.get('id'),
|
||
detail=f"params={kwargs}"
|
||
)
|
||
return result
|
||
return wrapper
|
||
return decorator
|
||
|
||
|
||
# 使用示例
|
||
@audit_log("dataset.create", "dataset")
|
||
async def create_dataset(...):
|
||
# 业务逻辑
|
||
pass
|
||
```
|
||
|
||
### 3.5 敏感数据脱敏规则
|
||
|
||
```python
|
||
# backend/app/core/masking.py
|
||
|
||
SENSITIVE_FIELDS = {
|
||
"token": "***",
|
||
"password": "***",
|
||
"phone": lambda x: f"{x[:3]}****{x[-4:]}",
|
||
"email": lambda x: x[0] + "***" + x.split("@")[1] if "@" in x else "***",
|
||
"id_card": lambda x: f"{x[:6]}********{x[-4:]}",
|
||
}
|
||
|
||
def mask_sensitive(data: dict) -> dict:
|
||
"""递归脱敏字典中的敏感字段"""
|
||
for key, value in data.items():
|
||
if key in SENSITIVE_FIELDS:
|
||
data[key] = SENSITIVE_FIELDS[key](value) if callable(SENSITIVE_FIELDS[key]) else "***"
|
||
elif isinstance(value, dict):
|
||
mask_sensitive(value)
|
||
return data
|
||
```
|
||
|
||
---
|
||
|
||
## 四、实施计划
|
||
|
||
### 4.1 Phase 1:基础增强(1-2 天)
|
||
|
||
- [ ] **P1-1** 升级 `JsonLogFormatter`,增加 `trace_id` 字段
|
||
- [ ] **P1-2** 新增 `StructuredLogger` 封装类
|
||
- [ ] **P1-3** 统一所有模块的日志格式为 JSON
|
||
- [ ] **P1-4** 实现 `mask_sensitive()` 脱敏函数
|
||
- [ ] **P1-5** 审计日志表新增 `trace_id`、`duration_ms` 字段
|
||
|
||
### 4.2 Phase 2:自动化(2-3 天)
|
||
|
||
- [ ] **P2-1** 编写 `@audit_log` 装饰器
|
||
- [ ] **P2-2** 为关键业务接口添加装饰器:
|
||
- 数据集 CRUD
|
||
- 模型 CRUD
|
||
- 微调任务创建/删除
|
||
- 用户登录/登出
|
||
- ACL 授权变更
|
||
- [ ] **P2-3** 实现日志异步写入队列(避免影响性能)
|
||
|
||
### 4.3 Phase 3:可观测性(3-5 天)
|
||
|
||
- [ ] **P3-1** 集成 ELK Stack 或 Loki(可选)
|
||
- [ ] **P3-2** 编写 Grafana 仪表板:
|
||
- 请求量趋势图
|
||
- 错误率统计
|
||
- 慢接口 TOP10
|
||
- 用户操作审计面板
|
||
- [ ] [ ] **P3-3** 实现告警规则(错误率超阈值触发)
|
||
|
||
---
|
||
|
||
## 五、配置示例
|
||
|
||
### 5.1 日志配置 (settings)
|
||
|
||
```yaml
|
||
# config.yaml 或 .env
|
||
LOGGING:
|
||
level: INFO # 生产环境用 INFO,开发用 DEBUG
|
||
dir: ./logs
|
||
file_prefix: app
|
||
max_bytes: 50MB # 单文件最大 50MB
|
||
retention_days: 30 # 保留 30 天
|
||
error_prefix: error # 错误日志单独文件
|
||
json: true # JSON 格式输出
|
||
|
||
AUDIT:
|
||
enabled: true
|
||
auto_record: true # 是否自动记录(通过装饰器)
|
||
sensitive_mask: true # 启用敏感数据脱敏
|
||
```
|
||
|
||
### 5.2 日志输出示例
|
||
|
||
**控制台输出(开发环境):**
|
||
```
|
||
2026-08-17 18:30:00.123 | INFO | pid=12345 | MainThread | req=req-abc | dataset.router:create_dataset | dataset/router.py:45 | 数据集创建成功 {"dataset_id":"ds_abc"}
|
||
```
|
||
|
||
**文件输出(JSON 格式):**
|
||
```json
|
||
{"@timestamp":"2026-08-17T18:30:00.123Z","level":"INFO","logger":"dataset.router","message":"数据集创建成功","module":"dataset.router","function":"create_dataset","file":"dataset/router.py","line":45,"process":12345,"thread":"MainThread","request_id":"req-abc","extra":{"dataset_id":"ds_abc"}}
|
||
```
|
||
|
||
**审计日志查询 SQL:**
|
||
```sql
|
||
-- 查询某用户最近7天的所有操作
|
||
SELECT time, action, target_type, target_id, detail, client_ip
|
||
FROM audit_logs
|
||
WHERE actor_id = 'u_admin'
|
||
AND time >= now() - interval '7 days'
|
||
ORDER BY time DESC;
|
||
|
||
-- 查询某资源的授权变更历史
|
||
SELECT * FROM audit_logs
|
||
WHERE action LIKE '%acl%'
|
||
AND target_id = 'ds_abc123'
|
||
ORDER BY time DESC;
|
||
```
|
||
|
||
---
|
||
|
||
## 六、附录
|
||
|
||
### A. 日志关键字段说明
|
||
|
||
| 字段 | 类型 | 说明 | 示例 |
|
||
|------|------|------|------|
|
||
| `trace_id` | string | 请求唯一标识,用于串联一次请求的所有日志 | `req-uuid-1234` |
|
||
| `parent_span_id` | string | 父 Span ID(用于分布式追踪) | `span-parent-5678` |
|
||
| `actor_id` | string | 操作人用户 ID | `u_admin` |
|
||
| `action` | string | 操作动作 | `dataset.create`, `model.delete`, `login.success` |
|
||
| `target_type` | string | 操作的资源类型 | `dataset`, `trained_model`, `user` |
|
||
| `target_id` | string | 资源 ID | `ds_abc123` |
|
||
| `detail` | string/json | 操作详情 | `{"name": "训练数据", "type": "train"}` |
|
||
| `client_ip` | string | 客户端 IP | `192.168.1.100` |
|
||
| `duration_ms` | real | 接口耗时(ms) | `125.5` |
|
||
| `status_code` | int | HTTP 状态码 | `200`, `404`, `500` |
|
||
|
||
### B. 推荐的 Python 日志库对比
|
||
|
||
| 库 | 特点 | 适用场景 |
|
||
|-----|------|---------|
|
||
| `structlog` | 结构化日志,高性能 | 推荐 ✅ |
|
||
| `loguru` | 简单易用,自动配置 | 小型项目 |
|
||
| `logging` | Python 标准库 | 当前已使用 |
|
||
|
||
### C. 参考链接
|
||
|
||
- [Python logging cookbook](https://docs.python.org/3/howto/logging.html)
|
||
- [ELK Stack 官方文档](https://www.elastic.co/guide/index.html)
|
||
- [OpenTelemetry 规范](https://opentelemetry.io/docs/)
|