# 可觀測性：三態健康檢查與 tracing 抽象層

> gortex 的三態健康檢查比 up/down 更貼近 K8s probe，tracing 只依賴 Tracer 介面，OTel 當 adapter 接上，換後端不動業務碼。

原文：https://sheng.page/posts/gortex-observability-health-tracing/ · 發布：2026-06-06 · 作者：Sheng

三態健康檢查（Healthy / Degraded / Unhealthy）比 up/down 更貼近 K8s probe。tracing 則只依賴一個 `Tracer` 介面，OTel 當 adapter 接上。這篇講介面驅動的可觀測性。

> 前置閱讀：〈[interface 與組合](/posts/go-interfaces-and-composition/)〉

## 三態健康檢查

傳統健康檢查只有 up/down，但真實世界常是「還能跑但快不行了」。gortex 用三態，貼近 K8s 的 readiness / liveness：

```go
const (
    HealthStatusHealthy   HealthStatus = "healthy"
    HealthStatusDegraded  HealthStatus = "degraded"  // 還能服務，但接近極限
    HealthStatusUnhealthy HealthStatus = "unhealthy"
)
```

聚合採 worst-wins：任一 check 是 Unhealthy，整體就 Unhealthy；否則有 Degraded 就 Degraded；全好才 Healthy。內建的 `MemoryHealthCheck` 示範得很清楚：超過上限是 Unhealthy，到 80% 就先報 Degraded 預警。

## 並行檢查與逾時

`HealthChecker` 背景開一個 goroutine 按 interval 定時跑（`runChecks` + ticker），結果快取起來，`/healthz` 那類端點直接讀快取、不每次現跑。

實際跑的時候所有 check 並行，各自帶 timeout，一個慢 check 不會拖垮其他：

```go
for name, check := range checks {
    wg.Add(1)
    go func(n string, c HealthCheck) {
        defer wg.Done()
        checkCtx, cancel := context.WithTimeout(ctx, hc.timeout)
        defer cancel()
        result := c(checkCtx) // 每個 check 各自的 deadline
        // ...存結果
    }(name, check)
}
wg.Wait()
```

`sync.WaitGroup` 等全部跑完。這是〈[goroutine 與 channel](/posts/go-goroutines-and-channels/)〉的並行加〈[記憶體與併發原語](/posts/go-memory-and-sync-primitives/)〉的同步原語，落在維運面的用法。

## Tracer 介面 + OTel adapter

這節是 interface 那篇講的介面驅動設計的活教材。框架只依賴一個小介面：

```go
type Tracer interface {
    StartSpan(ctx context.Context, operation string) (context.Context, *Span)
    FinishSpan(span *Span)
    AddTags(span *Span, tags map[string]string)
    SetStatus(span *Span, status SpanStatus)
}
type EnhancedTracer interface { // 介面嵌入
    Tracer
    StartEnhancedSpan(ctx context.Context, operation string) (context.Context, *EnhancedSpan)
}
```

附三種實作：`NoOpTracer`（關掉 tracing）、`SimpleTracer`（in-memory，開發用）、`OTelTracerAdapter`（接 OpenTelemetry）。要從 in-memory 換到 Jaeger / OTel，只是換一個實作，業務碼一行不改，這就是 interface 那篇講的 adapter。adapter 內部用一張 `severityMap`，把 gortex 的 8 級嚴重性（`DEBUG=10` … `EMERGENCY=80`）對映到 OTel 的屬性。

## Context propagation

span 怎麼跟著請求一路傳下去？用 `context`，而且用私有的 `contextKey` 型別避免 key 撞車：

```go
type contextKey string
const spanContextKey contextKey = "span"

func ContextWithSpan(ctx context.Context, span *Span) context.Context {
    return context.WithValue(ctx, spanContextKey, span)
}
func SpanFromContext(ctx context.Context) *Span { /* ctx.Value(spanContextKey) */ }
```

`StartSpan` 先用 `SpanFromContext` 取出父 span、建子 span、再把子 span 放回 ctx 往下傳。整條 call chain 就靠 context 串起來，不用全域變數、也不用手動到處傳。


## 補充筆記

- 三態健康（Healthy / Degraded / Unhealthy）比 up/down 貼近 K8s probe；聚合 worst-wins。
- check 背景定時跑 + 快取；執行時 `WaitGroup` 並行、各自 timeout，慢的不拖垮快的。
- tracing 只依賴 `Tracer` / `EnhancedTracer` 介面（介面嵌入）；NoOp / Simple / OTel 三種實作可抽換，正是 interface 那篇的 adapter。
- span 用私有 `contextKey` + `context.WithValue` 沿 call chain 傳遞；8 級 severity 對映 OTel。
- 可觀測性建在介面上，換後端不動業務碼。

原始碼：[yshengliao/gortex](https://github.com/yshengliao/gortex)。

---

> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 環境 Go 1.25（gortex go.mod）· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃，新漆未乾。