# SCS-020　監控與觀測機制（Observability）程式設計規格（跨域）

| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-020 |
| 版本 | 1.11 |
| 狀態 | **已核定（規格先行·跨域）** |
| 對應需求 | `BRD-003`：監控機制（21 項）、System Monitoring（11 項）、Monitoring（8 項）——均對應 `SAD-006 §5`、`CSP-004` |
| 技術棧 | OpenTelemetry → Jaeger（本地）／Prometheus + Grafana + Loki（參 `CSP-006`） |
| 關聯文件 | `BRD-003`、`SAD-006 §5`、`CSP-004`、`CSP-001`（例外／追蹤 `common-spring`） |

### 修訂記錄

| 版本 | 日期 | 異動 |
|---|---|---|
| **1.11** | 2026-10-01 | **新增 §9.12 分流比例管理端點（`api-gateway`）**——解鎖 `SDD-007 §5.1.2.3` 桶 C 之「**分流比例管理**」（`/system/split-ratio`，原記「**僅 actuator**」）。**選頁理由**：`SAD-004 §2.4` 之分流機制（`CycleSplitDecision`／`CycleSplitGlobalFilter`／`CycleSplitProperties`，POC-13 已驗收）**完整存在**，缺的**只是管理面**——**不需新領域模型、不會造空殼**，且「**灰度放量與回退**」為遷移期最高頻維運操作。**規格先行**：立本節 → 實作 → `SDD-007 §5.4.30` 逐鈕定義 → 頁面。**設計要點**：**定點行替換**（僅改 `enabled:`／`percentage:` 兩行，**保留註解**）＋ `ContextRefresher.refresh()`（**不重啟**）＋ **回讀驗證**（`effectiveMatchesRequest`，**不以請求值冒充生效值**）＋ **失敗不靜默**（`CC-PLT-5000`）。⚠️ **未竟**：角色級授權／稽核落庫／回退歷程／多實例組態同步（見 `§9.12.5`） |
| **1.10** | 2026-10-01 | **BEAM PMO 2026-10-01 裁定之兩項執行（`GOV-007`）**：① **§9.5 #3 更正**——`CorrelationIdFilter` 之「待裁定／未擅自註冊」係**過期記載**；實已由 `CorrelationIdAutoConfiguration`（登錄 `AutoConfiguration.imports`）以 `FilterRegistrationBean` 註冊（BEAM PMO 2026-09-21 18:57 裁定「保留並註冊」），並顯式指定 `ORDER`（`LOWEST_PRECEDENCE - 100`）確保**晚於** OTel 觀測範圍。② **§9.5 #5 實作**——19 個 `application.yml` 均設 `logging.logback.rollingpolicy.total-size-cap: 5GB`（BEAM PMO 裁定「限制 5GB」）；**併更正原建議之屬性名**（`logging.file.total-size-cap` **不存在**；正解為 `logging.logback.rollingpolicy.total-size-cap`，實查 metadata 確認）。⚠️ **per-service 語意**（本地 19 模組共用目錄）已於 §9.5 #5 誠實揭露 |
| **1.9** | 2026-10-01 | **新增 §9.11.6 業務 KPI 區塊（`BffDashboard.business`，`CR-026`）**：`/dashboard` 由「系統營運面」擴充為「系統營運面 ＋ 業務 KPI」。`business` 含 4 子塊（cards／applications／authorizations／disputes），各 `oneOf` schema-or-null，**獨立降級**（任一統計取不到 → 該子塊為 `null`，不以 0 充數）；由 `BusinessStatsService` 並行 4 路 `CompletableFuture` ＋ `DelegatingSecurityContextExecutor` 聚合，逾時 `STATS_TIMEOUT_MS=5000` |
| **1.8** | 2026-10-01 | **新增 §9.11 營運總覽聚合端點（`bff-web`）**——解鎖 `SDD-007 §5.1.2.3` 桶 C 之「**營運總覽**」（`/dashboard`）。**規格先行**：立本節 → 改 `tools/gen_openapi.py` → 重生契約 → 實作 → `SDD-007 §5.4.28` 逐鈕定義 → 頁面。★ **回訪查證（關鍵）**：掃全數 **17 服務**契約，**皆無「統計／摘要」端點**（僅 CRUD 與列表）→ **業務 KPI 無法由既有端點推得** → 故**明訂範圍界定**（`§9.11.1`）：**本版為系統營運面**（服務健康／批次／端點連線摘要，`bff-web` 自足），**業務 KPI 屬第二階段**（需各服務統計端點，另立 CR）。**設計要點**：**複用**既有三個聚合服務（**不重複探測**）、**並行**執行、**區塊降級**（失敗區塊為 `null`，**不以 0 充數**）、**不快取**、**fail-closed** |
| **1.7** | 2026-09-30 | **新增 §9.8.6「實作註記：分層與目標埠」（`HANDOVER #A-105`）**。⚠️ `HealthAggregationService`（`application`）原直接注入 `infrastructure.config.BffHealthProperties`，**違反 `CSP-002 §6.3`**（`LayeringTest` **於 `HEAD` 即紅**）→ 採「**應用層立埠**」：`application/HealthTargetsPort` ＋ `infrastructure/config/HealthTargetsConfigImpl`（沿用 `AccountOverviewPort` 慣例）；**併補 3 個單測**（JDK 內建 `HttpServer` 造真目標，**測真實網路行為而非 stub**）→ `:platform:bff-web:test` **12 測 0 失敗、全綠**（原 9 測 1 失敗）。⚠️ **併載**：空 `targets` 之語意**不得放寬**（回空 Map＝「未組態」→ 必須 `DOWN`），埠之實作**原樣轉發**即為確保不被靜默補值 |
| **1.6** | 2026-09-30 | **§9.9.1 更正 ＋ 四服務列表端點規格先行（`#A-104`）**。⚠️ **原載「四個既有端點，皆已實作並真機驗證」經實測不成立**——四個路徑**皆只有 `POST`**，`GET` 回 **405／404／404／404**。故**先行新增四個 `GET` 列表端點**：`GET /statement-runs`／`GET /clearing/runs`／`GET /migration-jobs`／`GET /report-jobs`（各 `?limit=N`，預設 20／上限 100），並於 `SCS-011 §6.1`／`SCS-013 §6.1`／`SCS-015 §6.1`／`SCS-017 §6.1` 各立規格。**時間錨點**：`clearing` **無 `startedAt` 欄位** → 本聚合取值順序為 **`startedAt` → `createdAt`**，**不臆造**。**§9.9.3 補 `limit` 參數定義**（每服務上限，預設 10／上限 50）。**四服務 DTO 刻意不統一**——以別名鏈吸收差異，避免各服務背負非其職責之欄位 |
| **1.5** | 2026-09-30 | **新增 §9.9 批次作業監控 ＋ §9.10 端點與連線管理（`bff-web`）**——解鎖桶 C 兩頁（`SDD-007 §5.4.26`／`§5.4.27`）。⚠️ **併補記**：§9.9／§9.10 之規格與契約已於同日先行完成（契約 **81 端點、0 錯誤**），惟**當時未登修訂記錄**，本版一併補登。**§9.10.2 之原設計經實測修正**：`gateway` actuator endpoint **於本技術棧版本（Spring Cloud Gateway 5.0.3）未被註冊**（`/actuator` 索引僅 7 個，加入 `exposure.include` 亦無效）→ 改採 **`api-gateway` 自供唯讀端點 `GET /api/v1/gateway/routes`**（`#A-103`）。**實作要點**：讀 `RouteDefinitionLocator`（**非** `RouteLocator`——後者已解析為物件、**無法還原原文**）；原文還原依 `getArgs()` 之 **`LinkedHashMap`** 插入順序（`javap` 確認）；**維持宣告順序**不重排；追蹤碼於 controller 方法進入時取（避免 Reactor 脈絡傳遞之依賴）。**真機**：`bff` 之 **`routesAvailable=true`**（修正前 `false`）、**17 筆**路由、`service` 由 `routeId` 正確推導；連線維度 UP **7/17** 不受影響。**§9.10.5 補** `filters` 未由 `bff` 呈現（契約未含，**不逕行加欄位**）與**不自訂錯誤碼**（`CC-GW-*` 無對應語意） |
| 1.4 | 2026-09-29 | **新增 §9.8 健康聚合端點（`bff-web`）**——解鎖 `SDD-007 §5.1.2.3` 桶 C 之「健康檢查與監控」頁。**規格先行**：立本節 → 改 `tools/gen_openapi.py` → 重生契約（**78 端點、0 錯誤**）→ 實作 → `SDD-007 §5.4.24` 逐鈕定義 → 頁面。**★ 併載兩個實測揭露之陷阱**：①`@Value("#{...}")` 讀 Map **靜默失敗**（property key 含 `-` 非合法 SpEL）→ 須用 `@ConfigurationProperties`；②**並行探測必須傳遞 `SecurityContext`**（token-relay 模式下 `RelayingOutboundTokenProvider` 僅適用同執行緒）→ 須用 `DelegatingSecurityContextExecutor`，否則**全部服務探測失敗** |
| 1.0 | 2026-09-19 | 初版核定（規格先行） |
| 1.2 | 2026-09-20 | WorkBuddy AI | `HANDOVER #C-23`：新增 **§9.6 推廣至全模組**——18 個模組之手動 OTel 組合改為 `spring-boot-starter-opentelemetry` 並補 tracing／結構化日誌組態；產生器模板 3 處同步；`prometheus.yml` 補 `cardcore-platform`（gateway／bff 原為監控缺口）。三支柱實測 **19/19**；§9.5 #4 解除 |
| 1.3 | 2026-09-20 | WorkBuddy AI | `HANDOVER #C-35`：新增 **§9.7 IAM（Keycloak）納入監控**——健康端點實為正常，僅因位於管理埠 9000 且未映射而由主機探測皆 404；修正為映射 9000 ＋ `KC_METRICS_ENABLED=true` ＋ Prometheus `keycloak` job（原**完全無指標**）。實測四端點皆 200、930 序列、權杖簽發回歸正常 |
| 1.4 | 2026-09-21 | WorkBuddy AI | **`HANDOVER #C-28` 收尾（BEAM PMO 裁定「改規格」，實則已於 `CSP-004` 1.1 施作）**：①**§9.5 #2 解除**——ECS 欄位落差已由 `CSP-004` 1.1 依實測改規格處理，本次**複核實測**真實 `posting-service.json`（137 筆）：`@timestamp`／`log.level`／`service.name`／`message`／`traceId`／`spanId` 與 `CSP-004 §3.1` **完全一致**，無 `event`（符合 §3.2）；`traceId` 僅 4 筆係取樣率 0.1，非缺漏。②**內文過時描述修正**——§2／§4／§7 原仍記載「`CorrelationIdFilter` 注入 `correlation_id` 貫穿日誌」，與 `CSP-004 §6`（已廢除第二套 correlation ID、統一 OTel traceId）**互相矛盾**，已改為現行機制並註明：該過濾器**從未被註冊**（去留待裁定），且**勿與 `DomainEvent.correlationId()`（領域事件信封欄位，仍有效）混淆**。⚠️ 連帶發現（未擅動）：`CSP-002 §97`／`CSP-006 §245`／`CSP-007 §411` 仍將「correlation-id 過濾器」列為 `common-spring` 職責，與該過濾器未註冊之事實不符，**待 `CorrelationIdFilter` 去留裁定後一併處理** |
| **1.1** | 2026-09-19 | **新增 §9 實作機制與實測證據**（W-4 接線）：三支柱實測結果、四項實作時發現之缺陷與修正、五項已知限制 |

---

## 1. 對應需求（NFR）

| NFR 類別 | 數量 | 來源 | 驗收 |
|---|---|---|---|
| 監控機制 | 21 | `SAD-006 §5`、`CSP-004` | 監控指標與告警規則驗證、儀表板檢視 |
| System Monitoring | 11 | `SAD-006 §5` | 監控覆蓋率檢核 |
| Monitoring | 8 | `SAD-006 §5` | 監控項目逐項驗證 |

---

## 2. 觀測架構

```
應用（Spring Boot）──OTel SDK──→ Jaeger（Trace，本地）
                            └──→ Prometheus（Metrics）→ Grafana（Dashboard）
                            └──→ Loki（Logs，結構化）
```

- **Trace**：統一以 **OTel W3C Trace Context** 為跨系統唯一識別（`CSP-004 §6`，BEAM PMO 2026-09-20 裁定），
  由 `common-spring` 之 `TraceContext` 讀取（取值順序：OTel span → MDC → 臨時值）。
  <br>⚠️ **已廢除第二套 `correlation_id`**——原 v1.0 設計另設 `correlation_id` 貫穿日誌，
  實測導致 API 回應與日誌之 ID **互不相通**（`10b8a469…` ≠ `894ff808…`），查測時無法對照。
  <br>⚠️ **`CorrelationIdFilter` 目前未註冊**（`AutoConfiguration.imports` 未登錄、類別亦無 `@Component`），
  其職責已被 OTel 取代；**註冊或退役待 BEAM PMO 裁定**（詳 §9.5 #3），故本節不將其列為現行機制。
  <br>ℹ️ **勿與 `DomainEvent.correlationId()` 混淆**——那是**領域事件信封**欄位
  （`CSP-007`、`SAD-001 AP-05`、`SDD-006 §4.1`），用於事件鏈之業務關聯，**仍然存在且有效**；
  被廢除的是「日誌／追蹤」層的那一個。
- **Metrics**：RED（Request/Error/Duration）＋ USE（Utilization/Saturation/Errors），經 Micrometer 暴露。

---

## 3. 指標（Metrics）

| 指標 | 說明 | 來源 |
|---|---|---|
| 授權成功率／失敗率 | 業務指標 | `authorization-service` |
| 端點 P99 延遲 | 效能指標 | 所有 REST 端點（Micrometer） |
| JVM／連線池飽和度 | USE | `common-spring` |
| Kafka 消費滯後 | 管線健康 | `common-event` |
| 批次執行狀態 | 作業指標 | `common-batch` |

---

## 4. 追蹤（Tracing）

- 所有 inbound/outbound 自動埋點（`common-spring` `TraceContext`）；資料庫／Kafka 跨度自動化。
- **OTel `traceId` 貫穿日誌**（ECS 輸出為頂層駝峰 `traceId`／`spanId`，見 `CSP-004 §3.1`），
  使 API 回應之 `traceId` 與日誌之 `traceId` **為同一值**，便於跨服務排查。
  <br>⚠️ 原設計之 `correlation_id` **已廢除**（`CSP-004 §6`），理由見 §2。

---

## 5. 日誌與稽核留痕

- 結構化 JSON 日誌 → Loki。
- **稽核留痕**：關鍵變更（管制碼設定、額度調整、爭議）除業務表（`block_code_history` 等）外，亦落日誌，符合銀行審計要求（`common-security` 加密／遮蔽）。

---

## 6. 告警（Alerting）

- Prometheus Alertmanager → Grafana 告警；依 `BRD-003` 監控機制 21 項定義規則（延遲門檻、錯誤率、飽和度）。
- 分級（P1/P2/P3）與值班（SRE）路徑。

---

## 7. 測試案例（對應 `TRP-001`）

- 監控覆蓋率檢核（各服務端點／批次指標齊全）
- 追蹤貫穿測試（gateway→bff→服務→Kafka→下游 **`traceId` 連續**；`correlation_id` 已廢除，見 §2）
- 告警規則觸發測試（注入延遲／錯誤驗證告警）

---

## 8. 已知限制（誠實揭露）

- 具體儀表板版面與告警閾值（與 `BRD-003` 21 項對應）待 SRE 與 `SAD-006 §5` 凍結。
- 本地 Jaeger 與生產 Prometheus/Grafana/Loki 之佈署差異（SAD-006 部署章節）待確認。
- 稽核日誌之保留期與合規（如金管會要求）待法遵確認。

---

## 9. 實作機制與實測證據（W-4 接線，2026-09-19）

> **目的**：將 §2 之觀測架構由「文件」落為「可驗證之實物」。
> 本節所有數據皆為**實測**，非設計推估。

### 9.1 三支柱實測結果

| 支柱 | 路徑 | 實測證據 | 判定 |
|---|---|---|---|
| **Metrics** | Prometheus 直接 scrape `host.docker.internal:<8081–8098>/actuator/prometheus` | 4 個服務（8084／8088／8089／8091）target **UP**；`jvm_memory_used_bytes` 回傳 **24 個序列**，含 heap／nonheap 實際值 | ✅ 通過 |
| **Traces** | Spring Boot → OTLP/HTTP → OTel Collector → Jaeger | Jaeger `/api/services` 列出 `party-service`；查得 5 筆 trace，span 含 `http get /api/v1/parties/{partyId}`（8,027µs） | ✅ 通過 |
| **Logs** | Spring Boot 結構化 ECS JSON 寫檔 → Collector `filelog` → OTLP → Loki | Loki 查得實際日誌，含 `message`／`traceId`／`spanId`／`log_level`／`deployment_environment=local` | ✅ 通過 |

> **Metrics 採「直接 scrape」而非「OTLP push」之理由**：
> `micrometer-registry-prometheus` 已於各服務就緒、`/actuator/prometheus` 已暴露，
> 直接 scrape **零新增依賴**且已驗證可用。OTLP push 需另加 `micrometer-registry-otlp`；
> Collector 之 metrics pipeline 已保留，日後可切換。

### 9.2 實作時發現之缺陷（原組態從未成功啟動）

> ⚠️ **重要更正**：先前認知為「容器已在，只差接線」。實測發現
> **`cc-otelcol` 自建立以來從未成功啟動**（無限重啟），組態有**三處致命缺陷**。

| # | 缺陷 | 實測症狀 | 修正 |
|---:|---|---|---|
| 1 | `loki` exporter 已自 contrib **移除** | `unknown type: "loki" for id: "loki"`，Collector 啟動即失敗 | 改走 Loki 3.x **原生 OTLP 攝取端點** `/otlp/v1/logs`（實測回 422 表示端點存在；`/otlp/v1/metrics` 回 404＝不存在） |
| 2 | `replace_pattern` 用 **Go RE2**，不支援 lookahead `(?=` | `invalid or unsupported Perl syntax: (?=`，啟動失敗 | 改為 `replace_pattern(..., "^.+$", "****")` **完整遮蔽**。亦較符合 `CSP-004 §6`「**禁止**在 span 記錄敏感 payload」 |
| 3 | `prometheus.yml` 之 target 寫容器 DNS 名（`party-service:8080`） | 17 個 target **全數 DOWN**（服務跑在主機，非 compose 內） | 改為 `host.docker.internal:<8081–8098>`（實測解析為 `192.168.65.254`） |

**附帶修正**：`otlp`→`otlp_grpc`、`otlphttp`→`otlp_http` 之 deprecated 別名；
Collector 健康埠 13133 原未映射至 compose，本機無法探測（易誤判為「沒起來」）。

### 9.3 Spring Boot 4 之 OTLP 陷阱（與 Flyway 同源）

**命名空間不一致**——誤用不報錯，只會**靜默不匯出**：

| 信號 | 正確屬性 | 所需依賴 |
|---|---|---|
| Traces | `management.opentelemetry.tracing.export.otlp.endpoint` | `spring-boot-starter-opentelemetry` |
| Metrics | `management.otlp.metrics.export.url` | `micrometer-registry-otlp` |
| Logs | `management.opentelemetry.logging.export.otlp.endpoint` | 尚需第三方 log bridge |

原寫於各服務之手動組合（`micrometer-tracing-bridge-otel` ＋ `opentelemetry-exporter-otlp`）
**不會觸發** `management.opentelemetry.tracing.export.otlp.*` 之自動配置。已改用 starter，
並確認其傳遞引入上述兩者（故原手動宣告為冗餘，已移除）。

### 9.4 日誌為何不走「應用直接 OTLP 推送」

Spring Boot 4.1 **不提供**將 Logback 日誌橋接至 `SdkLoggerProvider` 的內建機制
（官方文件：「Spring Boot doesn't support bridging logs to this bean out of the box」）。
唯一官方推薦之 `opentelemetry-logback-appender-1.0`，**所有版本皆為 `-alpha`**
（實測 Maven Central，最新 `2.31.1-alpha`）——銀行案不應引入 alpha 相依。

故採：Spring Boot **內建**結構化日誌（`logging.structured.format.file=ecs`，無須第三方 encoder）
寫檔 → Collector `filelog` receiver 尾隨讀取 → OTLP → Loki。**零第三方相依**。

### 9.6 推廣至全模組（`HANDOVER #C-23`，2026-09-20）

> **背景**：§9.5 #4 記載「僅 `party-service` 接線（觀測 1/17）」。本節為該項之解除。

**根因（實測，非推測）**：其餘 18 個模組（16 個服務 ＋ `api-gateway` ＋ `bff-web`）
之 `build.gradle.kts` 採**手動組合**——

```
implementation("io.micrometer:micrometer-tracing-bridge-otel")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
```

此組合**不會觸發** Spring Boot 4 之 `management.opentelemetry.tracing.export.otlp.*` 自動配置
（見 §9.3），故依賴看似齊備、**實則從未匯出任何 trace**，且**不拋出任何錯誤**。
與 §9.2 之 Flyway／loki exporter 同屬「**靜默不做**」型缺陷。

**修正範圍**

| # | 修正 | 數量 |
|---:|---|---:|
| ① | 依賴改為 `org.springframework.boot:spring-boot-starter-opentelemetry`（移除原手動組合） | **18** 個模組（`party-service` 原已正確） |
| ② | 組態補 `management.opentelemetry.tracing.export.otlp.endpoint` ＋ `logging.structured.format.file=ecs` ＋ `logging.file.name` | **18** 個模組 |
| ③ | `tools/gen_service_skeleton.py` 模板同步修正（service／gateway／bff **3 處**） | 避免新模組重蹈覆轍 |
| ④ | `prometheus.yml` 新增 `cardcore-platform` job（`api-gateway:8100`／`bff-web:8101`） | 原僅 scrape 17 個領域服務，**平台層為監控缺口** |

> ① 與 ② 皆同步修改「既有檔案」與「產生器模板」：產生器**只寫入不存在的檔案**，
> 故僅改模板無法修復既有 18 個模組，兩邊皆須動手。

**逐模組實測（逐一啟動，非抽樣）**

| 模組 | 埠 | `/actuator/health` | `/actuator/prometheus` | Jaeger trace | 結構化日誌 JSON | Loki 行數 |
|---|---:|---:|---:|---:|---|---:|
| `authorization-service` | 8081 | 200 | 200 | ≥5 | ✅ | 48 |
| `card-service` | 8082 | 200 | 200 | ≥5 | ✅ | 49 |
| `limit-service` | 8083 | 200 | 200 | ≥5 | ✅ | 48 |
| `party-service` | 8084 | 200 | 200 | ≥5 | ✅ | 112 |
| `product-service` | 8085 | 200 | 200 | ≥5 | ✅ | 48 |
| `pricing-service` | 8086 | 200 | 200 | ≥5 | ✅ | 48 |
| `posting-service` | 8087 | 200 | 200 | ≥5 | ✅ | 48 |
| `payment-service` | 8088 | 200 | 200 | ≥5 | ✅ | 49 |
| `installment-service` | 8089 | 200 | 200 | ≥5 | ✅ | 48 |
| `rewards-service` | 8091 | 200 | 200 | ≥5 | ✅ | 48 |
| `statement-service` | 8092 | 200 | 200 | ≥5 | ✅ | 48 |
| `dispute-service` | 8093 | 200 | 200 | ≥5 | ✅ | 48 |
| `clearing-service` | 8094 | 200 | 200 | ≥5 | ✅ | 48 |
| `merchant-service` | 8095 | 200 | 200 | ≥5 | ✅ | 49 |
| `migration-service` | 8096 | 200 | 200 | ≥5 | ✅ | 48 |
| `notification-service` | 8097 | 200 | 200 | ≥5 | ✅ | 48 |
| `report-service` | 8098 | 200 | 200 | ≥5 | ✅ | 48 |
| `api-gateway` | 8100 | 200 | 200 | ≥5 | ✅ | 26 |
| `bff-web` | 8101 | 200 | 200 | ≥5 | ✅ | 17 |

> 上表「Jaeger trace」為該服務當次探測回傳之 trace 筆數（查詢上限 5，故 `≥5` 即代表有匯出）。

**三支柱之最終驗證**

| 支柱 | 驗證方式 | 結果 |
|---|---|---|
| **Traces** | Jaeger `/api/services` | **19 個模組全數在列**（＋`jaeger-all-in-one` ＝ 20） |
| **Logs** | Loki 依 `service_name_extracted` 查詢 | **19/19 皆有日誌**（48–112 行；gateway 26／bff 17 行） |
| **Metrics** | Prometheus `/api/v1/targets` | **21 個 target**：領域服務 17 個、平台層 2 個、Collector、Prometheus 自身。<br>領域服務 scrape **UP 17/17**（分兩批實測：11 ＋ 6），平台層 **UP 2/2** |

**實測時之三個踩坑（皆為「不報錯」型）**

1. **取樣率**：預設 `TRACING_SAMPLING=0.1`（10%），單次請求有 90% 機率**不被取樣**，
   驗證會隨機失敗。實測時須設為 `1.0`。
2. **Loki 查詢語法**：`service_name_extracted` 係 Loki 3.x 自動萃取之
   **structured metadata**，**不能**置於 stream selector `{{}}` 內——查得到 0 筆且**不報錯**；
   須寫為 `{{service_name="unknown_service"}} | service_name_extracted="<svc>"`。
   另 `/loki/api/v1/query` 為 instant query，**不支援 log query**（須用 `/query_range`）。
3. **併發啟動限制**：19 個 JVM 同時啟動時，6 個服務因 HikariPool
   `Connection is not available, request timed out after 3003ms` 於 Flyway 初始化階段失敗。
   **屬本機資源限制（4 核／Oracle sessions 上限 322），與本次監控接線無關**——
   該 6 個服務於逐一啟動與後續分批啟動時皆正常，且皆已取得 trace／日誌／scrape 證據。

### 9.7 IAM（Keycloak）納入監控（`HANDOVER #C-35`，2026-09-20）

> **背景**：§9.6 完成 19 個應用模組之接線後，身分認證服務（Keycloak）**仍完全無指標**，
> 且其健康端點由主機探測一律 404。

**查明結果（實測 + 官方文件）**：健康端點**本來就正常**——
Keycloak 26 之 `/health*`、`/metrics` 位於**管理埠 9000**（官方文件：「exposed on the
management port 9000 by default」），**不是**主埠 8180。原 compose 未映射 9000，
故由主機探測皆 404，易被誤判為「端點壞掉」。
容器內實測 `9000/health/ready` → **200 `{"status":"UP"}`**。

| # | 修正 | 實測結果 |
|---:|---|---|
| ① | `docker-compose.yml` 映射 `9000:9000` | `/health`／`/health/ready`／`/health/live`／`/health/started` **皆 200**；內容含 `Keycloak database connections async health check: UP` |
| ② | 補 `KC_METRICS_ENABLED=true` | `/metrics` **200**；約 **930** 個序列（vendor 673／jvm 61／cache 48／http 40…）。<br>註：Database 健康檢查項目**需同時啟用指標**才會出現（官方文件） |
| ③ | `prometheus.yml` 新增 `keycloak` job（target `host.docker.internal:9000`，`metrics_path=/metrics`） | `up{job="keycloak"}=1`、序列 **930**；target 總數 21 → **22** |

**回歸驗證**：容器重建後，權杖簽發仍正常
（`POST /realms/cardcore/protocol/openid-connect/token` → `demo.holder`、角色 `cardcore-customer`）。
另註：`cardcore-services` 為**機密客戶端**，password grant 須帶 `client_secret`
（僅帶 `client_id` 會回 `unauthorized_client`）。

> ⚠️ **資安提醒（未解決，須於上線前處理）**：管理埠**不應對外暴露**。
> 本機開發可接受；SIT 以上應僅允許監控網段存取，或改以容器內 `HEALTHCHECK`
> 於容器內探測（`{ printf 'HEAD /health/ready HTTP/1.0\r\n\r\n' >&0; grep 'HTTP/1.0 200'; } 0<>/dev/tcp/localhost/9000`），
> 即**無須映射埠**。

### 9.8 健康聚合端點（`bff-web`）與作業台健康頁（2026-09-29）

> **緣由**：`SDD-007 §5.1.2.3` 之桶 C「**健康檢查與監控**」（`/system/health`）原記
> 「**須先於 bff-web 新增健康聚合端點**方可實作」——**本節即為該端點之規格**。
> 依「規格先行」：先立本節 → 改 `tools/gen_openapi.py` → 重生契約 → 實作 → `SDD-007 §5.4.24` 逐鈕定義 → 頁面。

#### 9.8.1 為何前端不能直接呼叫各服務 `/actuator/health`

| 障礙 | 說明 |
|---|---|
| **CORS** | 各服務未對瀏覽器來源開放 |
| **gateway 未代理 actuator** | 前端無法經閘道取得 |
| **bff-web 原僅 1 支聚合端點** | 無 `/health` 聚合 |

→ 故須由 `bff-web` **代為探測並聚合**。

#### 9.8.2 端點規格

**`GET /api/v1/bff/health`**（回應 `BffHealth`，`200`）

| 欄位 | 型別 | 說明 |
|---|---|---|
| `overall` | `UP`／`DEGRADED`／`DOWN` | **UP**＝全部 UP；**DEGRADED**＝部分 DOWN；**DOWN**＝全部 DOWN |
| `checkedAt` | `date-time` | 本次檢查時刻 |
| `services[]` | array | 各服務：`service`／`status`（`UP`／`DOWN`）／`latencyMs`／`error` |

#### 9.8.3 行為要求

1. **並行探測**各服務之 `/actuator/health`；**單一逾時不阻塞整體**（逾時 **2 秒**）。
2. **降級呈現**：失敗者標 `DOWN` 並帶 `error`，**不使整體失敗**——
   與 `BffSection` 之「單一下游失敗不拖垮整體」**同一政策**（`SAD-004 §2.2`）。
3. **不快取**——健康狀態須即時；快取會使畫面顯示過期狀態（比不顯示更危險）。
4. **失敗即如實呈現**，**不得**以樂觀預設值掩蓋（`CSP-001 §8 B2`）。

#### 9.8.4 ⚠️ 定位說明（回應 `SDD-007 §5.1.2.3` 之「**本頁定位須先釐清**」）

本端點與其頁面之定位為 **作業台**——維運人員之**即時總覽**（哪些服務 UP／DOWN）。

**不取代** Prometheus／Grafana 之**長期指標與告警**——後者仍為 **NFR Monitoring 之正式方案**。
→ 頁面須**明載此定位**，避免使用者誤解為「監控看板」（瀏覽器畫面無法呈現歷史趨勢與告警規則）。

#### 9.8.5 未竟

- 端點涵蓋之服務清單以 `cardcore.services.*` 之組態為準；**未組態者不列入**（避免硬編 17 個服務）。
- 本端點**不聚合** JVM／連線池等細部指標——那屬 `/actuator/prometheus`，仍由 Prometheus 蒐集。

#### 9.8.6 實作註記：分層與目標埠（2026-09-30 修正，`HANDOVER #A-105`）

> **緣由**：`HealthAggregationService`（`application`）原直接注入
> `infrastructure.config.BffHealthProperties`——違反 `CSP-002 §6.3` 之依賴方向
> （`interfaces → application → domain ← infrastructure`），
> `LayeringTest.applicationMustNotDependOnInfrastructure` **於 `HEAD` 即紅**。

**修正採「應用層立埠」**（**非**把屬性類別搬進 `application`）：

| 角色 | 類別 | 說明 |
|---|---|---|
| 埠（`application`） | `HealthTargetsPort` | `targets()`／`timeoutMs()`；**沿用 `AccountOverviewPort` 既有慣例** |
| 實作（`infrastructure`） | `HealthTargetsConfigImpl` | 適配 `@ConfigurationProperties`；**原樣轉發、不補預設值** |

**為何不搬類別**：屬性類別本質是**基礎設施之組態繫結**，搬進 `application` 只是把違規**搬家**，
且讓應用層背負 Spring 繫結細節。立埠後應用層只依賴抽象，**得以手寫 fake 測試**（無須啟動 Spring）。

> ⚠️ **空 `targets` 之語意不得放寬**：回**空 Map 代表「未組態」**，
> 呼叫端必須回報 `DOWN`（`§9.8.3 #4`），**不得**以樂觀預設值掩蓋。
> 埠之實作**原樣轉發**，即為確保此語意不被靜默補值。

**測試**（`HealthAggregationServiceTest`，3 例）：以 JDK 內建 `HttpServer` 造「**活的目標**」、
以連不上的埠造「**死的目標**」——測**真實網路行為**（逾時、狀態碼判定、逾時／連線失敗之分流），
**而非 stub 假造 HTTP 結果**（後者測的是 stub，不是探測邏輯）。三例：空 targets→`DOWN`／
一活一死→`DEGRADED`（UP 帶 `latencyMs`、DOWN **如實帶原因**）／全死→`DOWN`。

---

### 9.9 批次作業監控聚合端點（`bff-web`）與作業台批次頁（2026-09-30）

> **緣由**：`SDD-007 §5.1.2.3` 之桶 C「**批次作業監控**」（`/system/batch`）——
> 本節為其後端端點之規格。**與 §9.8 之健康聚合不同**：本節聚合的是**各服務的批次執行紀錄**。
>
> ⚠️ **前置變更（2026-09-30 補）**：聚合之前提是**四服務各有「列出批次」端點**——
> 實測**四者皆無**（僅有觸發與單筆查詢）→ 故**先行新增四個 `GET` 列表端點**（見 §9.9.1），
> 規格分載於 `SCS-011 §6.1`／`SCS-013 §6.1`／`SCS-015 §6.1`／`SCS-017 §6.1`。

#### 9.9.1 資料來源（⚠️ 原載「四個既有端點」**與事實不符**——已更正）

> ⚠️ **實測揭露（2026-09-30，`#A-104`）**：本節原載「**四個既有端點，皆已實作並真機驗證**」——
> **經實測不成立**：四個路徑**皆只有 `POST`（觸發）**，`GET` 實測分別回
> **405／404／404／404**。故**本規格先行新增四個 `GET` 列表端點**，`bff-web` 改接之。

| 服務 | **列表端點（本規格新增）** | 規格出處 | 狀態值域 | 時間錨點 |
|---|---|---|---|---|
| `statement-service` | `GET /api/v1/statement-runs?limit=N` | `SCS-011 §6.1` | `GENERATING`／`GENERATED`／`FAILED` | `startedAt` |
| `clearing-service` | `GET /api/v1/clearing/runs?limit=N` | `SCS-013 §6.1` | `RUNNING`／`MATCHED`／`DIFFERENCE`／`FAILED` | **`createdAt`** |
| `migration-service` | `GET /api/v1/migration-jobs?limit=N` | `SCS-015 §6.1` | `RUNNING`／`COMPLETED`／`FAILED` | `startedAt` |
| `report-service` | `GET /api/v1/report-jobs?limit=N` | `SCS-017 §6.1` | `QUEUED`／`RUNNING`／`COMPLETED`／`FAILED` | `startedAt` |

> ⚠️ **`clearing` 無 `startedAt` 欄位**（`CLEARING_RUNS` 僅有 `CREATED_AT`／`UPDATED_AT`）——
> 故本聚合之時間錨點取值順序為 **`startedAt` → `createdAt`**；**皆取不到者留空，
> 不以當前時間或 0 填充**（`§9.9.4 #5` 不臆造原則）。
> ⚠️ **四服務之列表回應形狀各異**（`CycleRunView[]`／`ClearingRunSummary[]`／
> `MigrationJobSummary[]`／`ReportJobSummary[]`）——本服務以 `firstText`／`firstLong` 之
> **別名鏈**吸收差異（如 `jobId`｜`runId`），**不強求四服務統一 DTO**（那會使各服務背負
> 非其職責之欄位）。

#### 9.9.2 ★ 統一狀態分組（**本節核心**）

**四服務之狀態值域各不相同**——若畫面直接顯示原始狀態，使用者無法一眼看出「**哪些批次要注意**」。
故定義**統一分組**（**原始狀態一律如實保留**，分組僅為**呈現用**）：

| 分組 | 涵蓋之原始狀態 | 語意 |
|---|---|---|
| `PENDING` | `QUEUED` | 已排入、尚未開始 |
| `RUNNING` | `GENERATING`／`RUNNING` | 執行中 |
| `SUCCEEDED` | `GENERATED`／`MATCHED`／`COMPLETED` | 成功完成 |
| `WARNING` | **`DIFFERENCE`** | **完成但有差異**（對帳不平——**非失敗，但須人工確認**） |
| `FAILED` | `FAILED` | 失敗 |

> ⚠️ **`DIFFERENCE` 不可歸為 `SUCCEEDED`**：對帳有差異之批次**帳務上不等於成功**，
> 歸為成功會使差異被忽略（**假綠**）。
> ⚠️ **分組不得取代原始狀態**——回應中**兩者並存**，畫面以原始狀態為主、分組為輔（篩選／排序用）。

#### 9.9.3 端點規格

**`GET /api/v1/bff/batch-jobs?limit=N`**（回應 `BffBatchJobs`，`200`）

| 參數 | 型別 | 說明 |
|---|---|---|
| `limit` | integer（可選） | **每服務**回傳筆數上限（轉傳至四服務之 `limit`）。預設 **10**，上限 **50** |

| 欄位 | 說明 |
|---|---|
| `checkedAt` | 本次查詢時刻 |
| `jobs[]` | 統一清單：`jobType`／`service`／`jobId`／`status`（原始）／`statusGroup`／`processedCount`／`successCount`／`errorCount`／`startedAt`／`finishedAt`／`detail` |
| **`unavailable[]`** | **查詢失敗之服務**：`service`／`error`——**如實呈現，不使整體失敗** |

**`jobType`**：`STATEMENT_CYCLE`／`CLEARING_RUN`／`MIGRATION_JOB`／`REPORT_JOB`。

#### 9.9.4 行為要求

1. **並行**查詢四個服務；**單一逾時不阻塞整體**（逾時 3 秒——批次查詢較健康檢查重）。
2. **部分失敗不拖垮整體**：失敗之服務列入 `unavailable[]` 並帶原因，其餘照常呈現。
3. **合併後依 `startedAt` 遞減**（最近執行者在前）。
4. **不快取**——批次狀態須即時。
5. **不得臆造**：服務未回傳之欄位（如 `clearing` 無 `processedCount`）**留空**，
   **不以 0 或推測值填充**（`CSP-001 §8 B2`）。

#### 9.9.5 未竟

- 本端點**僅查詢**（監控）；**不提供**觸發或中止批次之功能——那屬各服務之職責與權限範疇。
- `clearing` 之 `DIFFERENCE` 目前**僅能呈現差異筆數／金額**，無法下鑽至明細（需 `clearing` 另立端點）。

---

### 9.10 端點與連線管理聚合端點（`bff-web`）與作業台端點頁（2026-09-30）

> **緣由**：`SDD-007 §5.1.2.3` 之桶 C「**端點與連線管理**」（`/system/endpoints`）——
> 本節為其後端端點之規格。`SDD-007 §5.1.1` 另載：原型之「網路連線管理（拓樸／主機連線／容錯切換）」
> **已由本頁涵蓋**（判定為重疊）。

#### 9.10.1 本頁之兩個維度

| 維度 | 資料來源 | 說明 |
|---|---|---|
| **端點（路由）** | `api-gateway` 之 `GET /api/v1/gateway/routes`（**自訂唯讀端點**；原 `actuator` 版不可用，見 §9.10.2） | 各服務之**入口路由**（`routeId`／`uri`／`predicates`） |
| **連線（狀態）** | 各服務 `/actuator/health`（**與 §9.8 同源**） | 該路由之目標服務**是否連得上** |

> ★ **兩者並列才完整**：只看路由不知通不通；只看健康不知入口在哪。

#### 9.10.2 前置變更（gateway）——⚠️ **原設計經實測後修正（2026-09-30）**

**原擬**：於 `api-gateway` 之 `management.endpoints.web.exposure.include` 加入 `gateway`，
以取用 `/actuator/gateway/routes`。

**實測結果（本版本不可用）**：

| 檢查 | 結果 |
|---|---|
| `/actuator` 索引 | 僅 **7 個** endpoint：`prometheus`／`beans`／`env`／`info`／`health`／`metrics`／`refresh`——**無 `gateway`** |
| 啟動日誌 | `Exposing 7 endpoints beneath base path '/actuator'` |
| 路由屬性之前綴 | `spring.cloud.gateway.server.**webflux**.routes[...]`（Spring Cloud Gateway 2025 已改前綴） |

→ **`gateway` actuator endpoint 於本版本未被註冊**（非 exposure 設定問題——已加入 `include` 仍無效）。

**改採方案：自訂唯讀端點**

**`GET /api/v1/gateway/routes`**（`api-gateway` 自身提供）

| 欄位 | 說明 |
|---|---|
| `id` | 路由識別碼（如 `authorization`） |
| `uri` | 目標 URI |
| `predicates` | 述詞原文（如 `Path=/auth-api/**`） |
| `filters` | 過濾器原文（如 `RewritePath=/auth-api/(?<segment>.*), /api/$\{segment}`） |

> ⚠️ **`$\{segment}` 之反斜線是組態原文，不是筆誤**——`application.yml` 即如此書寫；
> Spring 之 `RewritePathGatewayFilterFactory` 會執行 `replacement.replace("$\\", "$")`
> 再交給 `String.replaceAll`（**經 `javap -c` 確認**）。本端點**原文透傳、不代為去轉義**
> ——去轉義等於**隱藏真實組態**，維運反而看不到「實際寫了什麼」。

**要求**：
1. **唯讀**——不提供新增／修改／刪除路由（避免誤觸造成路由中斷）。
2. **僅供內部維運**（`bff-web` 聚合用）——**不對外**；仍受 gateway 之認證保護。
3. ⚠️ **不得**以 `management.endpoint.env.show-values=ALWAYS` 替代——
   那會暴露**所有**環境變數（含憑證），**安全風險**，**不可接受**。

**★ 實作要點（2026-09-30 實作，`#A-103`）**：

| 項目 | 決定 | 理由 |
|---|---|---|
| 資料來源 | `RouteDefinitionLocator`（**非** `RouteLocator`） | 前者為**組態宣告**、保留宣告順序且可還原原文；後者已解析為 `Predicate`／`GatewayFilter` **物件**，**無法還原原文**（違反「原文透傳」） |
| 原文還原 | `Name=arg0, arg1`（依 `getArgs()` 之 `LinkedHashMap` 插入順序串接） | `_genkey_0`、`_genkey_1`… 之插入順序即組態順序（**經 `javap` 確認欄位為 `LinkedHashMap`**，非 `HashMap`） |
| 路由順序 | **維持宣告順序**，不重排 | 與 `application.yml` 逐條對照（排序會失去對照性） |
| 回應格式 | 統一封套（`CSP-003 §3`），`data` 為路由**陣列** | 全系統一致；`bff-web` 讀 `data` |
| 端點路徑 | `/api/v1/gateway/routes`（**非** `/actuator/**`） | 自訂端點；與既有路由前綴（`/auth-api/**` 等）**不重疊**，不會被述詞攔截 |
| 追蹤碼取值 | 於 controller 方法**進入時**取（非於 `map` 內） | `map` 之執行執行緒不保證仍帶 OTel 脈絡（需 `Hooks.enableAutomaticContextPropagation()`），於請求執行緒取值可確保回應 `traceId` 與日誌同源（`CSP-004 §6`） |
| 錯誤處理 | **不**自訂錯誤碼 | `SDD-006 §3.4` 無對應之 `CC-GW-*` 碼（僅 1001／1002／1003／4290／5001）；**不得自創碼**。且本端點讀記憶體組態、無實際失敗路徑，失敗時由 `bff-web` 以 `routesAvailable=false` 降級呈現 |

**★ 真機驗證（2026-09-30，`#A-103`）**：

| 驗證項 | 結果 |
|---|---|
| `GET /api/v1/gateway/routes`（SSO 開啟、無 token） | **401**——證實端點**受閘道認證保護**（要求 2「仍受認證保護」成立） |
| `GET /api/v1/gateway/routes`（SSO 關閉） | **200**、`data` 為 **17 筆**路由、順序＝`application.yml` 宣告順序、`predicates`／`filters` 原文正確 |
| `GET /api/v1/bff/endpoints` | **200**、**`routesAvailable=true`**（修正前為 `false`）、`routesError=null`、**17 筆路由**且 `service` 由 `routeId` 正確推導（`authorization` → `authorization-service`） |
| 連線維度（未受影響） | 17 筆、**UP 7/17**——與「7 個領域服務在跑」一致（`§9.10.4 #1` 兩維度獨立成立） |

> ⚠️ **驗證方式之誠實揭露**：取 Keycloak token 之指令**遭安全審批攔截**（逾時未授權），
> 故上表第 2 列改以**暫時關閉 SSO** 完成（`CARDCORE_SECURITY_ENABLED=false` 之 **JVM 覆寫**，
> **不動 `sso.env` 檔**）；**測畢已還原**，並確認 `sso.sh status` 為「**同調（兩側皆 true）**」。
> 處置與 `#A-102 ⑦ (iii)` 相同（截圖期間暫時關閉 SSO）。

> **教訓**：**「actuator endpoint 存在」是需實測的假設**，非查文件即可斷定；
> 且 Spring Cloud 版本演進會改變設定前綴與 endpoint 註冊條件。

#### 9.10.3 端點規格

**`GET /api/v1/bff/endpoints`**（回應 `BffEndpoints`，`200`）

| 欄位 | 說明 |
|---|---|
| `checkedAt` | 本次查詢時刻 |
| `routes[]` | `routeId`／`uri`／`predicates[]`／`service`（自 `routeId` 對映之服務名） |
| `connections[]` | `service`／`status`（`UP`／`DOWN`）／`latencyMs`／`error` |
| **`routesAvailable`** | gateway 路由是否取得成功（`false` 時 `routes[]` 為空並附 `routesError`） |

#### 9.10.4 行為要求

1. **兩部分獨立**：gateway 路由取不到**不影響**連線狀態之呈現（反之亦然）——
   分開回報，**不使整體失敗**。
2. **並行**探測連線（逾時 2 秒，同 §9.8）；gateway 路由查詢逾時 3 秒。
3. **不快取**。
4. ⚠️ **`service` 之對映**：由 `routeId` 推導（如 `authorization` → `authorization-service`）；
   **無法對映者**保留 `routeId` 原文並標示，**不臆造**。
5. ⚠️ **本頁不含「容錯切換」操作**——僅**呈現**狀態；
   切換屬維運程序（gateway 之 `refresh`／部署流程），**不由作業台直接觸發**（避免誤觸造成路由中斷）。

#### 9.10.5 未竟

- `predicates` 之呈現為**原文透傳**（如 `Path=/auth-api/**`）——不解析為結構化欄位（避免過度設計）。
- 本端點**不涵蓋**非 HTTP 之連線（DB／Kafka／Redis）——那屬各服務之 `/actuator/health` 細部
  與 Prometheus 指標範疇（`§9.8.5` 同一原則）。
- gateway 端點回傳之 `filters`（過濾器原文）**未由 `bff-web` 呈現**——`§9.10.3` 之 BFF 契約
  未含該欄位（作業台僅需「入口在哪」）。若日後需於頁面顯示改寫規則，須**同步擴充
  `§9.10.3` 契約與 `SDD-007 §5.4.27`**，**不逕行加欄位**。
- gateway 端點**無自訂錯誤碼**：`SDD-006 §3.4` 之 `CC-GW-*` 僅 5 碼（1001／1002／1003／4290／5001），
  無「路由查詢失敗」之語意，且 `CSP-003` **禁止 Coder 自創碼**——故不設錯誤映射，
  失敗時由 `bff-web` 以 `routesAvailable=false` 降級呈現（`§9.10.4 #1`）。

---

### 9.11 營運總覽聚合端點（`bff-web`）與作業台總覽頁（2026-10-01）

> **緣由**：`SDD-007 §5.1.2.3` 之桶 C「**營運總覽**」（`/dashboard`）——原記
> 「**跨服務聚合**——須先於 `bff-web` 立聚合端點（`§5.4.24` 之健康聚合為其子集）」。
> 依「規格先行」：先立本節 → 改 `tools/gen_openapi.py` → 重生契約 → 實作 →
> `SDD-007 §5.4.28` 逐鈕定義 → 頁面。
> **BEAM PMO 2026-10-01 裁定**採「**挑投報率最高者，先補後端、再規格先行、再實作**」——本頁為選定目標。

#### 9.11.1 ★ 範圍界定（**務必先讀**）

| 面向 | 本版 | 說明 |
|---|---|---|
| **系統營運面** | ✅ **本版** | 服務健康摘要／批次作業摘要／端點連線摘要——**`bff-web` 自足**，**不需新增領域服務端點** |
| **業務 KPI 面** | ⚪ **第二階段** | 發卡量／授權量／待辦件數等，**需各領域服務新增統計端點** |

> ⚠️ **回訪查證（2026-10-01）**：掃 `contracts/openapi/*.yaml` 全數 17 服務，
> **皆無「統計／摘要」端點**（僅 CRUD 與列表）。故業務 KPI **無法**由既有端點推得，
> **不得**以「服務有端點」推斷「已能呈現業務指標」。
> → 本版**明載**為系統營運面；業務 KPI **另立 CR**（見 `§9.11.5`）。

#### 9.11.2 端點規格

**`GET /api/v1/bff/dashboard`**（回應 `BffDashboard`，`200`）

| 欄位 | 型別 | 說明 |
|---|---|---|
| `checkedAt` | `date-time` | 本次彙整時刻 |
| `services` | object | `total`／`up`／`down`／`overall`（`UP`／`DEGRADED`／`DOWN`，判定同 `§9.8.2`） |
| `batch` | object | 依**統一狀態分組**（`§9.9.2`）計數：`pending`／`running`／`succeeded`／`warning`／`failed`／**`unknown`**／`unavailable`。⚠️ **`unknown` 必須計入**——服務回傳未定義狀態時**如實歸此**，**不得**略過（略過會使計數總和對不上，且掩蓋異常狀態） |
| `endpoints` | object | `routes`（gateway 路由數）／`connectionsUp`／`connectionsDown` |

**各區塊皆可為 `null`**（該區塊聚合失敗時）——前端須如實顯示「不可用」，**不得**以 0 充數。

#### 9.11.3 行為要求

1. **複用既有聚合服務**（`HealthAggregationService`／`BatchMonitorService`／
   `EndpointManagementService`，**同一 JVM**）——**不重複實作探測邏輯**；
   三者**並行**執行，避免回應時間線性累加（三區塊各自之逾時語意沿用原服務）。
2. **區塊降級**：任一區塊失敗 → 該區塊為 `null`，**不拖垮整體**——
   與 `BffSection` 之「單一下游失敗不拖垮整體」**同一政策**（`SAD-004 §2.2`）。
3. **不快取**——與 `§9.8.3 #3` 同（快取會使畫面顯示過期狀態）。
4. **fail-closed**：無目標 → `services.overall` 回 `DOWN`（`§9.8.6`），**不以樂觀預設值掩蓋**。

#### 9.11.4 與 `§9.8`～`§9.10` 之關係（**非重複造頁**）

本端點為**摘要層**（一眼看全貌）；`/system/health`／`/system/batch`／`/system/endpoints` 為**明細層**（逐項檢視）。
→ `/dashboard` **不取代**該三頁，亦**不重複**其探測邏輯（**複用**，見 `§9.11.3 #1`）。

#### 9.11.5 未竟（誠實揭露）

- **業務 KPI 未納入**（見 `§9.11.1`）——需各領域服務提供**統計端點**，**建議另立 CR**。
- 本端點**不聚合**歷史趨勢與告警規則——仍由 Prometheus／Grafana 負責（同 `§9.8.4` 之定位說明）。

#### 9.11.6 業務 KPI 區塊（`BffDashboard.business`，`CR-026`）

`CR-026` 補實作「業務 KPI 第二階段」——`BffDashboard` 新增 `business` 欄位（`oneOf` `BffDashboardBusiness` 或 `null`）。

**聚合來源**：各領域服務之統計端點（`card-service` `cards/stats`＋`card-applications/stats`、`authorization-service` `authorizations/stats`、`dispute-service` `disputes/stats`），由 `BusinessStatsService`（`application`）**並行** 4 路 `CompletableFuture` 呼叫（沿用 token-relay 之 `DelegatingSecurityContextExecutor` 跨執行緒傳遞 `SecurityContext`），逾時 `STATS_TIMEOUT_MS=5000`。

**`business` 之 4 子塊**（各 `oneOf` schema-or-null，獨立降級）：

| 子塊 | 來源端點 | 欄位 |
|---|---|---|
| `cards` | `GET /cards/stats` | `total`／`byStatus`／`byBlockCode` |
| `applications` | `GET /card-applications/stats` | `total`／`byStatus`／`pendingReview` |
| `authorizations` | `GET /authorizations/stats` | `total`／`today`／`byDecision`／`pendingUpload` |
| `disputes` | `GET /disputes/stats` | `total`／`byStatus`／`openCases` |

> ⚠️ **獨立降級**：任一統計端點取不到（逾時／非 200／解析失敗）→ 該子塊為 `null`，**不以 0 充數**；其餘子塊不受影響。`DashboardAggregationService` 之 `business` 區塊失敗 → 整體 `business=null`（區塊級降級），與 `§9.11.3 #2` 同政策。

---

### 9.12 分流比例管理端點（`api-gateway`）與作業台分流頁（2026-10-01）

> **緣由**：`SDD-007 §5.1.2.3` 桶 C 之「**分流比例管理**（`/system/split-ratio`）」原記
> 「**僅 actuator**」——即**機制已實作**（`SAD-004 §2.4`：`CycleSplitDecision` ＋
> `CycleSplitGlobalFilter` ＋ `CycleSplitProperties`，POC-13 已驗收），
> 惟**只有 `POST /actuator/refresh`**，**無管理端點**：維運無法從作業台讀取當前比例，
> 亦無法在畫面調整／回退——**必須登入機器改 `config/cycle-split.yml`**。
> 本節為該缺口之規格先行補建（比照 `#A-101`／`#A-102`／`#A-103` 模式）。

#### 9.12.1 選頁理由（投報率）

| 比較項 | 說明 |
|---|---|
| **機制是否已存在** | ✅ **完整存在**（`SAD-004 §2.4`；POC-13 七項驗收通過）——缺的**只是管理面** |
| **是否需新領域模型** | ❌ **不需要**（組態即資料；`CycleSplitProperties` 已為 `@ConfigurationProperties`） |
| **是否會造空殼／假資料** | ❌ **不會**——讀的是**真實生效值**，寫的是**真實熱更新檔** |
| **業務價值** | ★ **灰度放量與回退**為遷移期**最高頻之維運操作**（`BRD-001 §10.3 C-03` 波次 W0→W4）；回退速度直接對應事故處置時間 |

#### 9.12.2 端點規格（`api-gateway` 自供；同 `§9.10.2` 之既有慣例）

| 方法 | 路徑 | 說明 |
|---|---|---|
| `GET` | `/api/v1/gateway/split-ratio` | 讀取**當前生效**之分流組態 |
| `PUT` | `/api/v1/gateway/split-ratio` | 調整 `enabled`／`percentage` 並**觸發熱更新** |

**`SplitRatioView`（回應）**：

| 欄位 | 型別 | 說明 |
|---|---|---|
| `enabled` | boolean | 是否啟用分流干預 |
| `percentage` | integer | 分流比例 0–100（**新系統承接比例**） |
| `legacyUri` | string | 舊系統（Legacy ECS）轉接目標 |
| `merchantHeader` | string | 承載商店代號之標頭（分流維度） |
| `pathPrefix` | string | 分流作用之路徑前綴 |
| `configFile` | string \| null | 熱更新來源檔之**實際路徑**（解析 `spring.config.import` 之 `file:` 條目）；**未設定時為 `null`** |
| `configFileWritable` | boolean | 該檔是否**存在且可寫**（`false` 時 `PUT` 必失敗——**先揭露，不讓維運白試**） |
| `effectiveMatchesRequest` | boolean \| null | **僅 `PUT` 回應**：套用後**重讀**之值是否與請求一致（誠實驗證）；`GET` 為 `null` |
| `warning` | string \| null | **僅 `PUT` 回應**：`effectiveMatchesRequest=false` 時之說明；其餘為 `null` |

**`SplitRatioUpdateRequest`（請求）**：`enabled`（boolean，選填）、`percentage`（integer，**必填**、`0..100`）。

#### 9.12.3 行為要求

1. **驗證**：`percentage` **必填**且須為**整數 0–100** → 不符回 **`400` `CC-PLT-4003`**（**不自行另立錯誤碼**，`CSP-003 §2`）。⚠️ **由控制器自行驗證並組裝錯誤封套**——本模組**無** `common-spring` 之全域例外處理器（該類別非 auto-configuration，且本模組未掃描其套件），故**不使用** Bean Validation 之自動例外轉譯。
2. **定點行替換寫檔**：僅改寫 `cycle-split.yml` 之 `enabled:`／`percentage:` **兩行**——**保留檔內註解與其餘鍵**；**不重寫整檔**（避免抹掉維運註記）。
3. **觸發熱更新**：呼叫 `ContextRefresher.refresh()`（**等同** `POST /actuator/refresh`，`SAD-004 §2.4` 之既有機制）——**不重啟**。
4. **回讀驗證（★ 誠實原則）**：由**重綁後**之 `CycleSplitProperties` 讀回**實際生效值**；與請求不符時 `effectiveMatchesRequest=false` ＋ `warning`（**不以請求值冒充生效值**）。
5. **稽核**：記 `INFO` 日誌（`traceId`、`before` → `after`、目標檔）——對應 `SAD-004 §2.4` 之「每一筆轉接均須有可追溯之 LOG」之同源要求。
6. **失敗不靜默**：檔不可寫／refresh 拋錯 → **`500` `CC-PLT-5000`** 並帶原因（**不得**回 200 假成功）。
7. **不快取**。
8. **安全性**：端點**不另設白名單**——沿用閘道之既有認證鏈（`GatewaySecurityConfig`：啟用時除健康探測外一律須驗 JWT）。⚠️ **角色級授權未實作**（見 §9.12.5）。

#### 9.12.4 前端（`/system/split-ratio`）

依 `SDD-007 §5.4.30` 之逐鈕定義實作；開發期由 Vite 代理 `/gateway-api` → `api-gateway:8100`。

#### 9.12.5 未竟（誠實揭露）

- **角色級授權未實作**：任何通過閘道認證之使用者皆可調整比例——**正式環境應限維運角色**（待 `SAD-005 §4` 之角色矩陣落地）。
- **未寫入稽核表**：僅記日誌（無 `audit_trail` 落庫）——閘道**無資料庫**（`SAD-001 AP-02`）。
- **不支援回退歷史**：不保存比例變更歷程（需查日誌）。
- **僅單一組態檔**：多實例部署時各實例之 `cycle-split.yml` **須自行同步**（本端點只改**本實例**之檔）——⚠️ **正式環境應改為集中組態來源**（如 Config Server）。
- **請求主體非合法 JSON 時之回應非本封套**：該情境由 WebFlux 於**進入控制器前**擋下，回應為框架預設格式（**非** `ApiResponse`）——此為**已知落差**（`CSP-003 §3` 要求所有回應一律套用封套；本模組無全域例外處理器，故無法覆蓋此路徑）。
- **`GET` 讀的是生效值、非檔原文**：若組態檔與生效值不一致（例如改檔後未 refresh），`GET` 呈現的是**生效值**——**此為刻意**（維運要問的是「現在怎麼分流」），惟畫面**不呈現**檔內原文。

---

### 9.5 已知限制（本次未解決，誠實揭露）

1. **`service_name` 標籤恆為 `unknown_service`**——實測三種寫法
   （`move to: resource[...]`、`add field: resource[...] value: EXPR(...)`、OTTL 括號鏈式）
   皆無法將 `service.name` 寫入 resource，且**不報錯**。
   替代：Loki 自動萃取 body 之 `service.name` 為 `service_name_extracted`，仍可依服務辨識。
   根治方案：改為「每服務一個 filelog receiver ＋ 靜態 resource」，或改採 Grafana Alloy。
2. ~~**ECS 欄位名與 `CSP-004 §3` 不一致**~~ → ✅ **已解除（BEAM PMO 裁定「改規格」，`CSP-004` 升 1.1）**。
   原落差：規格要求 `ts`／`level`／`msg`／`correlationId`／`event`，
   實測輸出為 `@timestamp`／`log.level`／`message`，且**無 `event`**。
   <br>**處置（規格遷就實測，非放寬要求）**：`CSP-004 §3.1` 欄位對照改為**實測值**；
   §3.2 明載 `event` 事件碼**本期不實作**（專案尚未定義事件碼表，且 `CSP-003 §2` 禁止 Coder 自創碼，
   故**不設恆為空值之欄位**）；§6 廢除第二套 `correlationId`，統一以 OTel `traceId` 為準。
   <br>★ **2026-09-21 複核實測**（真實 `posting-service.json`，137 筆）：
   `@timestamp` 137／`log`（含 `level`）137／`service`（含 `name`）137／`message` 137／
   `traceId` 4／`spanId` 4／`ecs.version` 137——**與 `CSP-004 §3.1` 完全一致**，且**無 `event` 欄位**（符合 §3.2）。
   `traceId` 僅 4 筆係取樣率 `TRACING_SAMPLING` 預設 **0.1** 所致，非缺漏。
   <br>樣本：`{"@timestamp":"…","log":{"level":"DEBUG",…},"service":{"name":"posting-service",…},
   "message":"…","traceId":"ce98fa43…","spanId":"ed89ddf5…","ecs":{"version":"8.11"}}`
3. ✅ **API 回應之 `traceId` 與日誌之 `traceId` 為不同值** → **已修（2026-09-20，`C-29` 程式面）**。
   實測同一請求：回應 `10b8a469…`、日誌 `894ff808…`，兩套 ID 無法相互對照。
   <br>**根因比「有兩套 ID」更嚴重（本次實測盤點才發現）**：`common-spring` 之
   `CorrelationIdFilter` **從未被註冊為 bean**——`AutoConfiguration.imports` 只登錄
   Security×2 ＋ ServiceClient，該類別亦**無** `@Component`，故 17 個服務之 component scan
   皆不拾取。結果 `MDC.get("trace_id")` **恆為 `null`**，`TraceContext.currentTraceId()`
   **每次呼叫都現產一個新的亂數**——回應永遠帶著一個「看似合法」之 32 碼，
   實則與任何日誌都對不起來。屬**「靜默不做」型缺陷**（不報錯、不為空、功能卻不存在）。
   <br>**處置**：依 `CSP-004 §6`（BEAM PMO 2026-09-20 裁定「以 OTel trace ID 為準」），
   `TraceContext` 改為**優先取 OTel 當前 span 之 traceId**，其次 MDC，最後才產生；
   OTel 缺席時降級並**只記一次 WARN**（使「刻意不接」與「忘了接」可區分）。
   新增 `TraceContextTest` 7 例，並以**負樣本**驗證判定力（停用 OTel 優先 → 3 例精準轉紅）。
   `common-spring` **53 → 60 測試、0 失敗**。
   <br>✅ **已裁定並實作（BEAM PMO 2026-09-21 18:57：保留並註冊）**：`CorrelationIdFilter` 由
   `CorrelationIdAutoConfiguration`（已登錄 `AutoConfiguration.imports`）以 `FilterRegistrationBean`
   **註冊**，並**顯式**指定 `ORDER`（`LOWEST_PRECEDENCE - 100`）以確保**晚於** OTel 觀測範圍——
   故 MDC 與 OTel **不會**出現兩個值（`resolve()` 之優先序：OTel span → `traceparent` → `X-Correlation-Id` → 產生）。
   測試：`CorrelationIdFilterTest`／`TraceContextTest`。啟動日誌可見 `Filter 'correlationIdFilter' configured for use`。
   <br>⚠️ **原「待裁定／未擅自註冊」之記載係過期**（早於 09-21 之裁定），已更正。
4. ~~**僅 `party-service` 完成接線**（觀測 1/17）~~ → **已於 §9.6 推廣至 19/19 並逐模組實測**
5. ~~**結構化日誌無輪轉設定**~~ → ✅ **已查明為誤載（2026-09-21 實測）**：
   原記載「`logging.file.name` 未配置 `max-size`／`max-history`，長時間執行會持續增長」——
   **實測顯示輪轉正常發生**：`ops/local/logs/` 內確有
   `posting-service.json.2026-09-20.0.gz`／`statement-service.json.…`／`api-gateway.json.…` 等
   **5 個輪轉檔**，logs 目錄總量僅 **1.4M**。
   <br>**原因**：Spring Boot 於啟用 `logging.file.name` 時即帶有**預設輪轉組態**
   （`max-size` 10MB、`max-history` 7、檔名樣式 `${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz`），
   毋須於 `application.yml` 顯式宣告即生效——**「組態檔沒寫」≠「沒啟用」**。
   <br>⚠️ **仍存之真實顧慮（非「無輪轉」）**：`total-size-cap` 預設為 **0（無上限）**；
   19 個服務之最壞情況約為 `7 × 10MB × 19 ≈ 1.3GB`。
   <br>✅ **已裁定並實作（BEAM PMO 2026-10-01：5GB；`GOV-007 §3`）**：19 個 `application.yml` 均設
   `logging.logback.rollingpolicy.total-size-cap: 5GB`（並同步產生器模板）。
   ⚠️ **併更正本節原建議之屬性名**：原寫 `logging.file.total-size-cap`，**該屬性不存在**；
   Spring Boot 4.1.1 之正確屬性為 **`logging.logback.rollingpolicy.total-size-cap`**
   （預設 `0B`＝無上限；實查 `spring-configuration-metadata.json` 確認）。
   ⚠️ **per-service 語意（誠實揭露）**：Logback 之 `total-size-cap` 係 **per-appender**；
   本地 19 模組共用 `ops/local/logs/` 時，理論上限為「19 × 5GB」——惟 `max-history: 7` × `max-file-size: 10MB`
   先約束單服務約 70MB，故本地實測總量（~1.3GB）**遠低於 5GB**；設 5GB 為**硬天花板**
   （供日後放寬 `max-history` 時仍受約束）。**若需「本地目錄總量 ≤ 5GB」之硬保證，應改設每服務 ≈ 256MB**。
6. Grafana 儀表板尚未建立（§8 已列為待 SRE 凍結）。
