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

12 KiB
Raw Blame History

服务端接入层设计文档

一、设计原则

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-envelopeapplication/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 可使用简化格式:

{
  "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 认证中间件

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 错误指纹计算

用于错误去重和聚合:

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 接口,批量写入:

{
  "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不推荐太重了