light-sentry-sdk/docs/02-api-server.md

493 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 服务端接入层设计文档
## 一、设计原则
### 1.1 核心原则
- **高并发**:单节点支撑 1000+ QPS 事件上报
- **低延迟**:接口响应 < 50ms先入队再处理
- **高可用**进程崩溃不丢数据本地持久化队列
- **可扩展**支持横向扩展无状态设计
- **兼容性**完全兼容 Sentry 协议SDK 可无缝切换
### 1.2 性能指标
| 指标 | 目标值 | 说明 |
|------|--------|------|
| 单节点 QPS | 1000+ | 4C8G 服务器 |
| P99 响应时间 | < 50ms | 99% 请求在 50ms 内返回 |
| 数据丢失率 | < 0.01% | 异常情况下的数据丢失率 |
| 单事件处理耗时 | < 1ms | 从入队到写入存储 |
---
## 二、API 接口设计
### 2.1 接口列表
| 端点 | 方法 | 功能 | 优先级 |
|------|------|------|--------|
| `/api/{projectId}/envelope/` | POST | Envelope 批量上报 | 最高 |
| `/api/{projectId}/store/` | POST | 单事件上报 | |
| `/api/{projectId}/nel/` | POST | NEL 网络错误上报 | |
| `/api/{projectId}/health` | GET | 健康检查 | - |
| `/api/projects/` | GET/POST | 项目管理 | - |
| `/api/projects/{id}` | GET/PUT/DELETE | 项目 CRUD | - |
### 2.2 Envelope 接口(主要上报接口)
#### URL 格式
```
POST /api/{projectId}/envelope/?sentry_key={publicKey}&sentry_version=7
```
#### 请求头
| Header | 说明 | 必填 |
|--------|------|------|
| `Content-Type` | `application/x-sentry-envelope` `application/json` | |
| `X-Sentry-Auth` | Sentry 认证头备用方式 | |
#### 认证方式(二选一)
**方式 1URL 参数(推荐,兼容性好)**
```
?sentry_key={publicKey}&sentry_version=7
```
**方式 2Header 方式**
```
X-Sentry-Auth: Sentry sentry_version=7, sentry_key={publicKey}
```
#### Envelope 格式解析
标准 Envelope 格式
```
# 第 1 行envelope headerJSON
{"event_id":"abc123","sent_at":"2024-01-01T00:00:00Z"}
# 第 2 行item headerJSON
{"type":"event","length":123}
# 第 3 行item payloadJSON长度 = length
{"level":"error","message":"test",...}
# 第 4 行:下一个 item header
{"type":"transaction","length":456}
# ...
```
支持的 item 类型
| type | 说明 | 处理方式 |
|------|------|----------|
| `event` | 错误事件 | 完整处理 + 写入 Loki + 聚合 |
| `transaction` | 性能事务 | 提取关键指标 + 聚合 |
| `session` | 会话 | 计数 + 聚合 |
| `attachment` | 附件 | 忽略轻量版不支持 |
| `profile` | 性能剖析 | 忽略 |
| `statsd` | 客户端统计 | 忽略 |
| `user_report` | 用户反馈 | 存储 + 计数 |
#### 简化格式(自研 SDK 专用)
为了减少解析开销自研 SDK 可使用简化格式
```json
{
"events": [
{
"type": "error",
"data": { ... }
},
{
"type": "performance",
"data": { ... }
}
]
}
```
Content-Type: `application/json`
### 2.3 Store 接口(兼容旧版 SDK
```
POST /api/{projectId}/store/?sentry_key={publicKey}
Content-Type: application/json
{
"event_id": "abc123",
"level": "error",
"message": "...",
"exception": { ... }
}
```
### 2.4 NEL 接口Network Error Logging
```
POST /api/{projectId}/nel/
Content-Type: application/reports+json
[
{
"age": 123,
"type": "network-error",
"url": "https://example.com/",
"body": {
"sampling_fraction": 1.0,
"server_ip": "1.2.3.4",
"protocol": "h2",
"method": "GET",
"status_code": 0,
"elapsed_time": 123,
"type": "dns.failed"
}
}
]
```
---
## 三、处理流程
### 3.1 整体流程
```
HTTP 请求到达
Nginx (限流 + CORS + 日志)
1. 认证中间件
├─ 解析 projectId (URL)
├─ 解析 publicKey (query / header)
└─ 校验项目是否存在
2. 请求体解析
├─ 根据 Content-Type 选择解析器
├─ envelope 格式 → 逐行解析
└─ json 格式 → JSON.parse
3. 数据清洗
├─ 字段校验
├─ 敏感数据脱敏
├─ 计算错误指纹
└─ 补充默认字段
4. 写入内存队列(立即返回响应)
5. 异步批量消费
├─ 写入 Loki原始日志
├─ 更新 MySQL 聚合表
└─ 触发告警检测
```
### 3.2 认证中间件
```javascript
async function sentryAuth(req, res, next) {
// 1. 从 URL 中提取 projectId
const projectId = req.params.projectId;
// 2. 从 URL 参数或 Header 获取 publicKey
const publicKey = req.query.sentry_key ||
parseSentryAuthHeader(req.headers['x-sentry-auth']);
// 3. 校验项目是否存在
const project = projectStore.findByProjectId(projectId);
if (!project) {
return res.status(404).json({ detail: 'Project not found' });
}
// 4. 校验 publicKey
if (project.publicKey !== publicKey) {
return res.status(401).json({ detail: 'Invalid public key' });
}
// 5. 挂载到请求对象
req.project = project;
next();
}
```
### 3.3 中间件顺序(重要)
```
express.raw() ← Sentry 路由先挂载,避免 express.json() 干扰
sentryAuth ← DSN 认证
envelopeParser ← 解析 envelope 格式
rateLimit ← 限流
handler ← 业务处理
express.json() ← 其他 API 路由
其他业务中间件
```
**关键设计**Sentry 上报接口必须在 `express.json()` 之前挂载否则 raw body 会被解析成 JSON导致 envelope 格式解析失败
---
## 四、内存队列设计
### 4.1 队列结构
```
┌─────────────────────────────────────────┐
│ 内存队列 (Array) │
│ [ evt1, evt2, evt3, ..., evtN ] │
│ ↑ ↑ │
│ head(tail) maxSize │
└─────────────────────────────────────────┘
┌─────────────────────┐
│ 批量消费定时器 │
│ 每 1s 或满 100 条 │
└─────────┬───────────┘
┌─────────────────────┐
│ 批量写入 Loki │
│ 批量更新 MySQL │
│ 告警检测 │
└─────────────────────┘
```
### 4.2 队列参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `maxQueueSize` | 10000 | 队列最大长度 |
| `flushInterval` | 1000ms | 定时刷间隔 |
| `batchSize` | 100 | 每批处理数量 |
| `persistInterval` | 5000ms | 持久化间隔防止丢数据 |
### 4.3 队列满时的策略
1. **新事件丢弃最旧的**FIFO保证新事件优先
2. **采样降级**队列 > 80% 时,非错误事件自动降采样
3. **快速失败**:队列 > 95% 时,直接返回 429
4. **本地持久化**:优雅关闭时刷入本地文件
### 4.4 本地持久化(可选)
防止进程崩溃丢数据:
- 使用 LevelDB / SQLite 作为持久化队列
- 入队时先写磁盘,再读内存
- 启动时从磁盘恢复未处理的事件
- 性能影响QPS 从 1000+ 降到 ~500
---
## 五、数据清洗与脱敏
### 5.1 字段校验
| 字段 | 校验规则 | 不通过处理 |
|------|----------|-----------|
| `event_id` | 32 位 hex可选 | 自动生成 |
| `timestamp` | ISO 时间戳,可选 | 用服务器时间 |
| `message` | 长度 < 8KB | 截断 |
| `exception.stacktrace` | 深度 < 50 | 截断 |
| `breadcrumbs` | 数量 < 100 | 截断 |
| `extra` | 大小 < 16KB | 截断 |
| 总大小 | < 256KB | 拒绝 |
### 5.2 敏感数据脱敏
自动脱敏的字段
| 字段名不区分大小写 | 脱敏方式 |
|----------------------|----------|
| `password`, `passwd`, `pwd` | `***` |
| `token`, `access_token`, `refresh_token` | 4 + `***` |
| `secret`, `api_key`, `apikey` | `***` |
| `email` | `a***@b.com` |
| `phone`, `mobile` | `138****1234` |
| `id_card`, `idcard` | `110***********1234` |
| `credit_card`, `card_no` | `6222**********1234` |
| IP 地址 | 保留前两段192.168.x.x |
### 5.3 错误指纹计算
用于错误去重和聚合
```javascript
function computeFingerprint(event) {
const { exception } = event;
if (!exception) {
return md5(event.message || 'unknown');
}
// 提取关键信息
const type = exception.type || 'Error';
const message = normalizeMessage(exception.value); // 脱敏 + 去变量
const frames = exception.stacktrace?.frames || [];
// 取前 3 个 in_app 栈帧
const keyFrames = frames
.filter(f => f.in_app !== false)
.slice(0, 3)
.map(f => `${f.filename}:${f.lineno}`);
return md5(`${type}:${message}:${keyFrames.join('|')}`);
}
```
---
## 六、限流设计
### 6.1 限流维度
| 维度 | 默认限制 | 说明 |
|------|----------|------|
| 每项目每秒 | 100 | 项目级 QPS 限制 |
| 每项目每天 | 100000 | 项目级日配额 |
| IP 每秒 | 50 | IP QPS 限制 |
| 全局限流 | 1000 QPS | 服务器总 QPS |
### 6.2 限流算法
- **令牌桶算法**QPS 限流用令牌桶
- **滑动窗口**日配额用滑动窗口
- **内存计数**单节点足够分布式需 Redis
### 6.3 超限处理
| 超出比例 | 处理方式 |
|----------|----------|
| < 80% | 正常处理 |
| 80% - 100% | 非错误事件降采样 50% |
| 100% - 150% | 只保留错误事件其余丢弃 |
| > 150% | 全部丢弃,返回 429 |
---
## 七、批量消费设计
### 7.1 消费流程
```
批次事件
按项目分组
按事件类型分组 (error / performance / network / ...)
并行处理
├─ error 类型
│ ├─ 写入 Loki批量
│ ├─ 更新错误聚合表(按指纹分组计数)
│ └─ 触发告警检测
├─ performance 类型
│ ├─ 写入 Loki
│ └─ 更新性能指标表P50/P95/P99
└─ 其他类型
└─ 写入 Loki
```
### 7.2 Loki 批量写入
使用 Loki 的 `/loki/api/v1/push` 接口,批量写入:
```json
{
"streams": [
{
"stream": {
"project_id": "1001",
"level": "error",
"type": "error"
},
"values": [
["<ts_nano>", "<json_line>"],
["<ts_nano>", "<json_line>"]
]
}
]
}
```
### 7.3 MySQL 批量更新
- 使用 `INSERT ... ON DUPLICATE KEY UPDATE`
- 按批次聚合后一次性写入
- 避免逐条更新,提升性能
---
## 八、错误码设计
| HTTP 状态码 | 说明 | 场景 |
|-------------|------|------|
| 200 | 成功 | 正常接收 |
| 204 | 成功无内容 | 同上,兼容不同 SDK |
| 400 | 请求格式错误 | envelope 格式不对、body 为空 |
| 401 | 认证失败 | publicKey 错误 |
| 404 | 项目不存在 | projectId 无效 |
| 413 | 请求体过大 | 超过 256KB |
| 429 | 限流 | 超过配额 |
| 500 | 服务器错误 | 内部异常 |
---
## 九、可观测性
### 9.1 自身监控指标
| 指标 | 说明 |
|------|------|
| `events_received_total` | 接收事件总数 |
| `events_received_per_second` | 每秒接收数 |
| `events_dropped_total` | 丢弃事件总数 |
| `events_processed_total` | 处理成功总数 |
| `queue_size` | 当前队列长度 |
| `process_duration_ms` | 处理耗时P50/P95/P99 |
| `loki_write_errors_total` | Loki 写入错误数 |
| `mysql_write_errors_total` | MySQL 写入错误数 |
### 9.2 健康检查接口
```
GET /health
{
"status": "ok",
"timestamp": "2024-01-01T00:00:00Z",
"uptime": 86400,
"queue_size": 123,
"events_processed": 1234567,
"loki": "connected",
"mysql": "connected"
}
```
---
## 十、横向扩展
### 10.1 无状态设计
- API Server 完全无状态
- 项目配置缓存,启动时加载,定时刷新
- 队列在内存中,扩展时直接加机器
### 10.2 负载均衡
- Nginx 层做负载均衡
- 按 projectId 一致性哈希路由
- 同一项目的事件打到同一台机器(有利于缓存和聚合)
### 10.3 队列扩展
- 小流量:内存队列足够
- 中流量:加 Redis 作为分布式队列
- 大流量:加 Kafka不推荐太重了