# Metrics 巡检 API — 分析与实施文档

## 一、背景

当前 `RunMetricsHealthCheck()` 函数（`metrics_check_test.go`）以测试用例形式运行，按 Org 维度遍历所有集群执行 Metrics 巡检，结果写入飞书文档并发送飞书消息卡片。

目标：将核心巡检逻辑抽取为 HTTP API，实现单集群 Metrics 巡检，对标 `POST /clinic-internal/healthcheck/sql/slowquery-analyze` 的模式（执行巡检 → 生成 JSON → 异步上传 S3）。不需要飞书相关逻辑。

---

## 二、当前 RunMetricsHealthCheck() 完整调用链分析

### 2.1 入口与初始化

**文件**: `metrics_check_test.go`

```
RunMetricsHealthCheck()
  → confMetrics.InitForMetrics()       // 加载 config-metrics.yaml + cluster.yaml
  → orgs := confMetrics.OrgsConfig.Orgs // 获取组织列表
  → 遍历每个 org，调用 metrics.GetMetricsResult(ctx, org, orgName)
```

**InitForMetrics()** (`pkg/config/metrics/config.go:104-120`):
1. 加载 `configs/config-metrics.yaml` → `AppConfig`（MetricsConfig，包含 Prometheus 查询模板）
2. 加载 `configs/cluster.yaml` → `OrgsConfig`（组织和集群定义）
3. 初始化日志

### 2.2 GetMetricsResult() 核心流程

**文件**: `pkg/metrics/metrics_query.go:21-49`

```
GetMetricsResult(ctx, org, tenant)
  │
  ├─ Step1: getQueryConfig()                         // 获取查询配置
  │    → cpuQueries:  confMetrics.AppConfig.GetCPUResource()
  │    → memQueries:  confMetrics.AppConfig.GetMEMResource()
  │    → latencyQueries: confMetrics.AppConfig.GetLatencyQueries()
  │    → diskQueries: confMetrics.AppConfig.GetDISKResource()
  │    → resourceQuery = [cpuQueries, memQueries]
  │    → performanceQuery = [latencyQueries, diskQueries]
  │
  ├─ Step2: filterClustersByTenant()                 // 按租户过滤集群 → []ProjectInfo
  │
  ├─ Step3: 遍历每个 ProjectInfo:
  │    ├─ getDashboardResult()                       // 核心查询+计算
  │    │    └─ 遍历每个 Cluster:
  │    │         ├─ getMetricsResult(resource queries, "Resource")
  │    │         │    ├─ common.GetCluster(ctx, clusterId)  // 获取集群配置（带缓存）
  │    │         │    ├─ prometheus.MetricsQueryBatch()      // 执行 Prometheus 查询
  │    │         │    ├─ getResourceDiffResult()             // 计算资源指标差异
  │    │         │    └─ getResourceQueryResult()            // 格式化为表格行 + 状态判定
  │    │         ├─ getMetricsResult(performance queries, "Performance")
  │    │         │    ├─ prometheus.MetricsQueryBatch()
  │    │         │    ├─ getPerformanceDiffResult()          // 计算性能指标差异
  │    │         │    └─ getResourceQueryResult()
  │    │         └─ feishu.WriteDetailResult()               // [飞书] 写详细结果
  │    │
  │    ├─ filterDashboardResult()                    // 按 QueryName 去重，保留最大阈值
  │    └─ feishu.WriteDashboardResult()              // [飞书] 写 Dashboard
  │
  ├─ Step4: feishu.CreateHeading1ForTime()           // [飞书] 写时间标题
  ├─ Step5: GetMetricsMessageResult()                // 过滤"Need Attention"结果
  └─ Step6: feishu.SendMessageCardForMetrics()       // [飞书] 发消息卡片
```

### 2.3 Prometheus 查询细节

**文件**: `pkg/prometheus/metrics_query.go:41-108`

`MetricsQueryBatch(ctx, cluster, queries, timestamp)`:

1. 计算时间范围:
   - **Primary**: timestamp 前一天 00:00 ~ 23:59:59，step=4min
   - **Baseline**: Primary 前 14 天 ~ Primary 开始时间，step=30min

2. 创建 Prometheus client:
   - 使用 `CreatePrometheusClient()` 根据集群信息（Region、Provider、DeployType 等）获取 Prometheus URL

3. 并发执行每个 query:
   - 替换 `$rate`：primary 用 `4m`，baseline 用 `30m`
   - 特殊指标（P99QueryLatency/P999QueryLatency/P99TnxDuration）使用 `getAvgBaseLineFor14Day()` 按天分段查询后合并

4. **当前问题**：PromQL 中的 `$tidb_cluster` 变量没有被替换（后加的变量），dedicated 集群因 endpoint 是集群级别的所以不受影响，但 premium 集群需要显式过滤。

### 2.4 差异计算逻辑

**文件**: `pkg/metrics/metrics_calculate.go`

#### 资源指标 `getResourceDiffResult()` (lines 16-89)
1. 从 Prometheus 结果中提取数值 `extractValuesFromModelValue()`
2. 去掉头尾各 5% 的异常值 `removeTopAndBottom5Percent()`
3. 分别对 primary 和 baseline 计算 P90（`stats.Percentile(90)`）和 AVG（`stats.Mean()`）
4. 计算差异比:
   - `Diff_P90 = (primaryP90 / baselineP90) - 1`
   - `Diff_AVG = (primaryAvg / baselineAvg) - 1`
5. 返回 `[]QueryPoints`，每个包含 TypeName 和 DiffResult 列表

#### 性能指标 `getPerformanceDiffResult()` (lines 158-204)
1. 将 Prometheus 结果转换为 stream（`convertToStreams()`）
2. 按 metric label 匹配 primary 和 baseline stream
3. 对每个匹配的 stream 调用 `calculateStreamResult()`:
   - 计算 primary 的 Max、Avg
   - 对 primary 和 baseline 都做 trimming 后计算 trimmed avg
   - `Diff = (trimmedPrimaryAvg / trimmedBaselineAvg) - 1`
4. TypeName 格式: `queryName_labelValue`（如 `P99TiKVLatency_coprocessor`）

### 2.5 结果格式化与状态判定

**文件**: `pkg/metrics/metrics_table.go`

`getResourceQueryResult()` (lines 11-58):
- 将 `QueryPoints` 转换为 `[]TableRowAggregation`
- 设置 Threshold 为百分比格式（如 `"12.5%"`）
- 延迟类指标（名字含 latency/duration）的 Max/Avg 乘以 1000（秒→毫秒）
- 调用 `processQueryName()` 进行状态判定和 Grafana 链接生成

### 2.6 状态判定规则

**文件**: `pkg/metrics/metrics_table.go:66-261`

| 指标 | 阈值（Diff） | 判定为 Need Attention |
|------|-------------|----------------------|
| P99TiKVLatency_coprocessor | > 0.25 (25%) | Yes |
| P99TiKVLatency_kv_prewrite | > 0.25 (25%) | Yes |
| P99RegionNum | > 1.2 (120%) | Yes |
| P99TnxDuration_optimistic | > 1.3 (130%) | Yes |
| P99TnxDuration_pessimistic | > 1.3 (130%) | Yes |
| P99WriteTiKV | > 1.2 (120%) | Yes |
| DiskUsage | > 1.1 (110%) | Yes |
| ReadWaitLatency | > 1.15 (115%) | Yes |
| WriteWaitLatency | > 0.15 (15%) | Yes |
| CPS | > 0.2 (20%) | Yes |
| P999QueryLatency | > 0.2 (20%) | Yes |
| P99QueryLatency | > 0.2 (20%) | Yes |
| QPS | > 0.2 (20%) | Yes |
| TiDB/TiKV CPU/MEM 所有变体 | > 0.2 (20%) | Yes |
| 默认 | > 0.2 (20%) | Yes |

### 2.7 关键数据结构

**文件**: `pkg/dto/metrics/metrics_dto.go`

```go
type TableRowAggregation struct {
    Threshold       string `json:"threshold"`        // 变化百分比，如 "12.5%"
    Status          string `json:"status"`           // "Need Attention" 或 "No Risk"
    Link            string `json:"link"`             // Grafana 链接
    QueryName       string `json:"query_name"`       // 指标名
    TimeAggregation string `json:"time_aggregation"` // "P90" 或 "AVG"
    Max             string `json:"max"`              // 最大值
    Avg             string `json:"avg"`              // 平均值
}

type ClusterTableData struct {
    ClusterName           string            `json:"cluster_name"`
    ResourceDetailData    []DetailTableData `json:"resource_table_data"`
    PerformanceDetailData []DetailTableData `json:"performance_table_data"`
}

type DetailTableData struct {
    RuleName            string                `json:"rule_name"`
    TableRowAggregation []TableRowAggregation `json:"table_row_aggregation"`
}

type QueryBatchResult struct {
    Name                string      // 查询名称
    PrimaryQueryResult  model.Value // 当天查询结果
    BaseLineQueryResult model.Value // 14 天基线结果
}
```

### 2.8 PromQL 查询模板

所有查询定义在 `configs/config-metrics.yaml`：

**资源类** (`cpuResource` + `memResource`):
- TiDB_CPU_P90, TiDB_CPU_P80, TiDB_CPU
- TiKV_CPU_P90, TiKV_CPU_P80, TiKV_CPU
- TiDB_MEM_P90, TiDB_MEM_P80, TiDB_MEM
- TiKV_MEM_P90, TiKV_MEM_P80, TiKV_MEM

**性能类** (`latencyQueries` + `diskQueries`):
- P99QueryLatency, P999QueryLatency
- P99TiKVLatency (coprocessor/kv_prewrite)
- CPS, QPS
- ReadWaitLatency, WriteWaitLatency, DiskUsage

所有查询都包含两个变量：
- `$rate` — 由 MetricsQueryBatch 替换
- `$tidb_cluster` — 需要新增替换逻辑（premium 集群适配）

---

## 三、新 API 设计

### 3.1 路由

```
POST /clinic-internal/healthcheck/metrics/analyze
```

### 3.2 请求结构

复用 `HealthCheckCommonParameters`（`pkg/service/slowquery_analyzer.go:38-50`）：

```json
{
    "name": "metrics_healthcheck",
    "clusterId": "1379661944646416054",
    "physicalClusterId": "",
    "reportId": "report-xxx",
    "provider": "aws",
    "region": "us-west-2",
    "deployType": "dedicated",
    "projectId": "xxx",
    "tenantId": "xxx",
    "timeRange": {
        "start": "2026-03-14T00:00:00Z",
        "end": "2026-03-14T23:59:59Z"
    },
    "clusterUrls": {
        "clusterMetricsUrl": "",
        "dataPlaneMetricsUrl": ""
    }
}
```

| 字段 | 必填 | 说明 |
|------|------|------|
| clusterId | 是 | 集群 ID |
| physicalClusterId | premium 集群必填 | 物理集群 ID，用于 PromQL 中 `$tidb_cluster` 替换 |
| timeRange | 是 | 巡检时间范围 |
| clusterUrls | 否 | 为空时通过 clinicClient 自动获取 |
| region | 否 | 为空时自动获取 |
| provider | 否 | 为空时自动获取 |
| deployType | 否 | 集群类型（dedicated/premium） |

### 3.3 响应结构

```json
{
    "name": "metrics_healthcheck",
    "description": "Cluster metrics health check: compares current metrics against 14-day baseline",
    "checkType": "metrics",
    "status": "success",
    "resultJsonUrl": "s3://delicated-healthcheck/healthcheck-report/xxx.json",
    "summary": {
        "totalMetrics": 20,
        "needAttentionCount": 3
    },
    "results": [
        {
            "queryName": "TiDB_CPU",
            "timeAggregation": "P90",
            "threshold": "25.3%",
            "status": "Need Attention",
            "max": "0.85",
            "avg": "0.72",
            "link": "https://clinic.pingcap.com/grafana/d/..."
        }
    ]
}
```

### 3.4 处理流程

```
请求进入
  │
  ├─ 1. 解析请求、校验必填字段（clusterId, timeRange）
  │
  ├─ 2. 自动填充（当 clusterUrls/region 为空时）
  │     → clinicClient.GetClusterDataURLs(ctx, clusterId)
  │     → 填充 ClusterMetricsURL, DataPlaneMetricsURL, Region, Provider
  │
  ├─ 3. 构建 model.Cluster（从请求入参）
  │     → ClusterID, PhysicalClusterID, DeployType, Region, Provider, OrgID, ProjectID
  │
  ├─ 4. 加载查询配置
  │     → getQueryConfig() → resourceQuery + performanceQuery
  │
  ├─ 5. 执行 Prometheus 查询（MetricsQueryBatch 自动替换 $rate 和 $tidb_cluster）
  │     → 资源类: CPU + MEM
  │     → 性能类: Latency + Disk
  │
  ├─ 6. 差异计算
  │     → getResourceDiffResult() / getPerformanceDiffResult()
  │
  ├─ 7. 格式化 + 状态判定
  │     → getResourceQueryResult() → []TableRowAggregation
  │
  ├─ 8. 生成响应 + S3 路径
  │
  ├─ 9. 异步上传 S3（本地也保存一份）
  │
  └─ 10. 返回 JSON 响应
```

---

## 四、Premium 集群适配方案

### 4.1 问题

`config-metrics.yaml` 中所有 PromQL 查询都包含 `$tidb_cluster` 变量（如 `tidb_cluster_id="$tidb_cluster"`）。
当前 `MetricsQueryBatch` 只替换 `$rate`，没有替换 `$tidb_cluster`。

- **dedicated 集群**: Prometheus endpoint 是集群级别的，`$tidb_cluster` 未被替换不影响查询（endpoint 已隐式限定集群范围）
- **premium 集群**: Prometheus endpoint 是共享的（多租户），必须通过 `tidb_cluster_id` label 显式过滤

### 4.2 方案

直接修改 `MetricsQueryBatch`（`pkg/prometheus/metrics_query.go:72-73`），在替换 `$rate` 后同时替换 `$tidb_cluster`：

```go
// 替换 $rate（原有逻辑）
primaryQuery := strings.ReplaceAll(q.PrimaryQuery, "$rate", "4m")
baseQuery := strings.ReplaceAll(q.PrimaryQuery, "$rate", "30m")

// 新增：替换 $tidb_cluster（适配 premium 集群）
tidbClusterID := cluster.ClusterID
if cluster.DeployType == "premium" && cluster.PhysicalClusterID != "" {
    tidbClusterID = cluster.PhysicalClusterID
}
primaryQuery = strings.ReplaceAll(primaryQuery, "$tidb_cluster", tidbClusterID)
baseQuery = strings.ReplaceAll(baseQuery, "$tidb_cluster", tidbClusterID)
```

### 4.3 对现有逻辑的影响

- 现有 `RunMetricsHealthCheck()` 流程通过 `common.GetCluster()` 获取 `model.Cluster`，其中已有 `ClusterID`、`DeployType`、`PhysicalClusterID`
- dedicated 集群: `PhysicalClusterID` 为空，使用 `ClusterID` 替换 → Prometheus 端仍然是集群级别 endpoint，不影响结果
- **不会破坏现有逻辑**

---

## 五、需要修改/新建的文件

| 操作 | 文件路径 | 说明 |
|------|---------|------|
| 新建 | `pkg/service/metrics_healthcheck.go` | Service 层：请求/响应结构 + 巡检逻辑 |
| 修改 | `pkg/handlers/slowquery_handler.go` | 新增 MetricsHealthCheckHandler + Handler 方法 |
| 修改 | `pkg/server/router.go` | 注册新路由 |
| 修改 | `pkg/prometheus/metrics_query.go` | MetricsQueryBatch 中增加 $tidb_cluster 替换 |
| 修改 | `pkg/metrics/metrics_query.go` | 新增导出函数 GetSingleClusterMetrics() 包装现有私有函数 |

**不修改的文件**（仅复用）:
- `pkg/metrics/metrics_calculate.go` — 差异计算逻辑
- `pkg/metrics/metrics_table.go` — 格式化 + 状态判定
- `metrics_check_test.go` — RunMetricsHealthCheck() 不动

---

## 六、复用的现有函数

| 函数 | 文件 | 用途 |
|------|------|------|
| `confMetrics.AppConfig.GetCPUResource()` 等 | `pkg/config/metrics/config.go` | 获取查询配置 |
| `prometheus.CreatePrometheusClient()` | `pkg/prometheus/prometheus_api_sdk.go:135` | 创建 Prometheus 客户端 |
| `prometheus.CreatePrometheusClientWithUrls()` | `pkg/prometheus/prometheus_api_sdk.go:265` | 通过 URL 创建客户端 |
| `prometheus.MetricsQueryBatch()` | `pkg/prometheus/metrics_query.go:41` | 批量执行 Prometheus 查询 |
| `metrics.getResourceDiffResult()` | `pkg/metrics/metrics_calculate.go:16` | 资源指标差异计算 |
| `metrics.getPerformanceDiffResult()` | `pkg/metrics/metrics_calculate.go:158` | 性能指标差异计算 |
| `metrics.getResourceQueryResult()` | `pkg/metrics/metrics_table.go:11` | 格式化结果 + 状态判定 |
| `clinicClient.GetClusterDataURLs()` | `pkg/clinic-internal-api/client.go:71` | 反查集群 URLs |
| `aws.UploadFileToS3()` | `pkg/thirdapi/aws/s3.go` | S3 上传 |
| `HealthCheckCommonParameters` | `pkg/service/slowquery_analyzer.go:38` | 请求基类 |

---

## 七、验证方式

1. `go build ./...` — 编译通过
2. 启动 API server: `go run cmd/api/main.go`
3. 发送 POST 请求到 `/clinic-internal/healthcheck/metrics/analyze`
4. 验证返回 JSON 结构正确，指标结果包含资源类和性能类
5. 验证本地 `./output/healthcheck-report/` 下生成了结果文件
6. 验证现有 `RunMetricsHealthCheck()` 测试不受影响: `go test -v -run TestMetricsHealthCheck`
