# Seed Parent Metrics 的 NextGen 回退方案

## 问题背景

Grafana 的 parent metrics 路由接收如下格式的逻辑控制面标识：

```text
seed/<vendor>-<region>/<applicationID>
```

例如：

```text
seed/aws-us-west-2/usw2-byoc-pm
```

当前 `ResolveLogicalControlPlaneMetricsTarget` 会通过 `Client.GetSeedCluster` 解析所有逻辑 `seed` 目标。该方法始终使用 `DedicatedBizTypeWithEnv()` 构造 O11Y 集群列表查询。因此在 staging 环境中，请求参数固定为：

```text
bizType=staging-tidbcloud
tenantID=infra-staging
projectID=seed
```

部分逻辑 seed 应用（包括 NextGen 控制面）实际注册在 `staging-tidbcloud-nextgen` 下。由于当前 `control_plane_info` 没有携带 biz type，只查询 Dedicated 会得到空集群列表；即使存在完全匹配的 NextGen 记录，parent metrics 仍然会失败。

同一条链路还存在第二个错误处理问题。O11Y 集群列表为空时，当前代码返回 `no o11y cluster found`、`no region metric cluster found` 等普通错误。调用方使用 `errors.Is(err, o11y.ErrNotFound)` 判断是否可以安全执行兼容性回退，因此没有错误类型的空结果会阻止正确回退，并可能最终表现为 HTTP 500。

## 改造目标

1. 保持现有逻辑 `control_plane_info` 请求协议不变。
2. 抽取一个接收显式 O11Y biz type 的精确 seed 集群查询方法。
3. 提供命名明确的 Dedicated 和 NextGen seed 查询入口。
4. 优先查询 Dedicated，保持现有 Dedicated seed 行为不变。
5. 仅当 Dedicated 查询返回 `ErrNotFound` 时查询 NextGen。
6. O11Y 集群列表和 seed 查询涉及的所有空结果都必须包装 `ErrNotFound`。
7. 非 NotFound 错误不执行回退，确保鉴权失败、传输失败、响应格式错误以及多结果错误仍然可见。

## 非目标

- 本次改造不向 Grafana 请求新增 `control_plane_biz_type`、`deploy_type` 或 `deploy_type_v2`。
- 不根据 TiDB Cloud child 集群推导 seed biz type。
- 不改变 `control_plane_info` 的字符串格式。
- parent metrics 不使用 `ListGlobalClusters` 或全局 O11Y 集群信息缓存。
- 不改变 serverless/devtier parent 的解析逻辑。
- parent metrics 不回退到 child metrics。
- 同一个 biz type 查询到多个集群时，不静默选择其中一个。

## 当前调用链

```text
/metric/query?metrics_scope=parent
  -> TiDBClusterMetricHandler.handleControlPlaneParentMetrics
  -> ClusterURLsResolver.ResolveLogicalControlPlaneMetricsTarget
  -> Client.GetSeedCluster
  -> Client.ListClusterWithCache
  -> O11Y GET /api/v1/clusters
       bizType=<env>-tidbcloud
```

除 biz type 外，当前查询条件已经足够精确：

```text
vendor + region + tenantID + projectID + applicationID + showDeleted
```

现有问题只是 biz type 被固定在 Dedicated 命名空间。

## 目标设计

### 通用精确查询方法

在 `o11y.Client` 中新增一个未导出方法：

```go
getSeedCluster(ctx, vendor, region, applicationID, bizType)
```

该方法统一负责以下 seed 查询字段：

```text
BizType       = 显式传入的参数
Vendor        = control_plane_info 中的 vendor
Region        = control_plane_info 中的 region
TenantID      = infra-<env>
ProjectID     = seed
ApplicationID = control_plane_info 中的 application ID；为空时默认为 seed
ShowDeleted   = true
```

返回规则如下：

- 恰好一个结果：返回对应的 `ClusterInfo`；
- 零个结果：返回包装了 `ErrNotFound` 的错误；
- 多于一个结果：返回不属于 `ErrNotFound` 的歧义错误；
- 列表 API 或缓存失败：保留原始错误，并增加 seed 查询上下文。

### 明确的导出入口

提供两个方法：

```go
GetDedicatedSeedCluster(ctx, vendor, region, applicationID)
GetNextGenSeedCluster(ctx, vendor, region, applicationID)
```

它们分别使用以下 biz type 调用通用方法：

```text
Dedicated: DedicatedBizTypeWithEnv()
NextGen:   NextGenBizTypeWithEnv()
```

逻辑 parent resolver 不再调用含义模糊的 `GetSeedCluster`。为了避免破坏仓库外的潜在调用方，保留 `GetSeedCluster` 作为 Deprecated 的 Dedicated 兼容别名；改造后仓库内部不再使用该别名。

### Resolver 回退规则

`ResolveLogicalControlPlaneMetricsTarget` 保持当前解析逻辑和 `projectID == seed` 校验，然后按以下顺序解析：

```text
GetDedicatedSeedCluster
  -> 成功：使用 Dedicated 目标
  -> ErrNotFound：调用 GetNextGenSeedCluster
  -> 其他错误：停止并返回错误
```

该顺序用于保持历史 Dedicated 行为。只有在 Dedicated 命名空间中找不到精确 application ID 时，才把 NextGen 作为兼容性查询。

如果 NextGen 查询仍然未找到，resolver 返回一个包装了 `ErrNotFound` 的错误。这样 proxy 可以把失败映射为现有的 NotFound 响应，而不是当作内部错误处理。

### 空结果错误约定

`Client.ListClusterWithCache` 是成功的 O11Y 响应可能返回零个集群的公共边界。该情况改为：

```go
fmt.Errorf("%w: no o11y cluster found", ErrNotFound)
```

seed 通用方法也必须单独检查 `len(infos) == 0` 并包装 `ErrNotFound`，以覆盖缓存返回空切片以及未来可能新增的列表实现。

现有调用方已经使用 `%w` 包装列表错误，因此该 sentinel error 能够沿完整错误链被 `errors.Is` 识别。

## 错误与回退矩阵

| Dedicated 查询结果 | NextGen 行为 | 最终结果 |
| --- | --- | --- |
| 恰好一个集群 | 不调用 | 使用 Dedicated 集群目标 |
| 空结果或 `ErrNotFound` | 查询 NextGen | 使用 NextGen 查询结果 |
| 鉴权或传输错误 | 不调用 | 保留 Dedicated 错误 |
| 无效或无法解析的响应 | 不调用 | 保留 Dedicated 错误 |
| 返回多个集群 | 不调用 | 保留歧义错误 |
| `ErrNotFound` | 恰好一个集群 | 使用 NextGen 集群目标 |
| `ErrNotFound` | `ErrNotFound` | 最终错误继续包装 `ErrNotFound` |
| `ErrNotFound` | 非 NotFound 错误 | 保留 NextGen 错误 |

## 修改文件

### `pkg/o11y/client_new.go`

- `ListClusterWithCache` 的空结果包装 `ErrNotFound`。
- 从 `GetSeedCluster` 抽取 `getSeedCluster(..., bizType)`。
- 新增 `GetDedicatedSeedCluster`。
- 新增 `GetNextGenSeedCluster`。
- seed 通用方法的零结果检查包装 `ErrNotFound`。
- 保留 Deprecated 的 `GetSeedCluster` Dedicated 兼容别名。

### `pkg/o11y/urls/urls.go`

- 使用 `GetDedicatedSeedCluster` 替换 `GetSeedCluster`。
- 当 `errors.Is(err, o11y.ErrNotFound)` 时，使用 `GetNextGenSeedCluster` 重试。
- 其他错误类型不重试。
- 保持 `InternalReadUri` 校验和目标构造逻辑不变。

### `pkg/o11y/urls/urls_test.go`

- 保留现有 Dedicated 逻辑 seed 成功测试。
- 增加 NextGen 回退测试，验证准确的请求顺序和参数。
- 增加 Dedicated、NextGen 都不存在的测试，验证返回错误满足 `errors.Is(err, o11y.ErrNotFound)`。
- 增加 Dedicated 非 NotFound 失败测试，验证不会查询 NextGen。

### `pkg/o11y/client_test.go`

- 覆盖 Dedicated 和 NextGen 精确入口的请求参数构造。
- 验证 `ListClusterWithCache` 将成功响应中的空列表识别为 `ErrNotFound`。

## 验证计划

优先运行相关包测试：

```bash
go test ./pkg/o11y/...
go test ./pkg/proxy/...
```

相关测试通过后，在条件允许时运行更大范围测试：

```bash
go test ./pkg/...
```

必须验证以下行为：

1. 现有 Dedicated seed 查询发送 `<env>-tidbcloud`，成功后不查询 NextGen。
2. Dedicated 返回空结果后，只触发一次 `<env>-tidbcloud-nextgen` 查询。
3. NextGen 成功后，使用其 `InternalReadUri` 作为 metrics 目标。
4. 两个命名空间均为空时，最终错误可以被识别为 `o11y.ErrNotFound`。
5. Dedicated 的传输、API 或多结果错误不触发 NextGen 回退。
6. 现有 serverless 控制面测试保持不变并通过。
7. 现有 child metrics 解析行为保持不变并通过。

## 发布与可观测性

现有集群列表日志会记录完整的非敏感请求参数，包括 `bizType`、`vendor`、`region`、`tenantID`、`projectID` 和 `applicationID`。发布后，受影响请求应依次出现：

```text
staging-tidbcloud -> 未找到集群
staging-tidbcloud-nextgen -> 找到一个精确匹配的集群
```

已有 Dedicated seed 应用不应出现回退。可以通过持续出现的回退日志识别后续需要显式传递控制面 biz type 的 dashboard 或控制面。

## 后续工作

该兼容性回退没有消除 `control_plane_info` 本身的歧义。后续应在请求协议中增加经过校验的显式 control-plane biz type。当所有调用方都能提供该字段后，resolver 可以只执行一次精确查询，多 biz type 回退仅为旧请求保留。
