All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m16s
- 新增 Project 模型(repo_url, local_path, name, description) - 项目 CRUD API(GET/PUT /api/v1/projects) - 日志上报自动 upsert Project 记录 - ErrorLog 增加 failure_reason 字段 - update_task_status / create_repair_report 写入失败原因 - Repair Agent 优先从 API 获取项目配置,回退 .env - 新增 Web 端「项目管理」页面(表格 + 行内编辑) - BugList/BugDetail/RepairList 展示失败原因 - 更新接入指南文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
458 lines
12 KiB
Markdown
458 lines
12 KiB
Markdown
# Log Center 接入指南
|
||
|
||
## 概述
|
||
|
||
Log Center 是一个集中式错误日志收集与 AI 自动修复平台,提供 REST API 供各项目接入,实现运行时错误的统一收集、去重、追踪、分析和自动修复。
|
||
|
||
接入流程:
|
||
|
||
1. 项目首次上报日志时自动注册到 Log Center
|
||
2. 在 Web 管理端配置项目的**仓库地址**和**本地路径**
|
||
3. Repair Agent 根据配置自动拉取代码并修复 Bug
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 服务地址
|
||
|
||
| 环境 | API 地址 | 仪表盘 |
|
||
|------|----------|--------|
|
||
| 本地开发 | `http://localhost:8002` | `http://localhost:8003` |
|
||
| 生产环境 | `https://qiyuan-log-center-api.airlabs.art` | `https://qiyuan-log-center.airlabs.art` |
|
||
|
||
---
|
||
|
||
## API 接口
|
||
|
||
### 上报错误日志
|
||
|
||
**POST** `/api/v1/logs/report`
|
||
|
||
#### 请求体 (JSON)
|
||
|
||
```json
|
||
{
|
||
"project_id": "rtc_backend",
|
||
"environment": "production",
|
||
"level": "ERROR",
|
||
"timestamp": "2026-01-30T10:30:00Z",
|
||
"version": "1.2.3",
|
||
"commit_hash": "abc1234",
|
||
"repo_url": "https://gitea.example.com/team/rtc_backend.git",
|
||
"error": {
|
||
"type": "ValueError",
|
||
"message": "invalid literal for int() with base 10: 'abc'",
|
||
"file_path": "apps/users/views.py",
|
||
"line_number": 42,
|
||
"stack_trace": [
|
||
"Traceback (most recent call last):",
|
||
" File \"apps/users/views.py\", line 42, in get_user",
|
||
" user_id = int(request.GET['id'])",
|
||
"ValueError: invalid literal for int() with base 10: 'abc'"
|
||
]
|
||
},
|
||
"context": {
|
||
"url": "/api/users/123",
|
||
"method": "GET",
|
||
"user_id": "u_12345",
|
||
"request_id": "req_abc123"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 字段说明
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `project_id` | string | ✅ | 项目标识,如 `rtc_backend`, `rtc_web`, `airhub_app` |
|
||
| `environment` | string | ✅ | 环境:`development`, `staging`, `production` |
|
||
| `level` | string | ✅ | 日志级别:`ERROR`, `WARNING`, `CRITICAL` |
|
||
| `source` | string | ❌ | 来源:`runtime`(默认), `cicd`, `deployment` |
|
||
| `timestamp` | string | ❌ | ISO 8601 格式,不传则使用服务器时间 |
|
||
| `version` | string | ❌ | 应用版本号 |
|
||
| `commit_hash` | string | ❌ | Git commit hash |
|
||
| `repo_url` | string | ❌ | 项目仓库地址,首次上报时传入可自动关联到项目 |
|
||
| `error.type` | string | ✅ | 异常类型,如 `ValueError`, `TypeError` |
|
||
| `error.message` | string | ✅ | 错误消息 |
|
||
| `error.file_path` | string | ❌ | 出错文件路径(runtime 必填,cicd/deployment 可选) |
|
||
| `error.line_number` | int | ❌ | 出错行号(runtime 必填,cicd/deployment 可选) |
|
||
| `error.stack_trace` | array | ✅ | 堆栈信息(数组或字符串) |
|
||
| `context` | object | ❌ | 额外上下文信息(URL、用户ID等) |
|
||
|
||
> **项目自动注册**: 首次上报日志时,系统会根据 `project_id` 自动创建项目记录。如果同时传入 `repo_url`,会自动关联仓库地址,供 Repair Agent 使用。
|
||
|
||
#### 响应
|
||
|
||
**成功 (200)**
|
||
```json
|
||
{
|
||
"message": "Log reported",
|
||
"id": 123
|
||
}
|
||
```
|
||
|
||
**已存在 (200)** - 重复错误自动去重
|
||
```json
|
||
{
|
||
"message": "Log deduplicated",
|
||
"id": 123,
|
||
"status": "NEW"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 项目管理 API
|
||
|
||
项目在首次日志上报时自动创建,之后可通过 API 或 Web 管理端编辑配置。
|
||
|
||
#### 获取项目列表
|
||
|
||
**GET** `/api/v1/projects`
|
||
|
||
```json
|
||
{
|
||
"projects": [
|
||
{
|
||
"id": 1,
|
||
"project_id": "rtc_backend",
|
||
"name": "RTC 后端",
|
||
"repo_url": "https://gitea.example.com/team/rtc_backend.git",
|
||
"local_path": "/home/dev/projects/rtc_backend",
|
||
"description": "Django 后端服务",
|
||
"created_at": "2026-01-15T08:00:00",
|
||
"updated_at": "2026-02-20T10:30:00"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 获取项目详情
|
||
|
||
**GET** `/api/v1/projects/{project_id}`
|
||
|
||
返回单个项目的完整信息。
|
||
|
||
#### 编辑项目配置
|
||
|
||
**PUT** `/api/v1/projects/{project_id}`
|
||
|
||
```json
|
||
{
|
||
"name": "RTC 后端",
|
||
"repo_url": "https://gitea.example.com/team/rtc_backend.git",
|
||
"local_path": "/home/dev/projects/rtc_backend",
|
||
"description": "Django 后端服务"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `name` | string | 项目显示名称 |
|
||
| `repo_url` | string | Git 仓库地址(Repair Agent 克隆/推送代码用) |
|
||
| `local_path` | string | 本地项目路径(Repair Agent 在此目录执行修复) |
|
||
| `description` | string | 项目描述 |
|
||
|
||
> **注意**: `repo_url` 和 `local_path` 是 Repair Agent 正常工作的关键配置。未配置时 Agent 将无法执行 Git 操作或定位项目代码。可在 Web 管理端的「项目管理」页面中配置。
|
||
|
||
---
|
||
|
||
## 接入示例
|
||
|
||
### Python (Django / FastAPI)
|
||
|
||
```python
|
||
import requests
|
||
import traceback
|
||
import os
|
||
|
||
LOG_CENTER_URL = os.getenv("LOG_CENTER_URL", "http://localhost:8002")
|
||
|
||
def report_error(exc, context=None):
|
||
"""上报错误到 Log Center"""
|
||
tb = traceback.extract_tb(exc.__traceback__)
|
||
last_frame = tb[-1] if tb else None
|
||
|
||
payload = {
|
||
"project_id": "rtc_backend",
|
||
"environment": os.getenv("ENVIRONMENT", "development"),
|
||
"level": "ERROR",
|
||
"repo_url": os.getenv("REPO_URL", ""), # 可选:关联仓库地址
|
||
"error": {
|
||
"type": type(exc).__name__,
|
||
"message": str(exc),
|
||
"file_path": last_frame.filename if last_frame else "unknown",
|
||
"line_number": last_frame.lineno if last_frame else 0,
|
||
"stack_trace": traceback.format_exception(exc)
|
||
},
|
||
"context": context or {}
|
||
}
|
||
|
||
try:
|
||
requests.post(
|
||
f"{LOG_CENTER_URL}/api/v1/logs/report",
|
||
json=payload,
|
||
timeout=3 # 快速失败,不影响主业务
|
||
)
|
||
except Exception:
|
||
pass # 静默失败,不影响主业务
|
||
```
|
||
|
||
#### Django 集成位置
|
||
|
||
修改 `utils/exceptions.py` 的 `custom_exception_handler`:
|
||
|
||
```python
|
||
def custom_exception_handler(exc, context):
|
||
# 上报到 Log Center (异步,不阻塞响应)
|
||
report_error(exc, {
|
||
"view": str(context.get("view")),
|
||
"request_path": context.get("request").path if context.get("request") else None,
|
||
})
|
||
|
||
# ... 原有逻辑不变 ...
|
||
```
|
||
|
||
---
|
||
|
||
### JavaScript / TypeScript (React / Vue)
|
||
|
||
```typescript
|
||
const LOG_CENTER_URL = import.meta.env.VITE_LOG_CENTER_URL || 'http://localhost:8002';
|
||
|
||
interface ErrorPayload {
|
||
project_id: string;
|
||
environment: string;
|
||
level: string;
|
||
repo_url?: string;
|
||
error: {
|
||
type: string;
|
||
message: string;
|
||
file_path: string;
|
||
line_number: number;
|
||
stack_trace: string[];
|
||
};
|
||
context?: Record<string, unknown>;
|
||
}
|
||
|
||
export function reportError(error: Error, context?: Record<string, unknown>) {
|
||
// 解析堆栈信息
|
||
const stackLines = error.stack?.split('\n') || [];
|
||
const match = stackLines[1]?.match(/at\s+.*\s+\((.+):(\d+):\d+\)/);
|
||
|
||
const payload: ErrorPayload = {
|
||
project_id: 'rtc_web',
|
||
environment: import.meta.env.MODE,
|
||
level: 'ERROR',
|
||
error: {
|
||
type: error.name,
|
||
message: error.message,
|
||
file_path: match?.[1] || 'unknown',
|
||
line_number: parseInt(match?.[2] || '0'),
|
||
stack_trace: stackLines,
|
||
},
|
||
context: {
|
||
url: window.location.href,
|
||
userAgent: navigator.userAgent,
|
||
...context,
|
||
},
|
||
};
|
||
|
||
// 使用 sendBeacon 确保页面关闭时也能发送
|
||
if (navigator.sendBeacon) {
|
||
navigator.sendBeacon(
|
||
`${LOG_CENTER_URL}/api/v1/logs/report`,
|
||
JSON.stringify(payload)
|
||
);
|
||
} else {
|
||
fetch(`${LOG_CENTER_URL}/api/v1/logs/report`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
keepalive: true,
|
||
}).catch(() => {});
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Axios 拦截器集成
|
||
|
||
修改 `src/api/request.ts`:
|
||
|
||
```typescript
|
||
request.interceptors.response.use(
|
||
(response) => { /* ... */ },
|
||
(error: AxiosError) => {
|
||
// 上报到 Log Center
|
||
reportError(error, {
|
||
url: error.config?.url,
|
||
method: error.config?.method,
|
||
status: error.response?.status,
|
||
});
|
||
|
||
// ... 原有逻辑不变 ...
|
||
}
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### Flutter (Dart)
|
||
|
||
```dart
|
||
import 'dart:convert';
|
||
import 'package:http/http.dart' as http;
|
||
|
||
const logCenterUrl = String.fromEnvironment(
|
||
'LOG_CENTER_URL',
|
||
defaultValue: 'http://localhost:8002',
|
||
);
|
||
|
||
Future<void> reportError(dynamic error, StackTrace stackTrace, {Map<String, dynamic>? context}) async {
|
||
final stackLines = stackTrace.toString().split('\n');
|
||
// 解析第一行获取文件和行号
|
||
final match = RegExp(r'#0\s+.*\((.+):(\d+):\d+\)').firstMatch(stackLines.first);
|
||
|
||
final payload = {
|
||
'project_id': 'airhub_app',
|
||
'environment': const String.fromEnvironment('ENVIRONMENT', defaultValue: 'development'),
|
||
'level': 'ERROR',
|
||
'repo_url': 'https://gitea.example.com/team/airhub_app.git',
|
||
'error': {
|
||
'type': error.runtimeType.toString(),
|
||
'message': error.toString(),
|
||
'file_path': match?.group(1) ?? 'unknown',
|
||
'line_number': int.tryParse(match?.group(2) ?? '0') ?? 0,
|
||
'stack_trace': stackLines.take(20).toList(),
|
||
},
|
||
'context': context ?? {},
|
||
};
|
||
|
||
try {
|
||
await http.post(
|
||
Uri.parse('$logCenterUrl/api/v1/logs/report'),
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: jsonEncode(payload),
|
||
).timeout(const Duration(seconds: 3));
|
||
} catch (_) {
|
||
// 静默失败
|
||
}
|
||
}
|
||
```
|
||
|
||
在 `main.dart` 中全局捕获:
|
||
|
||
```dart
|
||
void main() {
|
||
FlutterError.onError = (details) {
|
||
reportError(details.exception, details.stack ?? StackTrace.current);
|
||
};
|
||
|
||
runZonedGuarded(() {
|
||
runApp(const MyApp());
|
||
}, (error, stack) {
|
||
reportError(error, stack);
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 错误去重机制
|
||
|
||
Log Center 使用 **指纹(fingerprint)** 对错误进行去重,按来源使用不同的指纹策略:
|
||
|
||
| 来源 | 指纹组成 |
|
||
|------|----------|
|
||
| `runtime` | `MD5(project_id \| error_type \| file_path \| line_number)` |
|
||
| `cicd` | `MD5(project_id \| cicd \| error_type \| job_name \| step_name)` |
|
||
| `deployment` | `MD5(project_id \| deployment \| error_type \| namespace \| deployment_name)` |
|
||
|
||
相同指纹的错误只会记录一次。如果已修复的错误再次出现,系统会自动重新打开(回归检测)。
|
||
|
||
---
|
||
|
||
## 错误状态流转
|
||
|
||
```
|
||
NEW → VERIFYING → PENDING_FIX → FIXING → FIXED → VERIFIED → DEPLOYED
|
||
↓ ↓
|
||
CANNOT_REPRODUCE FIX_FAILED
|
||
```
|
||
|
||
| 状态 | 说明 |
|
||
|------|------|
|
||
| `NEW` | 新上报的错误 |
|
||
| `VERIFYING` | 正在验证复现 |
|
||
| `CANNOT_REPRODUCE` | 无法复现 |
|
||
| `PENDING_FIX` | 等待修复 |
|
||
| `FIXING` | AI Agent 正在修复中 |
|
||
| `FIXED` | 已修复,待验证 |
|
||
| `VERIFIED` | 已验证修复 |
|
||
| `DEPLOYED` | 已部署上线 |
|
||
| `FIX_FAILED` | 修复失败(失败原因会记录到数据库并在 Web 端展示) |
|
||
|
||
---
|
||
|
||
## Web 管理端
|
||
|
||
### 项目管理
|
||
|
||
访问 Web 管理端的「项目管理」页面,可以:
|
||
|
||
- 查看所有已注册项目及其配置状态
|
||
- 编辑项目的**仓库地址**(`repo_url`)和**本地路径**(`local_path`)
|
||
- 未配置的字段会标红提示
|
||
|
||
> Repair Agent 依赖这两个配置来定位项目代码和执行 Git 操作。请确保在接入后及时配置。
|
||
|
||
### 缺陷追踪
|
||
|
||
- **缺陷列表**: 按项目、来源、状态筛选,修复失败的缺陷会直接显示失败原因
|
||
- **缺陷详情**: 查看完整错误信息、堆栈、上下文,以及修复历史记录
|
||
- **修复报告**: 查看每轮 AI 修复的详细过程(分析、代码变更、测试结果、失败原因)
|
||
|
||
---
|
||
|
||
## 最佳实践
|
||
|
||
1. **首次接入时传入 `repo_url`**: 在日志上报中包含仓库地址,省去手动配置步骤
|
||
2. **设置超时**: 上报请求设置 3 秒超时,避免影响主业务
|
||
3. **静默失败**: 上报失败不应影响用户体验
|
||
4. **异步上报**: 使用异步方式上报,不阻塞主流程
|
||
5. **添加上下文**: 尽量添加有用的上下文信息(用户ID、请求URL等)
|
||
6. **环境区分**: 正确设置 `environment` 字段区分开发/生产
|
||
7. **配置本地路径**: 接入后在 Web 端配置 `local_path`,使 Repair Agent 能正确定位代码
|
||
|
||
---
|
||
|
||
## 环境变量配置
|
||
|
||
### Python 项目
|
||
```bash
|
||
# .env
|
||
LOG_CENTER_URL=http://localhost:8002
|
||
ENVIRONMENT=development
|
||
REPO_URL=https://gitea.example.com/team/rtc_backend.git # 可选
|
||
```
|
||
|
||
### JavaScript 项目
|
||
```bash
|
||
# .env
|
||
VITE_LOG_CENTER_URL=http://localhost:8002
|
||
```
|
||
|
||
### Flutter 项目
|
||
```bash
|
||
# 编译时传入
|
||
flutter run --dart-define=LOG_CENTER_URL=http://localhost:8002
|
||
flutter run --dart-define=ENVIRONMENT=development
|
||
```
|
||
|
||
---
|
||
|
||
## API 文档
|
||
|
||
完整 API 文档请访问: [http://localhost:8002/docs](http://localhost:8002/docs)
|