light-sentry-sdk/docs/03-data-aggregation.md

14 KiB
Raw Blame History

数据聚合与存储设计文档

一、设计原则

1.1 存储分层策略

热数据7天内  ←→  Loki + MySQL高频查询
温数据7-30天 ←→  Loki低频查询
冷数据30天+  ←→  对象存储归档(极少查询)

1.2 存储选型

数据类型 存储 保留时长 说明
原始错误事件 Loki 7-30 天 堆栈、上下文、面包屑
原始性能事件 Loki 7 天 性能明细数据
原始行为事件 Loki 3 天 PV、点击等量大
错误聚合统计 MySQL 永久 按指纹 + 时间聚合
性能指标统计 MySQL 90 天 按指标 + 时间聚合
项目配置 MySQL 永久 项目、用户、告警规则
告警记录 MySQL 90 天 告警历史

1.3 设计目标

  • Loki 存储成本100万错误事件 ≈ 5GB/月
  • MySQL 存储成本:聚合数据 ≈ 100MB/月/项目
  • 查询性能:聚合查询 < 100ms原始日志查询 < 2s

二、Loki 存储设计

2.1 Label 设计

Loki 的 Label 是查询索引,设计原则:低基数、高区分度

Label 说明 基数 示例
project_id 项目 ID 低(项目数) 1001
type 事件类型 极低 error / performance / network / behavior
level 日志级别 极低 fatal / error / warning / info
platform 平台 javascript / node / python
environment 环境 production / staging / development
fingerprint 错误指纹 abc123def

反模式(不要用做 Label

  • event_id:基数太高,每个事件都不同
  • message:基数太高,且是文本
  • url:基数太高
  • user_id:基数太高

这些应该放在日志内容里,用 grep 查询。

2.2 日志格式JSON

每条日志是一行 JSON方便 Loki 的 json 解析器提取字段。

错误事件格式

{
  "event_id": "abc123...",
  "type": "error",
  "level": "error",
  "timestamp": 1704067200000,
  "project_id": "1001",
  "platform": "javascript",
  "environment": "production",
  "release": "1.0.0",
  "fingerprint": "a1b2c3d4e5f6",
  
  "message": "Cannot read property 'foo' of undefined",
  "exception": {
    "type": "TypeError",
    "value": "Cannot read property 'foo' of undefined",
    "stacktrace": {
      "frames": [
        {
          "filename": "https://example.com/app.js",
          "function": "onClick",
          "lineno": 123,
          "colno": 45,
          "in_app": true
        }
      ]
    }
  },
  
  "user": {
    "id": "123",
    "username": "testuser"
  },
  
  "tags": {
    "page": "/home",
    "browser": "Chrome 120"
  },
  
  "extra": {},
  "breadcrumbs": [],
  
  "request": {
    "url": "https://example.com/page",
    "headers": {
      "user_agent": "Mozilla/5.0..."
    }
  }
}

性能事件格式

{
  "type": "performance",
  "level": "info",
  "timestamp": 1704067200000,
  "project_id": "1001",
  
  "metric": "lcp",
  "value": 2500,
  "unit": "ms",
  "rating": "good",
  
  "tags": {
    "page_url": "https://example.com/page",
    "route": "/home",
    "browser": "Chrome 120",
    "os": "Mac OS X"
  }
}

网络事件格式

{
  "type": "network",
  "level": "info",
  "timestamp": 1704067200000,
  "project_id": "1001",
  
  "sub_type": "fetch",
  "method": "GET",
  "url": "/api/users",
  "status_code": 200,
  "duration": 123,
  "success": true,
  
  "tags": {
    "route": "/home"
  }
}

2.3 Loki 查询示例

查询某项目的错误总数1小时内

count_over_time(
  {project_id="1001", type="error"}[1h]
)

查询某错误指纹的出现次数

count_over_time(
  {project_id="1001", fingerprint="a1b2c3d4"}[24h]
)

查询某页面的 LCP P95

quantile_over_time(
  0.95,
  {project_id="1001", type="performance"}
  | json value=value
  | metric="lcp"
  | unwrap value
  [5m]
)

三、MySQL 聚合表设计

3.1 错误聚合表

error_stats_hourly - 错误小时统计表

按错误指纹 + 项目 + 小时聚合,用于趋势图。

字段 类型 说明
id BIGINT PK 主键
project_id VARCHAR(64) 项目 ID
fingerprint VARCHAR(64) 错误指纹
stat_hour DATETIME 统计小时(整点)
error_type VARCHAR(128) 错误类型TypeError 等)
error_message VARCHAR(512) 错误消息(截断)
count INT 发生次数
affected_users INT 影响用户数(估算)
first_seen DATETIME 首次出现时间
last_seen DATETIME 最后出现时间
created_at DATETIME 创建时间
updated_at DATETIME 更新时间

索引

  • (project_id, stat_hour) - 按项目+时间查询
  • (project_id, fingerprint, stat_hour) - 按指纹+时间查询
  • 唯一键:(project_id, fingerprint, stat_hour)

error_issues - 错误 Issue 表

按错误指纹聚合,用于错误列表管理。

字段 类型 说明
id BIGINT PK 主键
project_id VARCHAR(64) 项目 ID
fingerprint VARCHAR(64) UNIQUE 错误指纹
error_type VARCHAR(128) 错误类型
error_message TEXT 错误消息
stack_trace TEXT 堆栈摘要(前几帧)
level VARCHAR(32) 级别
platform VARCHAR(32) 平台
total_count BIGINT 总次数
today_count INT 今日次数
yesterday_count INT 昨日次数
affected_users INT 影响用户数
status VARCHAR(32) 状态active / resolved / ignored
assignee VARCHAR(64) 处理人
first_seen DATETIME 首次出现
last_seen DATETIME 最后出现
created_at DATETIME 创建时间
updated_at DATETIME 更新时间

状态流转

active ──→ resolved ──→ active再次出现时复活
  │
  └──→ ignored

3.2 性能统计表

performance_stats_hourly - 性能小时统计表

字段 类型 说明
id BIGINT PK 主键
project_id VARCHAR(64) 项目 ID
metric VARCHAR(32) 指标名lcp / fcp / cls / fid
stat_hour DATETIME 统计小时
page_url VARCHAR(256) 页面 URL可选空=全部)
sample_count INT 样本数
p50 DOUBLE 中位数
p75 DOUBLE 75 分位
p90 DOUBLE 90 分位
p95 DOUBLE 95 分位
p99 DOUBLE 99 分位
avg DOUBLE 平均值
good_rate DOUBLE Good 比例0-1
poor_rate DOUBLE Poor 比例0-1
created_at DATETIME 创建时间
updated_at DATETIME 更新时间

索引

  • (project_id, metric, stat_hour) - 主查询索引
  • 唯一键:(project_id, metric, stat_hour, page_url)

performance_stats_daily - 性能日统计表

同上,按天聚合,用于长期趋势。

3.3 网络请求统计表

network_stats_hourly - 网络小时统计表

字段 类型 说明
id BIGINT PK 主键
project_id VARCHAR(64) 项目 ID
stat_hour DATETIME 统计小时
method VARCHAR(16) 请求方法
url_pattern VARCHAR(512) URL 模式(归一化后)
total_count INT 总请求数
error_count INT 错误数4xx + 5xx
error_rate DOUBLE 错误率
avg_duration DOUBLE 平均耗时(ms)
p50_duration DOUBLE P50 耗时
p95_duration DOUBLE P95 耗时
created_at DATETIME 创建时间

URL 归一化

  • /api/users/123/api/users/:id
  • /static/app.abc123.js/static/app.[hash].js

3.4 环境维度表

用于减少 Loki 中环境信息的冗余,配合 SDK 的字典编码和会话级共享使用。

env_dimensions - 环境维度表

每条唯一的环境组合只存 1 条,日志中通过 env_id 关联。

字段 类型 说明
id BIGINT PK 维度 ID
project_id VARCHAR(64) 项目 ID
browser_name VARCHAR(32) 浏览器名称
browser_version VARCHAR(32) 浏览器主版本
os_name VARCHAR(32) 操作系统
os_version VARCHAR(32) OS 主版本
device_family VARCHAR(64) 设备系列
device_model VARCHAR(64) 设备型号
screen_resolution VARCHAR(32) 屏幕分辨率
language VARCHAR(16) 浏览器语言
hash VARCHAR(64) UNIQUE 所有维度的哈希值,用于快速查找
first_seen DATETIME 首次出现
last_seen DATETIME 最后出现
count BIGINT 出现次数(用于热度排序)

唯一键(project_id, hash)

为什么用维度表?

  • Loki 的 JSON 日志里只存 env_id1 个数字),不存完整的浏览器/OS/设备信息
  • 每条日志节省 100-200 字节,百万级事件节省几十 GB
  • 查询时 JOIN 维度表,或者直接用 Grafana 的变量查询

服务端处理流程

事件到达 → 计算环境维度的 hash → 
  ├─ 已存在 → 取 env_id更新 last_seen + count
  └─ 不存在 → 插入新记录,返回新 env_id
→ 日志写入 Loki只带 env_id

page_dimensions - 页面维度表

同理,页面 URL 也可以做维度化:

字段 类型 说明
id BIGINT PK 页面 ID
project_id VARCHAR(64) 项目 ID
url_pattern VARCHAR(512) 归一化后的 URL 模式
path VARCHAR(512) 路由路径
title VARCHAR(256) 页面标题
hash VARCHAR(64) UNIQUE 哈希
count BIGINT 访问次数

api_dimensions - 接口维度表

网络请求的 URL、method 等也可以维度化,日志里只存 api_id

字段 类型 说明
id BIGINT PK 接口 ID
project_id VARCHAR(64) 项目 ID
method VARCHAR(16) 请求方法GET/POST/...
url_pattern VARCHAR(512) 归一化后的 URL 模式
domain VARCHAR(128) 域名
hash VARCHAR(64) UNIQUE method + url_pattern 的哈希
total_count BIGINT 总请求数
error_count BIGINT 错误数
avg_duration DOUBLE 平均耗时
last_seen DATETIME 最后出现时间

为什么要做接口维度化?

  • 日志里只存 api_id8 字节),不用存完整 URL
  • URL 归一化后,同一路由的不同参数只会产生 1 条维度记录
  • 按接口统计错误率、耗时等指标时,直接 JOIN 维度表即可

3.5 项目配置表

projects - 项目表

字段 类型 说明
id VARCHAR(64) PK 内部 IDproj_xxx
project_id VARCHAR(32) UNIQUE Sentry 项目 ID纯数字
public_key VARCHAR(64) 公钥(默认=内部 ID
name VARCHAR(128) 项目名称
platform VARCHAR(32) 平台
environment VARCHAR(32) 默认环境
description TEXT 描述
status VARCHAR(32) 状态active / disabled
rate_limit INT 速率限制(事件/天)
sample_rate DOUBLE 采样率0-1
created_at DATETIME 创建时间
updated_at DATETIME 更新时间

alert_rules - 告警规则表

见告警引擎设计文档。


四、聚合任务设计

4.1 聚合方式

方式 实时性 复杂度 适用场景
实时增量聚合 秒级 错误计数、Issue 更新
定时批量聚合 分钟级 性能分位数、小时统计
离线重算 天级 数据修正、历史回刷

4.2 实时增量聚合(错误计数)

每次事件处理时,直接更新 MySQL

INSERT INTO error_issues 
  (project_id, fingerprint, error_type, error_message, 
   total_count, today_count, last_seen, first_seen, status)
VALUES 
  (?, ?, ?, ?, 1, 1, NOW(), NOW(), 'active')
ON DUPLICATE KEY UPDATE
  total_count = total_count + 1,
  today_count = today_count + 1,
  last_seen = NOW(),
  status = CASE WHEN status = 'resolved' THEN 'active' ELSE status END;

优点:实时性好,数据立刻可见 缺点:高并发下 MySQL 压力大

优化

  • 内存中先做 1 秒级别的微批聚合,再批量写 MySQL
  • 使用 Redis 做计数缓冲,定期刷入 MySQL

4.3 定时批量聚合(性能统计)

使用 Cron 定时任务,每 5 分钟从 Loki 拉取数据聚合:

每 5 分钟执行:
  1. 从 Loki 查询过去 5 分钟的性能事件
  2. 按 project + metric + 页面 分组
  3. 计算 P50/P90/P95/avg 等指标
  4. 写入 performance_stats_hourly 表

为什么用定时任务而不是实时?

  • 性能指标不需要秒级实时
  • 分位数计算需要一定数据量才准确
  • 定时批量更省资源

4.4 数据过期与归档

Loki 数据过期

通过 Loki 的 retention 配置自动删除:

limits_config:
  retention_period: 168h  # 7 天

MySQL 数据过期

  • 小时统计表:保留 30 天
  • 日统计表:保留 1 年
  • 错误 Issue 表:永久保留(只存聚合数据,量很小)

定时任务每天凌晨清理过期数据。


五、数据迁移与兼容

5.1 当前状态

目前项目使用 JSON 文件存储项目配置Loki 存储原始日志,没有 MySQL 聚合层。

5.2 演进路径

阶段 1JSON → SQLite单机版

  • 零依赖,开箱即用
  • 适合个人项目、小团队
  • 单节点足够

阶段 2SQLite → MySQL生产版

  • 支持并发
  • 性能更好
  • 适合多项目、中大型团队

阶段 3增加 ClickHouse大规模

  • 亿级数据量
  • 复杂分析查询
  • 一般不需要

六、Grafana 数据源配置

6.1 Loki 数据源

  • URL: http://loki:3100
  • 开启 JSON 解析
  • 配置 Derived fields从日志跳转到追踪

6.2 MySQL 数据源

  • 用于展示聚合数据(性能趋势、错误趋势)
  • 比 Loki 查询更稳定、更快

七、数据安全

7.1 数据脱敏

  • 入库前脱敏SDK 端 + 服务端双重脱敏
  • 查询时脱敏:敏感字段查询结果自动打码
  • 导出时脱敏:导出数据默认脱敏

7.2 数据隔离

  • 项目间完全隔离(通过 project_id label
  • 查询时强制带 project_id 条件
  • 管理后台有项目权限控制

7.3 数据备份

  • MySQL每日全量备份 + binlog 增量备份
  • Loki定期快照到对象存储
  • 配置文件Git 版本管理