diff --git "a/docs/\345\276\256\346\234\215\345\212\241\345\217\257\350\247\202\346\265\213\346\200\247\345\273\272\350\256\276\346\212\200\346\234\257\350\256\276\350\256\241.md" "b/docs/\345\276\256\346\234\215\345\212\241\345\217\257\350\247\202\346\265\213\346\200\247\345\273\272\350\256\276\346\212\200\346\234\257\350\256\276\350\256\241.md" new file mode 100644 index 0000000..19f4526 --- /dev/null +++ "b/docs/\345\276\256\346\234\215\345\212\241\345\217\257\350\247\202\346\265\213\346\200\247\345\273\272\350\256\276\346\212\200\346\234\257\350\256\276\350\256\241.md" @@ -0,0 +1,938 @@ +# 微服务可观测性建设技术设计 + +> 需求:[opensourceways/backlog#1938](https://github.com/opensourceways/backlog/issues/1938) +> 子任务 A(SDK 与试点):[#2061](https://github.com/opensourceways/backlog/issues/2061) · B(底座与大屏告警):[#2062](https://github.com/opensourceways/backlog/issues/2062) · C(全量铺开与验收):[#2063](https://github.com/opensourceways/backlog/issues/2063) +> 契约与 SDK 实现仓:[opensourceways/obs-sdk](https://github.com/opensourceways/obs-sdk) + +--- + +## 1. 背景 + +### 1.1 问题 + +基础设施团队维护的重点微服务缺乏统一可观测能力,故障定位靠人工登录机器翻日志,排障链路长、效率低,服务健康度不可见。 + +各社区另缺一个**统一的服务健康度大盘**(形态可对齐 [GitHub Status](https://www.githubstatus.com/)):社区用户与运维无法一眼看到整体与各组件(如评审机器人、账号、CLA 等)是否正常、异常落在哪一段? + +### 1.2 覆盖范围 + +以 2025-05-01 ~ 2025-07-31 三个月有 PR 合入的活跃服务为全集,剔除纯配置/部署仓、umbrella 大仓、框架库/工具仓、前端 website 仓后,保留 **15 个大仓 / 26 个微服务子仓 / 390 个合入 PR**。 + +语言分布不统一:Go 7 · Java 6 · Python 8(+3 待评估)· Node 1 · 编排仓 1。 + +### 1.3 现状基线 + +| 能力 | 现状 | 结论 | +| --- | --- | --- | +| 硬件级指标(CPU/内存/磁盘/网络) | 已由华为云 CCE 云原生监控插件(node-exporter / kube-state-metrics)+ icagent 上报控制台 | **复用,不建设** | +| 日志采集管道 | log-agent 插件(fluent-bit / otel-collector)已部署,全局规则 `default-stdout`(`allContainers: true`)采集所有 ns 容器 stdout | **复用,不建设** | +| 日志内容 | 已进 LTS 可全文检索,但**无结构化裸文本、无统一业务字段**(service / level / request_id 不保证、命名不统一) | **建设点:结构化 + 字段统一** | +| 业务指标 | 26 个微服务均未暴露 / 接入 `/metrics`;Prometheus 仅采系统组件 | **建设点:从零接入** | +| 告警 | 无 PrometheusRule、无 Alertmanager | **建设点:从零接入** | +| 监控大盘(对内指标视图) | 无业务级大盘 | **建设点:从零接入** | +| 服务健康度大盘(对外状态视图) | 无,各社区无统一入口,整体/组件级健康度不可见 | **建设点:从零接入** | + +### 1.4 关键约束 + +1. **多语言**:4 种语言、框架不统一(Gin / Spring Boot / Django / FastAPI / Flask / 上游开源项目),不能用单一语言的方案覆盖。 +2. **多社区、多 Region**:26 服务 prod 跨 **4 个 Region**(香港 / 北京一 / 北京四 / 贵阳二,约 17 个集群)。同一逻辑服务可能按社区拆成多套独立部署(如 `robot-universal-review` 在 **10 个社区 / 7 个集群**各有一套),需要能按社区维度切片查看健康度。 +3. **不能自研 instrumentation**:各语言日志/指标生态成熟,自研只会带来维护负担与生态割裂。 + +--- + +## 2. 目标 + +### 2.1 建设目标 + +| # | 目标 | 对应验收标准 | +| --- | --- | --- | +| G1 | 26 个微服务暴露 `/metrics`(QPS / 错误率 / 业务自定义指标)并接入 Prometheus | 验收 2 | +| G2 | 26 个微服务日志统一输出**结构化 JSON**(含 service / level / community / request_id 等契约字段)到 stdout,经现有 log-agent 进 LTS,支持按 service / level / community / request_id 检索 | 验收 3 | +| G3 | 建立业务级监控大盘,重点服务健康度集中可见,**支持按 community 维度过滤/分组** | 验收 4 | +| G4 | 关键服务配置告警规则(高错误率 / 宕机 / 资源超阈值)并打通通知到负责人 | 验收 5 | +| G5 | 硬件级指标复用云上,不重复建设 | 验收 1 | +| G6 | 建立**社区健康度大盘**(对外状态页),按社区聚合展示重点组件(评审机器人 / 账号 / CLA 等)的健康度 | **验收 6** | + +**目标达成后,落到具体的人身上是这样两件事**(完整处理路径见 §4.10.1 / §4.10.2): + +| 视角 | 场景 | 目标态下 | +| --- | --- | --- | +| **用户** | MindSpore 社区评审机器人不响应 | 查**社区健康度大盘**即知是机器人侧异常、落在哪一个社区——**不盲猜、不逐个私聊研发** | +| **研发** | CLA 签署失败(`app-cla-server`) | **告警先于用户报障**(自带 `cluster`)→ 指标大盘确认是服务自身异常 → 同屏日志检索定位到数据库 → 带证据转运维处理 | + +### 2.2 设计原则(方案阶段已定) + +1. **薄封装,不自研 instrumentation**。每语言 1 个薄 SDK,内部引用各语言官方库(Go→`client_golang`、Python→`prometheus-client`、Java→`micrometer`+Actuator、Node→`prom-client`),SDK 只做装配:中间件挂载 + 通用字段注入 + 命名对齐 + 一行初始化。 +2. **契约先行**。跨语言统一的日志 schema、通用字段、指标命名规范收敛到单一仓库的 `spec/` 目录,是唯一事实来源;四语言实现与 spec 不一致时以 spec 为准。 +3. **log 与 metrics 同仓不同 package**,不拆成两个库。 +4. **单仓 monorepo**:`opensourceways/obs-sdk`,顶层 `spec/` + `go/ python/ java/ node/`,各语言子目录自管版本。 +5. **首期只做 log + metrics,不含 trace**。`trace_id` / `span_id` 作为**预留注入位**存在(字段可写可透传、有值才输出),二期接入 OTel 时零日志格式返工。 +6. **Go SDK 独立成库,不放进 robot-framework-lib** —— 后者是机器人领域框架,SDK 需领域中立以服务 24 个非机器人服务。 + +--- + +## 3. 整体架构 + +### 3.1 范围与分层视图 + +基础设施可观测建设整体分**三块** + +```mermaid +flowchart TB + subgraph L1["① 范围"] + direction LR + A1["【服务】微服务
26 个微服务子仓 · 4 种语言"] + A2["【资源】昇腾 CI 资源
昇腾 NPU 算力 · CI 节点"] + A3["【其他】账号等"] + end + + subgraph L2["② 采集"] + direction LR + B1["obs-sdk(本项目产出)"] + B2["探测 CronJob + Agent"] + B3["(待补充)"] + end + + subgraph L3["③ 存储(日志 · Region 级自持 / 指标 · 汇聚到独立中心)"] + C1["日志 · LTS 各 Region 日志流
指标 · 各集群 Agent remote_write → 独立中心 Prometheus"] + end + + subgraph L4["④ 汇聚(只读分析路径)"] + D1["日志 · Grafana + LTS 数据源(访问层汇聚 · 不搬数据)
指标 · 中心 Prometheus 天然汇聚(无需额外聚合层)"] + end + + subgraph L5["⑤ 视图(只读分析路径)"] + F1["统一大盘 · 自建 Grafana ×1(指标 + 日志)
服务健康度 · 按 community 昇腾资源水位 账号 / 配额"] + end + + subgraph L6["⑥ 告警(从 ③ 分叉)"] + direction LR + E1["日志 · LTS 告警规则
各 Region 本地、逐流配置(17 条)"] + E2["指标 · 中心 Alertmanager
一条规则覆盖全部集群(不经 SMN)"] + E3["SMN 主题 · 每 Region 一个(仅日志告警)
订阅端统一 → 邮件 / 短信 / 企微 / 钉钉 / Webhook"] + end + + A1 --> B1 + A2 --> B2 + A3 --> B3 + B1 --> C1 + B2 --> C1 + B3 --> C1 + + C1 --> D1 + D1 --> F1 + + C1 --> E1 + C1 --> E2 + E1 --> E3 + + classDef scope stroke:#d9534f,stroke-width:2px + class A1,B1 scope +``` + + +**本方案在底座上的四点定位**: + +1. **日志 —— 复用,不新建**:继续用各集群**现有的日志流**(CCE 云原生日志采集插件为每集群建的 `k8s-log-*` 组),本方案只改**日志的内容格式**(结构化 JSON + 契约字段),**不动采集管道**。 +2. **指标 —— 新建**:26 个服务目前均未暴露 `/metrics`,需从零接入;存储侧**自建 Prometheus 实例**(独立中心集群,见 §4.7),不走华为云托管。 +3. **大盘 —— 自建 Grafana**:**一个**实例同时挂中心 Prometheus(指标)+ 17 个日志数据源(日志),见 §4.7.4 / §4.6.4。 +4. **已有现成闭环可参照**:**昇腾 CI 资源块**已跑通 **多集群 CronJob + Agent 上报 → 中心 Prometheus 存储 → Grafana 大盘 + Alertmanager 告警 → 邮件接收** 的完整链路,本方案的**指标侧与之同构**(架构图见 §3.1.1,参数对照见 §4.7.2)。 + +#### 3.1.1 形态先例:昇腾 CI 资源块**现有**架构 + +> ⚠️ 本图描述的是**已跑通的存量系统**(`ascend-ci-deployment` 仓 `monitoring/`),**不是本方案要建的东西**。本方案建成后的形态见 §3.2。 + +```mermaid +flowchart TB + subgraph BIZ["业务集群 ×12 · 跨 4 个 Region
cn-north-12 ×4 | guizhou 贵州 ×6 | hongkong 香港 ×1 | wulanchabu 乌兰察布 ×1"] + direction LR + AG["Prometheus Agent
--agent(本地只抓不存)
scrape kube-state-metrics / node-exporter"] + CJ["拨测 CronJob
github-probe · cloud-account · cert-expiry …
产出 Prometheus text 格式"] + end + + subgraph CTR["中心监控集群 · infra-cn4-x86-common-cluster(beijing)· ns infra-monitoring"] + direction TB + PGW["Pushgateway
LoadBalancer + 持久化"] + PROM["Prometheus
TSDB + PrometheusRule 评估
replicas 1 · 保留 15d · 100Gi"] + AM["Alertmanager
告警路由 + 邮件模板
replicas 1"] + end + + subgraph VIEW["视图(只读)"] + GRAF["Grafana
数据源 = 中心 Prometheus"] + end + + AG -->|"① remote_write"| PROM + CJ -->|"② HTTP push"| PGW + PGW -->|"scrape"| PROM + PROM -->|"规则触发"| AM + AM -->|"SMTP"| MAIL["运维团队
HTML 邮件"] + PROM --> GRAF +``` + +**两条上报路径**是本图的关键,也是本方案要复用的形态: + +| 路径 | 机制 | 数据来源 | +| --- | --- | --- | +| **① 采集** | 各集群 **Prometheus Agent**(`--agent`,本地只抓不存)`scrape` 后 `remote_write` 到中心 | 系统 / 基础设施指标(kube-state-metrics、node-exporter) | +| **② 拨测** | 各集群 **CronJob** 跑探测脚本,产出的指标 **push 到中心 Pushgateway**,再由中心 Prometheus `scrape` | 服务进程之外的东西:外部 API 可达性、云账号余额、证书有效期、共享盘 | + +### 3.2 部署与流程视图 + +```mermaid +flowchart TB + subgraph L1["① 部署 · 服务 × 集群 × Region
26 仓 / 4 种语言(同一服务按社区拆成多套)→ 17 个集群 / 4 个 Region"] + direction LR + subgraph RG2["beijing4"] + direction TB + C21["集群1
评审机器人 · CLA"] + C22["集群2
账号"] + C23["集群3
评审机器人 · 账号"] + end + subgraph RG3["guiyang2"] + direction TB + C31["集群1
评审机器人 · CLA · 账号"] + end + subgraph RG4["hongkong"] + direction TB + C41["集群1
账号 · CLA · 评审机器人"] + end + subgraph RG1["beijing1"] + direction TB + C11["集群1
账号"] + end + end + + subgraph L2["② 存储 · 日志每集群 1 条流(各 Region 自持)/ 指标汇聚到 1 个中心集群"] + direction LR + M1["日志
集群1 · 集群2 … 集群17
→ 17 条流"] + M2["指标
Agent 模式 · 本地不存
→ remote_write"] + end + + subgraph L3["③ 大盘 / 监控 —— 两侧形态完全不同"] + direction LR + subgraph KI["指标侧"] + direction TB + KI1["大盘
中心 Grafana ×1"] + KI2["监控告警
中心 Alertmanager ×1
一条规则覆盖全部集群"] + end + subgraph KL["日志侧"] + direction TB + KL1["大盘
Grafana 挂 17 个日志数据源"] + KL2["监控告警
各 Region 本地 LTS ×4 → SMN ×4"] + end + end + + RG1 --> M1 + RG2 --> M1 + RG3 --> M1 + RG4 --> M1 + + RG1 --> M2 + RG2 --> M2 + RG3 --> M2 + RG4 --> M2 + + M1 --> KL + M2 --> KI +``` + +本项目的部署形态可先记住三个数:**26 个微服务子仓(4 种语言)→ 17 个 CCE 集群(4 个 Region)→ 上报 17 条日志流 + 1 个中心指标集群**。 + +第一个数会**放大**:**26 仓 ≠ 26 个部署实例**,同一逻辑服务按社区拆成多套——`robot-universal-review` 一个仓就有 **16 套部署**(生产 10 社区 / 7 集群),所以实际 Pod 实例数远多于 26。 + +**两侧的上报粒度完全不同**:日志侧**每集群 1 个日志组**(共 17 个,各 Region 自持,见 §4.6);指标侧**17 个集群的 Agent `remote_write` 汇聚到 1 个独立中心 Prometheus**(见 §4.7)。告警路径自存储层分叉:**指标告警在中心 Alertmanager 统一(一条规则覆盖全部集群)**,**日志告警仍逐 Region 本地 LTS 配置 → SMN**(LTS 无跨流查询,见 §4.6)。**该结构与 §3.1 一致,本图不重复展开。** + +**服务侧的接入形态**(每个 Pod 内): + +```mermaid +flowchart LR + APP["微服务进程
(SDK 已 Init)"] + APP -->|"obs-sdk 日志 · 单行扁平 JSON"| OUT["stdout"] + APP -->|"obs-sdk 指标 · Prometheus 文本"| MET["GET /metrics"] + OUT --> LA["log-agent
(节点 DaemonSet)"] + LA --> LTS["LTS"] + MET --> SM["ServiceMonitor"] + SM --> PROM["Prometheus Agent
(--agent,本地只抓不存)"] + PROM -->|"remote_write"| CTR["中心 Prometheus
(独立中心集群)"] +``` + + +### 3.3 底座选型(方案阶段已确认) + +| 能力 | 选型 | 说明 | +| --- | --- | --- | +| 日志存储 | **LTS**——**日志组按集群划分:每集群 1 个 `k8s-log-{集群ID}`,共 17 个** | 不自建 ES;容器日志写入 `stdout-{集群ID}` 日志流。**各 Region 自持,无跨日志组检索能力**(该限制决定了下方的日志告警形态) | +| 指标存储 | **自建中心 Prometheus**(kube-prometheus-stack):17 集群 Agent(`--agent`,只抓不存)`remote_write` 汇聚到**1 个中心实例** | 不自研 SDK,**但自建底座**——理由与形态见 **§4.7**。原「AOM Prometheus for CCE」路线**已弃用**(官方确认多实例聚合不支持跨 Region) | +| 日志大盘 | **自建 Grafana(同一实例)挂 LTS 日志数据源 ×17** | 与指标大盘同屏、**跨账号免多登**(§4.6.4)。✅ **接入日志流已确认可行**(2026-09-14 华为云,前提是流已结构化) | +| 指标大盘 | **自建 Grafana(1 个实例)挂中心 Prometheus**。**独立 Deployment、与中心 Prometheus 同集群**(§4.7.4) | 替代原「AOM 托管 Grafana ×4」——一个实例即可覆盖全部 17 集群(§4.7.4 / §4.6.4)。**只做看板,不进告警链路**(见下行注释) | +| 日志告警 | **各 Region 本地 LTS 内逐流配置(17 条)**,出口经 SMN 统一到订阅端 | 因 LTS **无跨流查询**,一条规则无法绑定多个日志流,**明确接受「无跨集群联合告警」**。**SMN 仅日志告警仍用**(Region 级资源),「统一」落在各 Region 主题**订阅同一接收端**,而非同一主题 | +| 指标告警 | **统一在中心 Alertmanager**(一条规则覆盖全部 17 集群) | 指标侧不再逐 Region 各配;`group_by: [cluster, alertname]` 区分来源 | + +**为什么指标告警统一、日志告警仍散**——两条链路能力不同,不是取舍不一致: + +1. **指标侧已统一**:数据经 `remote_write` 汇入中心 Prometheus,**告警规则天然覆盖全部 17 集群**,一条规则即可(`group_by: [cluster, alertname]` 区分来源)。这是自建中心相对 AOM 路线的主要收益之一(§4.7.3); +2. **日志侧统一不了**:LTS **没有跨日志组 / 流的检索能力**,一条告警规则无法绑定多个日志流——该限制已与华为云团队确认。因此日志规则只能**逐流在本地 LTS 内配置(17 条)**,且**跨集群联合条件写不出来**(如「某服务在所有社区的某类失败总数」); +3. **日志告警出口仍是 SMN**:**SMN 主题是 Region 级资源**(URN 形如 `urn:smn:::`),故「统一」不在同一个主题,而在**各 Region 主题订阅同一接收端**(邮件组 / 统一 webhook 接收服务)。 + +### 3.4 贯穿维度:community + +`community` 是本次建设的**核心维度**——支撑「按社区查看单服务健康度」。它在日志字段与指标 label 上语义一致,采用**双层注入**: + +| 层 | 场景 | 取值来源 | 日志表现 | 指标表现 | +| --- | --- | --- | --- | --- | +| **部署级默认**(静态) | 服务按社区拆实例(每实例只服务一个社区) | Init 注入:`OBS_COMMUNITY` 环境变量或显式参数 | 进程内所有日志的常驻字段 | 所有时间序列的常驻 const label | +| **请求级覆盖**(动态) | 中心化单实例服务多社区(靠路由/鉴权识别归属) | 请求上下文,由 SDK 从 context 读取 | 该请求关联日志的字段值 | 该请求打点的 series 的 label 值 | + +--- + +## 4. 详细设计 + +### 4.1 契约层(`spec/`) + +契约层是本次建设的**技术核心**:它把「多语言输出形态一致」这件事从「靠人自觉」变成「有据可查、可校验」。 + +#### 4.1.1 日志格式(`spec/log-format.md`) + +**输出形态**:每行一条日志,单行 JSON(不 pretty、不换行),UTF-8 写 stdout。 + +**顶层字段与顺序**: + +| 字段 | 必填 | 说明 | +| --- | --- | --- | +| `time` | 是 | RFC 3339 UTC,**固定毫秒精度(3 位小数)**,以 `Z` 结尾 | +| `level` | 是 | 小写枚举 `debug` / `info` / `warn` / `error` | +| `msg` | 是 | 人类可读的**常量短语** | +| `service` | 是 | 服务名(= service.yaml 的服务名) | +| `env` | 是 | `prod` / `test` / `preview` / `staging` | +| `instance` | 是 | 实例标识(k8s pod 名 / hostname) | +| `community` | 是 | 社区标识(部署级默认 + 请求级覆盖) | +| `request_id` | 否 | 单请求关联 ID | +| `trace_id` | 否(预留) | 二期 trace 接入前恒空/省略 | +| `span_id` | 否(预留) | 二期 trace 接入前恒空/省略 | +| `logger` | 否 | 调用位置 `文件:行号`(调试定位) | +| `error` | 否 | 错误信息;有异常上下文的语言可含完整 traceback | +| 其余 | 否 | 业务字段**平铺在顶层**,`snake_case`,禁止嵌套对象 | + +**两条关键传参约束**: + +1. **`msg` 必须是常量短语,禁止 printf 风格**(`log.Errorf("failed for user %s", uid)` 违规)。所有可查询维度通过 kv 传入:`log.ErrorContext(ctx, "get account failed", "user_id", uid, "error", err)`。 + *理由*:`msg` 一旦嵌值,同一事件每行都不同 → LTS 无法按 `msg` 聚合计数,日志告警规则**静默失效**;改文案即破坏已配置的检索。四语言 SDK **只提供 kv 形式,无 printf 变体**。 +2. **业务字段扁平**,不允许嵌套对象。嵌套会给 LTS 侧字段提取带来歧义。 + +> `error` 字段**不适合聚合**(取值逐次不同,Python 等语言还含 traceback)。按错误类型聚合请用 `msg` 常量 + 业务字段(如 `error_code`)。 + +#### 4.1.2 通用字段(`spec/common-fields.md`) + +7 个通用字段的来源与注入优先级: + +| 字段 | 注入层级 | 静态/动态 | 环境变量 | 内置默认 | +| --- | --- | --- | --- | --- | +| `service` | Init(进程级) | 静态 | `OBS_SERVICE` | `unknown` | +| `env` | Init(进程级) | 静态 | `OBS_ENV` | `unknown` | +| `instance` | Init(进程级) | 静态 | `OBS_INSTANCE` | **hostname** | +| `community` | Init + 请求上下文 | 双态 | `OBS_COMMUNITY` | `unknown` | +| `request_id` | 请求上下文 | 动态 | — | 无 | +| `trace_id` | 请求上下文(预留) | 动态/预留 | — | 无 | +| `span_id` | 请求上下文(预留) | 动态/预留 | — | 无 | + +取值优先级:**SDK Init 显式参数 > 部署环境变量 > 内置默认**。 +变量名统一 `OBS_*` 前缀,四语言读同一套变量名,保证部署模板(helm values)不因语言而异。 + +**`request_id` 注入策略**:可信入站头(如 `X-Request-Id`,**在服务已校验可信时**)> 中间件自动生成(UUID)> 无。出站调用应向下游传播,保证全链路同 ID。 + +#### 4.1.3 指标格式(`spec/metrics-format.md`) + +- **命名**:全小写 `snake_case`,单位后缀遵循 Prometheus 约定(`_total` / `_seconds` / `_bytes`)。 + - 业务指标强制前缀 `_`(服务短名)。例:`review_http_requests_total`。 + - 共享中间件公共指标用 SDK 保留前缀 **`obs_`**(不按 service 名开头——同一条 series 已带 `service` label,查询按 label 过滤)。例:`obs_http_server_requests_total`。 +- **通用 label 集**(每条 series 必须带):`service` / `env` / `instance` / `community`。 + - 前三个是 const label;**`community` 是唯一可动态的公共 label**。 +- **community 双层注入的指标实现**:对需要区分社区的指标,注册时把 `community` 声明为**普通可变 label**(service/env/instance 仍作 const label),打点时由 SDK 从上下文取覆盖值。**注册一次,单社区场景填默认值、多社区场景填覆盖值,两用。** +- **高基数禁令**:`request_id` / `trace_id` / `span_id` / PR number / commit sha 等**禁止**作 label,会撑爆时序基数。 +- **暴露**:每服务一个 `/metrics` 端点,HTTP `GET`,Prometheus text format。 + +#### 4.1.4 community 枚举(`spec/community-values.md`) + +18 个社区:`Ascend · BoostKit · CANN · Common · HiFloat · HPCKit · Infrastructure · Merlin · MindSpore · openEuler · OpenFuyao · openGauss · OpenJiuwen · openLookeng · OpenPangu · OpenUBMC · UnifiedBus · Xihe`。 +权威来源是 `infrastructure` 仓 `service.yaml` 的 `communities` 段,随上游刷新。 + +--- + +### 4.2 四语言 SDK + +#### 4.2.1 语言 × 能力矩阵 + +| 能力 | Go(`go/`) | Python(`python/`) | Java(`java/`) | Node(`node/`) | +| --- | --- | --- | --- | --- | +| 模块/包名 | `github.com/opensourceways/obs-sdk/go` | `obs-sdk-python` | `obs-sdk-java` | `obs-sdk-node` | +| log 底层 | stdlib `log/slog` + 自研 Handler | stdlib `logging` + 自定 JSON Formatter | logback + logstash JSON encoder | 自定 JSON serializer | +| metrics 底层 | `client_golang` | `prometheus-client` | `micrometer` + Prometheus registry | `prom-client` | +| Init | `log.Init(cfg)` / `metrics.New(cfg)` | `log.init(...)` / `metrics.init(...)` | `ObsLogging.init(cfg)` / `ObsMetrics.of(cfg)` | `log.init(opts)` / `new Metrics(opts)` | +| 请求上下文 | `sdkctx`(`context.Context`) | `_context`(contextvars) | `RequestContext`(MDC + ThreadLocal) | `context`(AsyncLocalStorage) | +| 中间件 | `middleware`(net/http)+ `ginmw`(gin) | `fastapi_wrap` / `flask_middleware` / `DjangoMiddleware` | `ObsFilter`(Servlet Filter) | `makeMiddleware`(express) | +| 服务端指标 | `obs_http_server_*`(中间件) | 同上 | 走 Actuator + Micrometer 官方 server instrumentation(不重复埋点) | `obs_http_server_*`(中间件) | +| UT 数量 | 31 | 15 | 19 | 10 | +| 已发版 | ✅ `go/v1.0.0` | ❌ 版本号 `0.1.0`,未打 tag | ❌ 版本号 `0.1.0`,未打 tag | ❌ 版本号 `0.1.0`,未打 tag | +| CI | `go vet` + `go test` | `pytest`(Py3.10) | `mvn test`(JDK 17) | `npm test`(Node 20) | + +#### 4.2.2 Go SDK 设计要点 + +```go +// 启动时 Init 一次,之后全进程用包级函数(形状对齐 kratos v3,无需在每个调用点绑定 logger) +obslog.Init(obslog.Config{Service: "review", Community: "openeuler"}) + +// 日志:msg 常量 + kv 交替(禁止 printf) +obslog.Info("job done", "event", "release", "issue", "2061") +obslog.ErrorContext(ctx, "get account failed", "user_id", uid, "error", err) // ctx 里请求字段自动附加 + +// 指标:注册一次;community label 自动排首位,取请求覆盖或回退默认 +m := obsmetrics.New(obsmetrics.Config{Service: "review"}) +built := m.NewCounterVec("built_releases", "发布的构建数", "kind") +built.IncWithContext(ctx, "tag") + +// 中间件 +h := obshttpmw.New(obshttpmw.Options{Metrics: m, ResolveCommunity: fn}).Then(myHandler) +``` + +关键实现选择: +- **自定义 slog Handler** 而非第三方库:直接控制字段、顺序、时间格式,且无需引入额外依赖。 +- **手工构造 `slog.Record` 并控制 `runtime.Callers` skip**,让 `logger` 字段指向业务调用点而非 SDK 内部包装函数。 +- 包级 API 形状对齐主流生态(`log/slog`、kratos v3),降低接入心智负担。 + +#### 4.2.3 Java SDK 设计要点 + +Java 侧有一个**非显然的坑**,必须在接入文档里强调: + +> 日志 JSON 输出**必须**用 `java/examples/logback-json.xml`,它只挂 SDK 的 `ObsJsonProvider`。**不要退回 encoder 自带的 provider** —— `` 只能输出大写、字段名不可配、且不配 `` 会整条丢弃 throwable。 + +即:Java 的字段名/顺序/时间格式/级别小写/异常堆栈**由 SDK 的 provider 保证**,不能靠 logstash encoder 的配置凑。 + +Java 的**服务端指标不重复埋点**——走 Spring Boot Actuator + Micrometer 官方 server instrumentation,把 `m.meterRegistry()` 暴露成 bean 由 Actuator 托管。 + +#### 4.2.4 已知实现问题(需修复) + +| 问题 | 影响 | 位置 | +| --- | --- | --- | +| Java `fromEnvironment()` **无任何兜底**:只读 `OBS_*`,未设置时为 `null`,而 provider 对空值「省略该键」 | `service` / `env` / `instance` / `community` 四个**必填**字段在未注入环境变量时**整个 Key 不出现**,违反契约。Go / Python / Node 均回退到 `unknown`(`instance` 回退 hostname) | `java/.../ObsSdkConfig.java:41` | +| Python / Node / Java 未打 tag | 消费方无法按版本引用 | 各语言子目录 | + +--- + +### 4.3 服务接入模式(按语言) + +#### 4.3.1 Go 服务(7 个) + +`app-cla-server` / `cve-sa-backend` / `cve-manager-ng` / `robot-universal-label` / `review` / `repo-watcher` / `welcome` + +```go +obslog.Init(obslog.Config{Service: ""}) +m := obsmetrics.New(obsmetrics.Config{Service: ""}) + +// HTTP 服务端:gin 用 ginmw,net/http 用 obshttpmw.New(...).Then(handler) +r.Use(ginmw.Middleware(ginmw.Options{Metrics: m})) +r.GET("/metrics", gin.WrapH(m.Handler())) + +// 指标:基础名注册,community label 由 SDK 自动补、排首位 +prHandled := m.NewCounterVec("pr_handled", "处理的 PR 事件数", "action") + +// 请求内:community 在可信判定点解析后覆盖,request_id 注入 +ctx = sdkctx.WithCommunity(ctx, "openEuler") +ctx = sdkctx.WithRequestID(ctx, "req-123") +obslog.InfoContext(ctx, "handle request", "repo", repo) +prHandled.IncWithContext(ctx, "opened") +``` + +#### 4.3.2 Java 服务(6 个) + +`EasySearch` / `EasySearchImport` / `om-webserver` / `certification-server` / `easysoftware-autoupgrade` / `APIMagic`,均 Spring Boot 3.x: + +```java +ObsSdkConfig cfg = ObsSdkConfig.fromEnvironment(); // 读 OBS_* +ObsLogging.init(cfg); // 部署级字段写入 MDC +ObsMetrics m = ObsMetrics.of(cfg); // meterRegistry() 暴露为 bean,交 Actuator 托管 + +log.info("job done"); // SLF4J 输出走 logback-json.xml(挂 ObsJsonProvider) + +// 请求内:可信判定点解析 community 后 push +try (RequestContext.Scope s = RequestContext.push("openEuler", "req-123", null)) { + m.counter("pr_handled", "处理的 PR 事件数", "action").inc("opened"); +} +``` + +`logback-json.xml` 见 [java/examples/logback-json.xml](../java/examples/logback-json.xml);`/metrics` 由 Actuator 暴露(打开 `prometheus` endpoint),不重复埋服务端指标。 + +#### 4.3.3 Python 服务(11 个) + +```python +from obs_sdk import log, metrics, _context +from obs_sdk.middleware import fastapi_wrap # Flask: flask_middleware;Django: DjangoMiddleware + +log.init(service="") # env/community 由 SDK 读 OBS_*,进程内幂等 +metrics.init(service="") +fastapi_wrap(app, resolver=resolve_community) # 注入 request_id + 可信判定点解析 community + 记 obs_http_server_* + +logger = log.get_logger(__name__) +logger.info("job done", extra={"event": "release", "issue": "2061"}) + +with _context.bind(community="openEuler", request_id="req-123"): + metrics.counter("pr_handled", "处理的 PR 事件数", ["action"]).inc(1, action="opened") +``` + +`/metrics` 暴露:`return Response(content=metrics.generate_text(), media_type=metrics.content_type())`。 + +适配器按框架分三类:Django×3(meeting-center / meeting-platform / app-meeting-server)、FastAPI×4(oss-map / robot-issue-manage / hotopic-data-clean / om-dataarts)、Flask×1(forum-reply-robot)。**另 3 个非标准框架无现成适配器,需单独方案**:mailman(GNU Mailman)、copr_docker(Fedora COPR)、hotopic-mining(脚本/包型)。 + +#### 4.3.4 Node 服务(1 个) + +`etherpad-lite` 是**上游开源项目**(Etherpad,Express + Socket.IO)。接入需注意与上游代码保持可合并性,改造面要尽量小。 + +#### 4.3.5 不直接接入 + +`meeting-server` 是编排/部署型子仓(Shell + Go Template),由旗下子服务(app-meeting-server / meeting-center / meeting-platform)覆盖。 + +--- + +### 4.5 部署侧字段注入 + +日志/指标要带上 `env` 和 `community`,必须由部署侧注入(pod 内无免费信号可推): + +- `instance` **不用注入** —— SDK 默认取 hostname(k8s 下 = pod 名)。 +- `service` **不用注入** —— 编译期常量,代码 `Init` 里写死。 +- **`env` / `community` 必须注入** —— 无法从 pod 名、namespace、节点名、镜像 tag 推导。 + - namespace 推不出:`openEuler` 的 prod-02 与 test 都叫 `robot-openeuler`;`OpenUBMC` 的 prod 与 test 都叫 `robot-openubmc`。 + - 集群名推不出:它是 ArgoCD 规范名,**pod 内无法自省**。 + +**注入方式**:在 `Open-Infra-Ops/helm-chart-value` 各部署目录的 `values.yaml` 里,给对应 deployment 加: + +```yaml + env: + - name: OBS_ENV + value: prod + - name: OBS_COMMUNITY + value: ascend +``` + +(chart 已支持 env 透传,同文件 `sync-bot` 在用同款写法。) + +**值域口径**: +- `community` **统一小写**(依据 `spec/community-values.md`;四语言 SDK 都不做大小写归一,注入值即最终形态)。 +- `env` 需 `prod*` → `prod` 的映射(service.yaml 里存在 `prod-github`、`prod-02` 等,不在契约枚举内)。 + +**范围(以 `robot-universal-review` 为例)**:需改 **12 个部署目录**(不是 10 个——Common 与 Ascend 各有两套部署,同社区同值)。生成脚本要**按部署目录迭代,不能按社区迭代**。 + + +--- + +### 4.6 日志链路 + +日志侧**复用现有底座,不新建**:采集管道沿用 CCE 云原生日志采集插件,存储沿用 LTS。本方案只改**日志的内容格式**(结构化 JSON + 契约字段),不动采集管道。 + +#### 4.6.1 日志采集 + +**形态**:复用 CCE 云原生日志采集插件(`log-agent`,基于 fluent-bit,节点 DaemonSet)。集群级 `default-stdout` 策略已覆盖**全 ns 全容器 stdout**,铺开**无需新增采集策略**——服务只要写 stdout 就进 LTS。 + +#### 4.6.2 日志存储 + +**形态:LTS,各 Region 自持**,不自建 ES。日志组按集群划分,每集群 1 个 `k8s-log-{集群ID}`,容器日志写入 `stdout-{集群ID}` 流——**共 17 组 / 17 流**。 + +**⚠️ LTS 无「跨日志组检索」**:搜索、快速查询及其列表的 API 全部以 `groups/{group_id}/topics/{topic_id}` 限定在**单个日志组 / 流**内,控制台也是逐组进入详情页。统一入口只能靠 Grafana 多数据源拼装。 + +**结构化解析**:只针对 **obs-sdk 契约格式**配 **JSON 规则**。 + +**容量口径**:单流 **100 MB/s** 为**软限制**(官方标 `Not mandatory`,超限不是拒绝写入,而是**不保证 QoS / 可能丢日志**,且**无补发语义**);另有单流 **500 次/秒**写入次数上限。⚠️ 该数字随版本变化大(旧版文档写的是 5 MB/s + 25K 条/秒),**容量规划须按各 Region 实际版本文档核,不要引用本文数字**。 + +**其他约束**:一个日志流只能配**一种**结构化方式(ICAgent 结构化**或**云端结构化,切换**必须先删除**原配置);**结构化配置修改只对新写入生效**,历史数据不按新规则重新解析;云端结构化解析**消耗 LTS 算力**,官方称未来按日志量收取「日志加工流量费」。 + +日志读写限制:https://support.huaweicloud.com/productdesc-lts/lts-0718.html + +#### 4.6.3 日志告警 + +**形态:仍走华为云 LTS 原生,各 Region 逐流配置(17 条)**,出口经**各 Region 的 SMN 主题** → 同一接收端(邮件组 / 统一 webhook 接收服务)。 + +#### 4.6.4 日志大盘 + +**形态:并入自建 Grafana(1 个实例),挂 LTS 数据源 ×17**(每个数据源绑一个日志流 ID,各带一套 AK/SK 凭证)。 + +> ✅ **硬依赖已确认(2026-09-14,华为云团队)**:**自建 Grafana 可以接入各集群的日志流**,**前提是日志流已配置结构化解析**。 +> 至此日志大盘链路上的**唯一卡点就是「结构化解析」本身**: +> +> ```text +> 日志大盘能否落地 = 自建 Grafana 接入 ✅(已确认) AND 流已结构化 ❓(唯一卡点) +> ``` + +**为什么用自建 Grafana 而不是 AOM 托管 Grafana ×4**:LTS 插件用 **AK/SK 认证,每个数据源可带自己的一套凭证**,所以**一个实例就能横跨 N 个华为云账号**、把 17 个日志流全挂上,**免多登**;而 AOM 托管 Grafana 是**账号级资源**,N 个账号要登录 N 次。且指标侧本就要自建 Grafana(§4.7.4),日志接进来是**边际成本近乎为零**(多配 17 个数据源)。 + +**面板筛选策略**: + +| 面板 | `community` | `service` | 理由 | +| --- | --- | --- | --- | +| 明细 / 排障 | **必选(多选,至少 1)** | **必选(多选,至少 1)** | 排除误伤(全量采集下流里混有基础设施 / sidecar / 第三方日志);两字段合用 ≈ **精确定位到一个部署**。**必须多选**——单选会堵死「互调服务联合排查」 | +| 聚合 / 对比 | 默认全选 | 默认全选 | 跨社区横向对比(如「某服务在所有社区的表现」)是真实需求,且结果集小 | + +> **为什么日志侧必选、指标侧不强制**:**「必选筛选」本质是给「日志存储物理分裂成 17 个流」打的补丁**;指标侧存储本就聚合(§4.7.4),不需要这个药。 + +**已知代价(需明确接受,不是意外)**: + +1. **`robot-framework-lib` 的 121 处 logrus 打点(client 88 / config 14 / interrupts 11 / framework 6 / utils 2)在大盘上不可见**——其字段集无 `community` / `service`,被必选筛选条件排除; +2. **配额 / 存储不设防**——全量落流; +3. **聚合 / 对比面板要打 17 个数据源**,加载慢,任一失败则面板残缺。 + +--- + +### 4.7 指标链路 + +指标侧**与日志侧相反:底座新建**。26 个服务目前均未暴露 `/metrics`,存储侧自建 Prometheus 实例,不走华为云托管。 + +核心结论:**弃用「AOM Prometheus for CCE + 多账号聚合」路线,改为「自建中心 Prometheus 集群」**——原路线经官方确认**不支持跨 Region 聚合**,拿不到统一大盘与统一告警。 + +#### 4.7.1 指标采集 + +**形态**:各集群部署 **Prometheus Agent**(`--agent`,**本地只抓不存**,本地只有 WAL),以 ServiceMonitor 选目标、抓各服务 `/metrics`,再 `remote_write` 到中心。形态照搬资源块(见 §4.7.2),但**独立部署、不复用其实例**——资源块 Agent 覆盖的是 CI 集群,与服务块 prod 集群基本不重叠;资源块的价值是**形态先例与配置模板**。 + +**⚠️ 跨 Region 链路是本方案的唯一硬门槛**(各集群 Agent → 中心): + +- 必须解决:(a) 传输加密(至少 HTTPS);(b) 合规(生产指标数据出公网需确认);(c) 带宽与成本。 +- 候选路径:**云连接 CC**(按带宽计费、不过公网)/ **专线** / **HTTPS + 公网 EIP**。 +- ⚠️ **不得照搬资源块的公网明文 HTTP**(`http://113.44.182.82:9090` + Basic Auth)。**该链路不成立则整个中心方案不成立。** + +#### 4.7.2 指标存储 + +**形态:1 个独立中心 Prometheus 集群**(kube-prometheus-stack,放独立中心集群)。17 个集群的 Agent 经 `remote_write` 汇入同一实例,`enableRemoteWriteReceiver: true`。 + +| 项 | 决定 | +| --- | --- | +| **HA** | `replicas: 2` + pod 反亲和(资源块当前为 `1`,其 values 注释已写明「正式上线时恢复 `replicas: 2`」)。**它是单点,挂了全网指标断线** | +| **保留期** | 一期**本地存储 30d,不上 VictoriaMetrics**——先跑一两个月看真实 series 增长再定,避免过早引入额外组件 | +| **安全** | `enableRemoteWriteReceiver: true` + 公网端口 = **拿到 basic auth 就能往中心写任意指标**。生产至少要做:强凭据 + 轮换、源 IP 白名单 / 安全组收敛、只放行 `/api/v1/write`。**独立集群的好处正是把风险面收敛到这一个集群上** | + +**为什么弃用 AOM 路线**: + +| 维度 | AOM Prometheus for CCE(原路线) | 自建中心 Prometheus(现路线) | +| --- | --- | --- | +| **跨 Region 统一大盘** | ❌ 官方已确认**多实例聚合不支持跨 Region** | ✅ 天然成立(就是**一个**实例) | +| **跨 Region 统一告警** | ❌ 需逐 Region 各配一套(4 套规则) | ✅ **一条规则覆盖全部集群** | +| **跨账号** | 走华为云多账号机制 | ✅ 每个 Agent 各带一套凭证即可,**与账号归属无关** | +| **运维** | 云托管,**零运维** | ❌ **自行运维**(HA / 容量 / 升级 / 备份) | +| **网络** | 零改造 | ❌ **需打通各集群 → 中心**并解决合规 | + +**取舍**:用**运维与网络成本**,换**统一大盘、统一告警前提**。 + +**容量粗算**(`磁盘 ≈ 保留秒数 × 每秒样本数 × 1.7 B`,按 `scrapeInterval: 60s`): + +| active series | 日增 | 30d 占用 | +| --- | --- | --- | +| 20 万 | ≈ 0.5 GB/天 | ≈ 15 GB | +| 200 万 | ≈ 4.9 GB/天 | ≈ 147 GB | + +**series 数是这里唯一的未知量**,取决于 26 个服务的指标设计——**尤其不要把高基数塞进 label**(§4.1.3 铁律:`request_id` / `trace_id` / `span_id` 禁止当 label)。 + +#### 4.7.3 指标告警 + +**形态:统一在中心 Alertmanager**——数据已经汇聚,**一条规则覆盖全部 17 集群**,用 `group_by: [cluster, alertname]` 区分来源。**不经 SMN**(SMN 仅日志告警仍用)。 + +这是自建中心相对 AOM 路线的**主要收益之一**:原路线需逐 Region 各配一套,阈值与规则变更要多点同步、易产生配置漂移。 + +#### 4.7.4 指标大盘 + +**形态:自建 Grafana(1 个实例)挂中心 Prometheus**,**独立 Deployment、与中心 Prometheus 同集群同 ns**。 + +| 设计点 | 说明 | +| --- | --- | +| **「独立」的价值在生命周期解耦,不在物理位置** | 独立 Deployment 已让 Grafana 的升级 / 重启 / 回滚不碰 Prometheus,反之亦然——这是真正需要的隔离 | +| **同集群走集群内 svc 查询** | `prometheus-kube-prometheus-prometheus.infra-monitoring.svc:9090`,零网络成本、零延迟。Grafana 是查询密集型无状态服务,离 Prometheus 越近越好 | +| **故障域与告警解耦** | 关键在**告警用 Alertmanager、不用 Grafana Alerting**——Grafana 挂了只是看不了图,**告警不受影响** | +| **不单开一个集群** | 多一份集群底座成本,还**新增一条跨集群链路**(Grafana → Prometheus) | + +**两点可照抄资源块**:Grafana PVC 只要 **10Gi**(dashboard 走 ConfigMap provisioning,PVC 只存状态);**和 Prometheus 复用同一个 ELB、端口分开**,省一个 ELB。 + +**面板筛选策略**:`community` **默认全选、不强制**——中心 Prometheus 存储已聚合(全部 17 集群汇入同一实例),不筛选无查询放大问题;`community` 是低基数 label,`sum by (community)` 成本可忽略。**必选会禁掉横向对比,而横向对比恰是指标大盘最有价值之处**;担心误读时在面板标题显示当前筛选值即可。 + +**大盘内容**:服务健康度(QPS / 错误率 / 延迟 / 资源)+ 日志检索(与 §4.6.4 同屏,跨账号免多登),支持按 `community` 过滤 / 分组。**「看」与「告警」解耦**——大盘只影响可见性,采集 / 存储 / 告警均不依赖它。 + +#### 4.7.5 社区大盘 + +这是**与内部指标大盘并列的另一个视图**,本方案需同时给出: + +| | 指标大盘(§4.7.4) | 社区大盘(本节) | +| --- | --- | --- | +| **读者** | 运维 / 研发 | 社区用户 | +| **回答的问题** | **为什么坏**(QPS / 错误率 / 延迟 / 资源) | **哪块坏了**(组件是否正常、异常落在哪一段) | +| **形态** | Grafana 面板 | GitHub Status 形态的状态页 | +| **粒度** | 服务 × community × 集群 | **按社区聚合的组件级健康度** | + +**目标**:一眼看到**整体与各组件**(从重点微服务中挑选,如评审机器人、账号、CLA 等)是否正常、异常落在哪一段,替代当前「靠零散告警与人肉打听拼凑判断」。 + +**与 Grafana 解耦**:状态页是**对外**入口,不应把内部 Grafana 直接暴露出去(多租户、安全域、可用性要求都不同)。落地形态(静态页 + 定时拉取 / 轻量服务)另议。 + +**待定**: + +1. **组件级健康的判定口径**——几个指标、什么阈值算「异常」,需与各社区运维对齐; + +--- + +### 4.8 Prometheus 中心集群与 Agent 搭建 + +指标底座要搭**两处**:一处**中心**(1 个),一处**每个业务集群**(17 个)。都走 **kustomize + ArgoCD**——目录组织照抄资源块:`base/` 放公共清单,`config-for-<集群>/` 放该集群的 patch 与 Secret,一份 Application 对应一个集群。 + +#### 4.8.1 中心集群(1 处) + +**位置**:**独立中心集群**(新申请,不与任何业务集群复用),ns `infra-monitoring`——与 Alertmanager、Grafana(§4.9)同 ns,走集群内 DNS。 + +**组件**:一个 `kube-prometheus-stack` 出齐 Prometheus + Alertmanager + Operator,**关掉 chart 自带的其他组件**——`grafana.enabled: false`(Grafana 独立部署,§4.9)、`kubeStateMetrics` / `nodeExporter` / `kubeApiServer` / `kubelet` / `kubeControllerManager` / `kubeScheduler` / `kubeProxy` / `kubeEtcd` 全关(基础设施指标复用云上,§1.3 G5),`defaultRules` 里对应规则一并禁用。 + +| values 关键项 | 取值 | 理由 | +| --- | --- | --- | +| `prometheusSpec.replicas` | **2** + pod 反亲和 | 中心是单点,挂了全网指标断线(§4.7.2) | +| `retention` | **30d** | 一期本地存储,先看真实 series 增长再定;>30d 才考虑 VictoriaMetrics | +| `scrapeInterval` | 60s | 与 Agent 侧一致,§4.7.2 的容量公式按此口径 | +| `enableRemoteWriteReceiver` | **true** | 接收 17 集群 Agent 的写入 | +| `enableFeatures` / `tsdb.outOfOrderTimeWindow` | `out-of-order-ingestion` / `10m` | 跨 Region 写入乱序是常态,不开会丢点 | +| `storageSpec` | `storageClassName: csi-disk`、**100Gi** | 规格按 §4.7.2 公式核算后回填 | +| `service.type` | `LoadBalancer` + ELB ID | 对外入口,供各集群 Agent 写入 | + +**入口与安全**(§4.7.2 的安全项落到配置): + +- ⚠️ **不照搬资源块的公网明文 HTTP**——资源块当前是 `http://:9090` + Basic Auth,而其 TLS 清单(`prometheus-remote-write-certificate` / `-ingress`)在 `kustomization.yaml` 中**被注释、标为 Phase 2**。本方案**搭建时就要上 HTTPS**(§4.7.1),不留这个尾巴。 +- 凭据走 Secret 挂**文件**(`password_file`),不进 values 明文;配合源 IP 白名单 / 安全组收敛,**只放行 `/api/v1/write`**。 +- 把暴露面收敛到这一个集群,正是独立中心相对 AOM 的好处(§4.7.2)。 + +**PrometheusRule** 与 **Alertmanager 配置**(含邮件模板 / 接收人)也集中在中心定义,一条规则覆盖全部 17 集群,`group_by: [cluster, alertname]` 区分来源(§4.7.3)。 + +#### 4.8.2 各业务集群 Agent(17 处) + +**形态**:`prometheus --agent`(**本地只抓不存**,只有 WAL),`scrape` 完 `remote_write` 到中心。每个集群一套,用 `config-for-<集群>/` 打 patch 注入本集群的 `cluster` label 与写入凭据。 + +| 配置项 | 做法 | +| --- | --- | +| **采集目标** | ServiceMonitor 选各服务 `/metrics`(按 ns / label 选,**服务侧无需改配置**);基础设施指标仍复用云上,Agent 不重复抓 | +| **来源标注** | `cluster` label 是告警与大屏 `group_by` 的依据,在 Agent 侧统一打上 | +| **写入地址** | 中心 Prometheus 的写入端点,**HTTPS**(见 §4.8.1) | +| **鉴权** | `basic_auth` + `password_file` 挂 Secret,**每集群一套凭据**——跨账号场景各带各的,与账号归属无关(§4.7.2) | +| **写入瘦身** | 用 `write_relabel_configs` drop 无用 label(资源块的做法是 drop `container` / `container_id` / `uid`)——直接省中心磁盘与跨 Region 流量 | +| **资源限制** | 常驻但轻量,按集群规模给 requests / limits | + +**可照抄资源块**:`prometheus-agent-deployment.yaml`、`prometheus-agent-configmap.yaml`、RBAC / SA 清单,以及「`base/` + `config-for-*/`」的目录组织。**不可照抄**的只有写入地址与鉴权方式。 + +#### 4.8.3 搭建顺序 + +1. 中心集群起 `kube-prometheus-stack`(Prometheus + Alertmanager),开 `enableRemoteWriteReceiver`; +2. **先把跨 Region 网络打通再铺 Agent**——它是本方案唯一硬门槛(§4.7.1),链路不通则后面全白做; +3. 选 **1 个集群**试点 Agent:验证 `remote_write` 写入、`cluster` label、带宽与容量; +4. 批量铺 17 集群(脚本化生成 kustomize 目录 / ArgoCD Application); +5. 用实测 series 量**回填**中心磁盘规格与保留期(§4.7.2)。 + +--- + +### 4.9 Grafana 看板搭建 + +**形态**:**1 个** Grafana 实例,与中心 Prometheus **同集群同 ns**、**独立 Deployment**(§4.7.4)。一个实例同时挂**中心 Prometheus + 17 个 LTS 数据源**,指标大盘与日志大盘同屏(§4.6.4)。 + +#### 4.9.1 部署 + +清单组成照抄资源块 `monitoring/grafana/`,用 kustomize 管: + +| 清单 | 作用 | 资源块取值(可直接参照) | +| --- | --- | --- | +| `deployment.yaml` | 实例本体 | `replicas: 1`、`strategy: Recreate`(PVC 是 RWO)、`runAsNonRoot`、**镜像钉版本** | +| `pvc.yaml` | 只存 Grafana 自身状态 | **10Gi**——dashboard 走 ConfigMap provisioning,不占 PVC(§4.7.4) | +| `service.yaml` | 入口 | `LoadBalancer`,**复用中心 Prometheus 的那个 ELB、端口分开**,省一个 ELB | +| `grafana-datasource.yaml` | 数据源 provisioning | 见 §4.9.2 | +| `grafana-dashboard-provider.yaml` | dashboard 加载器 | 从 `/etc/grafana/dashboards/json` 读;`disableDeletion: true` + `editable: false` | +| admin Secret | 管理员密码 | Secret 注入,不进 values | + +#### 4.9.2 数据源 + +| 数据源 | 类型 | 接入方式 | +| --- | --- | --- | +| **中心 Prometheus** | prometheus | 集群内 DNS(`http://prometheus-kube-prometheus-prometheus.infra-monitoring.svc:9090`)、`access: proxy`、`isDefault: true`——零网络成本、零延迟(§4.7.4) | +| **LTS 日志流 ×17** | 华为云 LTS 插件 | **每个数据源绑一条日志流、各带一套 AK/SK**(§4.6.4)——正因为凭证是数据源级的,一个实例才能横跨 N 个账号、**免多登** | + +> ⚠️ LTS 侧的前提是**日志流已配结构化解析**(§4.6.2)——该前提不成立时,数据源挂上了也查不出结构化字段。 + +#### 4.9.3 看板 + +**dashboard 用代码管**:JSON 放 Git,经 `configMapGenerator` 生成 ConfigMap 挂进容器——**改看板 = 提 PR**,可 review、可回滚。资源块放了 2 块(`CI-Network-Overview` / `Pod-Network-Traffic`),本方案要做的: + +| 看板 | 内容 | 依据 | +| --- | --- | --- | +| **服务健康度大盘** | QPS / 错误率 / 延迟 / 资源,按 `community` 过滤 / 分组(默认全选、不强制) | §4.7.4 | +| **日志检索** | 与指标**同屏**;`community` / `service` **必选**筛选,排除基础设施 / sidecar / 第三方日志 | §4.6.4 | +| **聚合 / 对比** | 跨社区横向对比(默认全选) | §4.6.4 | + +**「看」与「告警」解耦**:Grafana 只做看板,**不进告警链路**——告警走中心 Alertmanager(§4.7.3),Grafana 挂了只是看不了图。 + +### 4.10 典型场景的问题处理路径 + +本节给出**两个端到端场景**——一个从**用户**出发,一个从**研发**出发——串起 §4.6 日志链路与 §4.7 指标链路,说明这套建设落地后**「问题怎么被发现、怎么被定位」**与今天有什么不同。 + +#### 4.10.1 用户视角:MindSpore 社区的评审机器人不响应 + +**场景**:贡献者向 MindSpore 社区提交 PR,机器人评审迟迟没有响应。 + +**今天**(§1.1 描述的问题):贡献者**无法区分**三种可能——机器人挂了、PR 还在排队、还是自己的提交方式有问题。唯一的办法是到社区群里问,或**逐个私聊研发**「机器人是不是又挂了」;研发同样没有全局视图,被问到了才去登机器翻日志。 + +**建设后**: + +| 步骤 | 动作 | 用到的建设成果 | +| --- | --- | --- | +| 1 | 打开**社区健康度大盘** | §4.7.5 社区大盘(对外状态页) | +| 2 | 看到 **MindSpore 社区的「评审机器人」组件异常**,同屏其余组件正常 | 按社区聚合的**组件级**健康度 | +| 3 | 得出结论:**是机器人侧的问题,不是自己 PR 的问题**;同时看到异常落在哪一个社区 | 状态页回答的是「**哪块坏了**」,而非「为什么坏」 | +| 4 | 按状态页指引等待或上报,**不再逐个联系研发** | — | + +**收益**:把「用户盲猜 + 骚扰研发」变成「用户自助查询状态页」;研发也从「是不是挂了」这类问询中解放出来。 + +#### 4.10.2 研发视角:CLA 签署失败 + +**场景**:`app-cla-server`(§4.3.1)出现 CLA 签署失败率上升。 + +**今天**:只能等**用户来报障**(「签不了 CLA」)。研发拿到的是一个模糊现象,需要登机器**逐个实例 grep 日志**,且很难判断问题落在**自己的代码**还是**下游数据库**。 + +```mermaid +flowchart LR + alert["① 告警:CLA 失败率超阈值
(自带 cluster / alertname)"] + dash["② 指标大盘
失败率↑、QPS 正常
→ 定位到服务"] + log["③ 日志检索(同屏)
connection refused
→ 定位到数据库"] + ops["④ 联系运维
带集群 / 社区 / 错误原文"] + recover["⑤ 服务恢复
告警自动消解"] + + alert --> dash --> log --> ops --> recover +``` + +| 步骤 | 动作 | 用到的建设成果 | +| --- | --- | --- | +| 1 | 收到 **CLA 失败率告警**(邮件 / 统一 webhook),告警自带 `cluster` / `alertname` 标签 | §4.7.3 中心 Alertmanager,`group_by: [cluster, alertname]` | +| 2 | 打开**指标大盘**,按 `service=app-cla-server` + `community=<受影响社区>` 筛选 | §4.7.4 指标大盘(G3:支持按 community 过滤 / 分组) | +| 3 | 看到**失败率曲线抬升、QPS 与延迟正常** → 排除流量突增与上游调用,**确认是服务自身异常** | §4.2–§4.3 SDK 埋点的业务指标 | +| 4 | 同屏切到**日志检索**,沿用同一组筛选条件,看到 `error: get account from db: connection refused` → **确认故障在数据库,而非代码逻辑** | §4.6.4 日志大盘(Grafana 同实例挂 LTS ×17,免多登) | +| 5 | 带着「哪个集群 / 哪个社区 / 什么错误原文」**直接联系运维**处理 DB | — | +| 6 | 运维处理后**失败率回落、告警自动消解**,无需人工消警 | §4.7.3 告警基于实时汇聚的指标 | + +**关键在第 3→4 步**:这是今天耗时最长、也最靠人肉的一段。建设后它变成**同一个 Grafana 界面内的连续下钻**——用指标定位「是哪个服务」,用日志定位「是什么原因」,且**跨社区 / 跨集群无需切换登录**(§4.6.4 的 AK/SK 多数据源)。 + +**本场景对 §4.6 / §4.7 的覆盖**: + +| 链路 | 在本场景中的作用 | +| --- | --- | +| §4.7.1 指标采集 | 服务指标上报到中心 | +| §4.7.3 指标告警 | 失败率超阈值触发,且带 `cluster` 定位来源 | +| §4.7.4 指标大盘 | 确认「是服务异常、不是流量问题」 | +| §4.6.4 日志大盘 | 确认「异常在数据库、不是代码」 | +| §4.6.2 日志存储(结构化解析) | 日志能按 `service` / `community` 检索的**前提**(⚠️ 前提:日志流已配结构化解析) | + +--- + +## 5. 工作量预估 + +> **口径说明**:以下为**粗估**,单位人天,含开发 + 自测 + 联调,不含需求评审与排期等待。 +> 单价基于试点实测数据校准前的一般经验;**建议在子任务 A 的试点完成后回填真实数据再修正 C 的估算**。 + +### 5.1 子任务 A —— SDK 建设与试点接入(#2061) + +| 项 | 状态 | 剩余人天 | +| --- | --- | --- | +| 契约层 `spec/`(4 份文档) | ✅ 已完成 | 0 | +| obs-sdk-go(log / metrics / sdkctx / middleware+ginmw,31 UT) | ✅ 已完成,已发 `go/v1.0.0` | 0.5 | +| obs-sdk-python(15 UT) | ✅ 代码完成 | 0.5(打 tag + 安装验证) | +| obs-sdk-java(19 UT) | ✅ 代码完成,**存在无兜底缺陷** | 1.5(缺陷修复 + UT + 打 tag) | +| obs-sdk-node(10 UT) | ✅ 代码完成 | 0.5(打 tag) | +| 试点接入:`robot-universal-review` 日志([#2161](https://github.com/opensourceways/backlog/issues/2161)) | 📋 已开 issue | 1.5 | +| 试点接入:`robot-universal-review` 指标([#2162](https://github.com/opensourceways/backlog/issues/2162)) | 📋 已开 issue | 1.5 | +| 部署侧 `OBS_*` 注入(helm-chart-value,12 个目录) | 📋 待办 | 2~3(含跨团队确认) | +| **小计** | | **8~9.5** | + +### 5.2 子任务 B —— 底座搭建与大盘告警(#2062) + +| 项 | 人天 | +| --- | --- | +| 中心 Prometheus 集群搭建(kube-prometheus-stack + HA + 存储 + Alertmanager) | 5~8 | +| 各集群 Prometheus Agent 铺开(17 集群)+ 采集链路验证 | 4~6 | +| **跨 Region 网络打通**(CC / 专线 / 公网 HTTPS 三选一)+ 安全加固 | 3~6 | +| 自建 Grafana 部署(独立 Deployment + 持久化 + 入口) | 2~3 | +| Grafana 大盘(服务健康度 + community 维度 + 日志检索面板) | 6~9 | +| 中心 Alertmanager 指标告警规则 + LTS 日志告警(17 条)+ SMN 通知 | 4~6 | +| 容量与成本核算(series 量 / 磁盘 / 跨 Region 流量) | 1~2 | +| **社区健康度大盘(G6)**——判定口径 + 数据聚合 + 状态页 + 发布流程 | 7~12 | +| **小计** | **32~52** | + +### 5.3 子任务 C —— 26 仓全量铺开与验收(#2063) + +按语言与框架的改造复杂度分档: + +| 服务分组 | 数量 | 单价(人天) | 小计 | +| --- | --- | --- | --- | +| Go · 机器人服务(robot-universal-label / repo-watcher / welcome + 试点已做的 review) | 3 | 1~1.5 | 3~4.5 | +| Go · 普通 Gin(app-cla-server / cve-sa-backend / cve-manager-ng) | 3 | 0.5~1 | 1.5~3 | +| Java · Spring Boot(Actuator 原生路径) | 6 | 1~1.5 | 6~9 | +| Python · Django + DRF | 3 | 1~1.5 | 3~4.5 | +| Python · FastAPI | 4 | 0.5~1 | 2~4 | +| Python · Flask | 1 | 0.5~1 | 0.5~1 | +| Python · **非标准框架待评估**(mailman / copr_docker / hotopic-mining) | 3 | 3~5 | 9~15 | +| Node · etherpad-lite(上游开源,需保持可合并) | 1 | 2~3 | 2~3 | +| 编排仓 meeting-server(由子服务覆盖) | 1 | 0 | 0 | +| 逐服务验收核对(字段 / label / 大盘) | — | — | 3~5 | +| **小计** | **26** | | **30.5~49** | + +### 5.4 汇总 + +| 子任务 | 人天 | 对应 issue | +| --- | --- | --- | +| A · SDK 建设与试点接入 | 8~9.5 | #2061 | +| B · 底座搭建与大盘告警 | 32~52 | #2062 | +| C · 26 仓全量铺开与验收 | 30.5~49 | #2063 | +| **合计** | **70.5~110.5** | 约 **3.5~5.5 人月** | + +**估算的最大不确定性**在 C 的「非标准框架待评估」三项(9~15 人天)与 Java 组(6~9 人天)。建议: +1. 试点完成后用实测数据回填单价; +2. 三项非标准框架服务(mailman / copr_docker / hotopic-mining)与 Node 的 etherpad-lite **先做技术验证再报价**,避免按经验值低估; +3. 「26 仓」中 `hotopic-data-clean` / `hotopic-mining` 的 prod 目前挂在 **test 集群**(`infra-hk-test-cluster-001`),`oss-map` / `om-dataarts` 在 service.md 无 prod 部署记录 —— **这 4 个是否纳入本次铺开需先澄清**,否则 C 的范围可能变动。 + +--- + +## 6. 风险与待确认 + +| # | 项 | 影响 | 处置 | +| --- | --- | --- | --- | +| R1 | ~~**15 天存储窗口**:AOM 指标默认存 15 天(超期按量计费)~~ → **【2026-09-14 转换】** 自建中心 Prometheus **保留期可自定**(建议 30d),不再受 15 天约束(§4.7.2) | ~~故障回溯窗口可能不足~~ **风险消除**;但**磁盘容量成为新的约束** | 按 §4.7.2 公式核算 series 量 → 定保留期与磁盘规格;>30d 需求再上 VictoriaMetrics | +| R2 | ~~ServiceMonitor 在 4 Region 的可用性~~ → **【转换 · 2026-09-14 扩容】各集群 Agent → 中心集群的采集链路、网络可达性与安全合规**未经生产验证,**这是本方案的唯一硬门槛**(§4.7.1) | 指标可能采不上——**且这次没有云托管兜底**;链路不成立则**整个指标自建方案不成立**(损失的是全部指标) | 试点阶段**最优先**确定网络方案(CC / 专线 / 公网 HTTPS)并**逐集群**验证 Agent 与 `remote_write` 连通性;同步做传输加密与源 IP 收敛。⚠️ **不得照搬资源块的公网明文 HTTP**(`http://113.44.182.82:9090`) | +| R3 | ~~自定义指标采样点计费量级未知~~ → **【转换】自建中心的容量与成本**:series 量、磁盘、**跨 Region 流量费**三项均未测算 | 26 仓铺开成本不可控(原为按量计费,现为**自建资源 + 出网流量**) | 试点阶段实测 17 集群真实 series 量后外推(§4.7.2 已给容量公式);跨 Region 流量按所选网络方案(CC 按带宽 / 公网按流量)分别核算 | +| R5 | **Java SDK 无兜底**,未注入环境变量时 4 个必填字段整个缺失 | 违反契约,Java 服务日志字段不全 | 单独立项修复(加 hostname / `unknown` 兜底 + 补 UT,断言落在真实 JSON 输出上) | +| R6 | Python / Node / Java 未打 tag,Java 进私服路径未验证 | 消费方无法按版本引用 | 随各语言首次接入服务时一并处理 | +| R8 | Java SDK 的 logstash-logback-encoder / jackson-core 在 SDK 内为 `provided` | 接入服务运行时必须自带,否则日志不可用 | 接入文档中明确要求,或改为传递依赖 | +| R14 | **同一日志流内并存两套格式**:obs-sdk 契约行与 `robot-framework-lib` 的 logrus JSON(后者无 `service` / `env` / `community`,且 `level: fatal`、`time` 秒精度均与契约冲突)(§4.6.2) | LTS 侧的检索 / 聚合 / 告警规则要同时兼容两种格式;且**不是过渡期现象**——框架那 121 处打点会长期保留,两套格式的共存是稳态 | 明确 LTS 侧怎么处理这两套(分别配解析?云端做字段归一化?只对 obs-sdk 行做索引、logrus 行仅全文检索?)。**这一点未定之前,「结构化解析配得上」仍不成立** | +| R20 | **中心集群的 HA 未验证**:`replicas: 2` + pod 反亲和下的去重与告警行为(§4.7.2) | 中心是单点,挂了全网指标断线 | 试点期验证。若将来上 VictoriaMetrics,必须同时配 `-dedup.minScrapeInterval`,否则两个副本各写一份、**数据翻倍** | + +--- + +## 附录 + +### A. 相关链接 + +| 项 | 链接 | +| --- | --- | +| SDK 仓 | https://github.com/opensourceways/obs-sdk | +| 契约层 | [spec/](../spec/README.md) | +| Go 用法 | [go/README.md](../go/README.md) | +| Python 用法 | [python/README.md](../python/README.md) | +| Java 用法 | [java/README.md](../java/README.md) | +| Node 用法 | [node/README.md](../node/README.md) | +| 服务与社区映射 | https://github.com/opensourceways/infrastructure/blob/main/service.yaml | +| 试点日志 issue | [#2161](https://github.com/opensourceways/backlog/issues/2161) | +| 试点指标 issue | [#2162](https://github.com/opensourceways/backlog/issues/2162) | + +### B. 术语 + +| 术语 | 含义 | +| --- | --- | +| **community** | 社区标识(openEuler / Ascend / MindSpore 等 18 个),本次建设的核心切片维度 | +| **部署级 vs 请求级** | 字段/label 的两种取值来源:前者是 Init 一次注入的进程常量,后者是每请求从 context 覆盖 | +| **契约层** | `spec/` 目录,四语言输出格式的唯一权威定义 | +| **薄封装** | SDK 不自研 instrumentation,只做官方库的装配与字段注入 | +| **高基数** | 取值近乎唯一(request_id、trace_id、PR number 等),不能作 metrics label | +| **log-agent** | CCE 云原生日志采集插件的旧名,基于 fluent-bit + OTel,以 DaemonSet 部署在每个节点 | +| **default-stdout** | 该插件下的集群级日志采集策略名,内容为「全 ns 全容器 stdout」,业务日志进 LTS 的通道 | +| **聚合账号** | 汇聚方账号——是**跨账号**(基于 Organizations)而非「跨区域」。原设计通过 LTS 多账号日志汇聚中心 / AOM 多账号聚合实例承接各 Region 上报的日志与指标。**【2026-09-14】该路线已弃用**:指标侧改为**自建中心 Prometheus**(§4.7,不涉及华为云多账号机制);日志侧跨账号由**自建 Grafana 的 AK/SK 多数据源**解决(§4.6.4)。**本术语保留备查** | +| **中心指标集群** | 【2026-09-14 新增】独立部署的 Prometheus 中心集群,17 个集群的 Agent 经 `remote_write` 汇入,承载**统一指标大盘与统一告警**。形态参考资源块 `ascend-ci-deployment/monitoring`,**但独立部署、不复用其实例**(§4.7) | +| **Prometheus Agent** | `--agent` 模式,本地只抓取不存储,通过 `remote_write` 把数据推给中心——**汇聚发生在存储时,不是查询时** | +| **remote_write** | Prometheus 的远程写协议,本方案跨 Region 汇聚指标的**唯一机制**。⚠️ 资源块当前走**公网明文 HTTP**,生产不得照搬(§4.7.1) | +| **SMN** | 消息通知服务,告警的通知出口。**SMN 是 Region 级资源**;**【2026-09-14】现在仅日志告警使用**——指标告警已统一在中心 Alertmanager,不经 SMN |