# CSP-004　可觀測性與日誌規範（Observability & Logging）

| 項目 | 內容 |
|---|---|
| 文件編號 | CSP-004 |
| 版本 | **1.2**（`C-29` 程式面落地：新增 §6.1 單一讀取端 `TraceContext`，並記錄 `CorrelationIdFilter` 從未註冊之實測根因） |
| 狀態 | 已核定 |
| 撰寫者 | SA / SRE |
| 審查者 | Security Architect |
| 對應需求 | NFR-PERF 效能群組、NFR-OPS 維運群組；SAD-006 |

---

## 1. 目的

定義統一的**結構化日誌、指標、分散式追蹤、健康檢查**標準，使 17 個服務具備
一致的可觀測性，滿足 SoW 之效能門檻（授權端到端 P95 ≤ 500ms、核心 P99 ≤ 200ms、路由 ≤ 5ms）。

---

## 2. 三支柱

| 支柱 | 技術基線 | 產出 |
|---|---|---|
| Logs | SLF4J + Logback，**JSON 結構化輸出** | 每行一筆 JSON 事件 |
| Metrics | Micrometer → Prometheus | 計數器、計時器、分佈摘要 |
| Traces | OpenTelemetry → Tempo/Jaeger | 跨服務呼叫鏈 |

---

## 3. 結構化日誌規範

**產出方式**：一律使用 Spring Boot 4.1 **內建**之結構化日誌
（`logging.structured.format.file=ecs`），**不自訂 Logback encoder**——
官方 Logback→OTel 橋接於 4.1 尚未內建、可用 appender **全為 `-alpha`**
（詳 `SCS-020 §9.6`）。日誌寫檔後由 Collector 之 `filelog` receiver 讀取，
零第三方相依。

### 3.1 欄位對照（**實測為準**）

ECS 輸出為「巢狀／扁平混用、駝峰與底線並存」，**勿依 ECS 慣例猜測**：

| 邏輯欄位 | 實際 JSON 路徑 | 說明 |
|---|---|---|
| 時間 | `@timestamp` | ECS 慣例以 `@` 開頭，**非** `ts` |
| 層級 | `log.level`（**巢狀**） | **非**頂層 `level` |
| 服務 | `service.name`（**巢狀**） | **非**頂層 `service` |
| 訊息 | `message` | **非** `msg` |
| 追蹤 ID | `traceId`（頂層**駝峰**） | ＝ OTel trace ID，見 §6 |
| 跨度 ID | `spanId`（頂層**駝峰**） | |
| 事件碼 | —— | **本期未實作**，見 §3.2 |

```json
{"@timestamp":"2026-09-20T10:20:30.123Z","log.level":"INFO",
 "service.name":"authorization-service","traceId":"894ff808…","spanId":"e5f6",
 "message":"AUTH_APPROVED cardRef=****1234 amount=30000 currency=TWD"}
```

### 3.2 事件碼（`event`）**本期不實作**

原 v1.0 §3 要求之 `event` 事件碼，**本期不實作**——專案尚未定義事件碼表，
且事件碼須與 `SDD-006 §3.4` 之錯誤碼一併規劃（`CSP-003 §2`：Coder 不得自創碼）。

在事件碼表核定前，以**結構化參數**記錄事件事實：

```java
log.info("AUTH_APPROVED cardRef={} amount={} currency={}", mask(card), amt, cur);
```

由 `message` 與附帶欄位承載語意，**不另設 `event` 欄位**——
以免產生恆為空值之欄位（空欄位＝假訊息，與「不可假綠」同旨）。

- **禁止 `string concatenation` 拼日誌**：一律使用結構化參數。
- **日誌層級**：`ERROR`＝需告警、`WARN`＝需關注、`INFO`＝關鍵業務事件、`DEBUG`＝僅開發。

---

## 4. 個資遮蔽（對應 SAD-005）

日誌與追蹤中出現之敏感欄位**必須經 `common-security` 遮蔽工具**處理：

| 欄位 | 遮蔽規則 |
|---|---|
| 卡號 | 僅留末 4 碼（`****1234`） |
| 身分證字號 | 僅留首 1 + 末 1（`A*****8`） |
| 手機 | 中段遮罩（`0912***567`） |
| CVV / 密碼 | **禁止**出現於任何日誌／追蹤 |

```mermaid
flowchart LR
    A[業務邏輯] --> B[log.info 結構化]
    B --> C{含敏感欄位?}
    C -- 是 --> D[common-security.mask]
    C -- 否 --> E[輸出 JSON]
    D --> E
    style D fill:#FFF6E0,stroke:#C8A45C
```

---

## 5. 指標規範

每服務必須暴露（Prometheus 格式）：

| 指標 | 型別 | 標籤 | 用途 |
|---|---|---|---|
| `http_server_requests_seconds` | Histogram | `service,route,status` | 延遲分佈（算 P95/P99） |
| `auth_decision_total` | Counter | `result(approve/decline/standin)` | 授權決策量 |
| `posting_invariant_violation_total` | Counter | `type` | 餘額不變式違反（必須 ≈0） |
| `downstream_calls_total` | Counter | `target,result` | 下游健康 |
| `jvm_memory_used_bytes` | Gauge | — | 資源 |

**SLO 範例**：授權 `http_server_requests_seconds{route=/authorizations}` P95 ≤ 0.5s、可用性 ≥ 99.99%。

---

## 6. 分散式追蹤

- 所有對外 HTTP／Kafka 呼叫自動注入 `traceparent`（W3C）。
- **跨系統之唯一識別為 OTel `traceId`**（W3C 標準，32 位十六進位），
  由 OTel 於入口產生並貫穿全鏈（含 Kafka header）。
- ⚠️ **不再另設第二套 correlation ID**。原 v1.0 §3／§6 規定之 `correlationId`
  與 OTel `traceId` 實為**兩套 ID**——實測同一請求：
  API 回應 `10b8a469…`（`common-spring` correlation ID）
  ≠ 日誌 `894ff808…`（OTel trace ID），**兩者無法相互對照**，
  客戶持 API 回應之 ID 查不到對應日誌（`SCS-020 §9.5 #3`）。
  經裁定（BEAM PMO 2026-09-20）**統一以 OTel `traceId` 為準**：
  **API 回應之 `traceId` 與日誌之 `traceId` 必須為同一值**。
- 取樣率預設 1/100；錯誤路徑（4xx/5xx）強制 100% 取樣。
- **禁止**在追蹤 span 中記錄敏感 payload；僅記錄長度與欄位名。

### 6.1 實作點：單一讀取端 `TraceContext`（2026-09-20 落地）

**回應封套之 `traceId` 一律經 `common-spring` 之 `TraceContext.currentTraceId()`
取得**，不得另行產生。取值順序：

1. **OTel 當前 span 之 `traceId`**（權威）
2. MDC 之 `trace_id`（OTel 不在場時之降級路徑）
3. 產生 32 碼臨時值（確保永不為空，`SAD-001 AP-09`）

> ⚠️ **實測發現之根因（比「有兩套 ID」更嚴重）**：`CorrelationIdFilter`
> **從未被註冊為 bean**——`AutoConfiguration.imports` 僅登錄 Security×2 ＋ ServiceClient，
> 該類別亦無 `@Component`。故 `MDC.get("trace_id")` 恆為 `null`，
> `currentTraceId()` 每次呼叫都**現產一個新的亂數**，回應帶著一個「看似合法」
> 卻與任何日誌都對不上之追蹤碼——**「靜默不做」型缺陷**。
> 改以 OTel 為權威後，只要 OTel 在場即不再受此缺陷影響。

- **OTel 缺席時**：降級至 MDC／臨時值，並**只記一次 WARN**
  （使「刻意不接 OTel 之模組」與「忘了接」在日誌上可區分）。
- ⚠️ **待裁定**：`CorrelationIdFilter` 註冊或退役（其職責已被 OTel 取代；
  註冊涉及過濾器順序，順序錯會讓 MDC 與 OTel 出現兩個值，故**未擅自註冊**）。

---

## 7. 健康檢查與告警

- `GET /actuator/health/liveness`：程式存活（不依賴外部）。
- `GET /actuator/health/readiness`：依賴（DB、下游、Kafka）就緒。
- 告警分級：P1（可用性跌幅／不變式違反）即時通訊軟體＋電話；P2（延遲超標）工單。

---

## 8. 禁止事項

| 編號 | 禁止 | 原因 |
|---|---|---|
| O1 | 明文印卡號／密碼／CVV | 資安（SAD-005） |
| O2 | 生產開啟 `DEBUG` 全域 | 效能與個資風險 |
| O3 | 以 `System.out.println` 代替日誌框架 | 無結構、無遮蔽 |


---

## 9. 修訂記錄

| 版本 | 日期 | 修訂者 | 說明 |
|---|---|---|---|
| 1.0 | 2026-09-17 | SA / SRE | 初版 |
| 1.1 | 2026-09-20 | WorkBuddy AI | **依實測結果修正規格（BEAM PMO 2026-09-20 裁定）**：①**§3 欄位對照改為實測值**——原規定 `ts`／`level`／`service`／`msg` 與 Spring Boot 4.1 內建 ECS 之實際輸出 `@timestamp`／`log.level`（巢狀）／`service.name`（巢狀）／`message` 不符；②**§3.2 `event` 事件碼本期不實作**——專案尚未定義事件碼表，且須與 `SDD-006 §3.4` 錯誤碼一併規劃（`CSP-003 §2` 不得自創碼），改以結構化參數承載語意，**不設恆為空值之欄位**；③**§6 廢除第二套 `correlationId`**，統一以 OTel `traceId` 為跨系統唯一識別——原設計使 API 回應與日誌之 ID 互不相通（實測 `10b8a469…` ≠ `894ff808…`），查測時無法對照。本次為**規格遷就實測**（ECS 輸出為框架既成事實，自訂 encoder 成本過高），非放寬要求 |
| 1.2 | 2026-09-20 | WorkBuddy AI | **`C-29` 程式面落地**：新增 **§6.1 單一讀取端 `TraceContext`**（取值順序：OTel span → MDC → 臨時值；OTel 缺席時降級並只記一次 WARN）。⚠️ **並記錄實測發現之根因**：`CorrelationIdFilter` **從未被註冊為 bean**（`AutoConfiguration.imports` 無 web 自動配置、類別亦無 `@Component`）→ `MDC.get("trace_id")` 恆為 `null`，`currentTraceId()` 每次現產一個亂數，回應看似帶合法追蹤碼實則與日誌對不上——**「靜默不做」型缺陷**。⚠️ **待裁定**：該過濾器註冊或退役（職責已被 OTel 取代；順序錯會造成兩個值，故未擅動） |

| O4 | 指標名稱含具體實例 IP／Pod 名 | 聚合失效 |
| O5 | 每秒高頻 log 同一事件無節流 | 磁碟／成本爆量 |
