Day 11,我們盤點了應用程式安裝的套件與已知弱點。
但產品進入正式環境後,新的問題才剛開始。外部服務可能變慢,某個 Endpoint 可能持續回傳 500,使用者也可能在團隊發現以前就已經無法結帳。
如果系統只在 Terminal 印出一句:
Something went wrong
工程師不知道問題影響多少人,也不知道錯誤從哪個服務開始。
今天會建立一個本機 Checkout API,並同時產生三種可觀測性訊號:
我們還會模擬付款服務逾時,觀察三種訊號各自回答什麼問題。
Monitoring 會收集與顯示已知指標。例如:
Observability 則關心我們能否從系統輸出的訊號,理解內部發生什麼事。
當 Checkout 錯誤率升高時,我們還要繼續回答:
Monitoring 告訴我們「發生問題」。Observability 協助我們追查「問題為什麼發生」。
兩者不是互斥選項。團隊需要先監控使用者可見的症狀,再使用 Logs 與 Traces 找出原因。
| 訊號 | 適合回答 | 不適合單獨回答 |
|---|---|---|
| Logs | 某次請求發生什麼事件?錯誤代碼是什麼? | 整體錯誤率是否持續升高? |
| Metrics | 流量、錯誤率與延遲如何變化? | 某次失敗經過哪些服務? |
| Traces | 單次請求在哪個步驟耗時或失敗? | 一個月內總共有多少次錯誤? |
只保存 Logs,團隊可能要掃描大量文字才能知道錯誤是否普遍。
只保存 Metrics,團隊會知道錯誤率上升,卻不知道是哪個依賴造成。
只保存 Traces,成本與資料量可能快速增加,也不適合計算所有長期趨勢。
三種訊號透過相同的 Route、Trace ID、錯誤類型與時間範圍互相連結,才能縮短排查時間。
今天的程式放在:
demo-app/observability-demo/scenario.js
腳本會啟動一個本機 HTTP Server,並送出四個請求:
進入 Demo:
cd /media/mickey/777/ithome/demo-app
執行:
npm run observability:demo
這個實驗不會連接真正的付款服務。程式只用延遲模擬成功與 Timeout。
每個 HTTP Request 完成後,Demo 會寫入一筆 JSON:
{
severity,
event: "http.request.completed",
requestId,
traceId,
route,
status,
durationMs,
errorType
}
成功的 Checkout Log 類似:
{
"severity": "INFO",
"event": "http.request.completed",
"requestId": "checkout-success-1",
"traceId": "<trace-id>",
"route": "/checkout",
"status": 200,
"durationMs": 9
}
付款逾時則產生:
{
"severity": "ERROR",
"event": "http.request.completed",
"requestId": "checkout-timeout-1",
"traceId": "<trace-id>",
"route": "/checkout",
"status": 503,
"durationMs": 37,
"errorType": "DEPENDENCY_TIMEOUT"
}
實際時間會依電腦負載改變。重要的不是固定數字,而是欄位與單位保持一致。
以下文字 Log 適合人眼閱讀:
Checkout failed because payment timed out after 37 ms
但查詢工具很難可靠拆出 Route、Status 與 Duration。
JSON 把每項資料放在固定欄位:
{
"route": "/checkout",
"status": 503,
"durationMs": 37,
"errorType": "DEPENDENCY_TIMEOUT"
}
Cloud Logging 把 JSON Payload 視為 Structured Log。團隊可以查詢指定欄位,也能建立索引與統計。
欄位命名必須穩定。今天叫 durationMs,明天不能隨意改成 elapsed,否則既有查詢與 Dashboard 可能失效。
一筆 Request Log 通常需要:
Log 不應直接記錄:
Day 8 已經證明,完整記錄 Request 會讓 Log 變成另一個資料外洩面。
Request ID 用來識別進入目前服務的一次請求。客服可以把 Request ID 提供給工程師,工程師再查詢對應 Log。
Trace ID 用來連結同一條跨服務流程中的所有 Span。
例如:
Browser
└── Checkout API
├── Inventory Service
└── Payment Service
三個服務應傳遞同一個 Trace ID。每個服務再建立自己的 Span ID。
如果系統只有單一服務,Request ID 與 Trace ID 看起來可能很相似。但服務拆分後,Trace ID 才能重建完整路徑。
Logs 記錄四次請求的個別結果。Metrics 則把結果聚合。
這次執行產生:
http_requests_total{route="/health",status="200"} 1
http_requests_total{route="/checkout",status="200"} 2
http_requests_total{route="/checkout",status="503"} 1
Checkout 共收到三次請求,其中一次失敗:
Checkout error rate = 1 / 3 = 33.3%
這個比例只用來說明計算方式。四次測試請求的樣本太小,不能代表正式環境表現。
程式也計算各 Route 的 Duration:
http_request_duration_ms{route="/health"} count=1 avg=2 max=2
http_request_duration_ms{route="/checkout"} count=3 avg=18 max=37
實際系統通常使用 Histogram 計算 P50、P95 與 P99,而不是只看平均值。
平均值會隱藏少數很慢的請求。例如九次請求需要 100 毫秒,一次需要 5 秒,平均值約為 590 毫秒。多數人很快,但仍有使用者等待 5 秒。
Google SRE 建議優先觀察四種訊號:
今天的 Demo 已涵蓋前三項:
http_request_duration_ms 表示 Latency。http_requests_total 表示 Traffic。dependency_failures_total 表示 Errors。Demo 尚未模擬 Saturation。正式服務還要觀察 CPU、Memory、Connection Pool、Queue Length 與 API Quota 等資源。
以下 Label 適合聚合:
route="/checkout"
status="503"
dependency="payment"
error_type="timeout"
以下值不適合直接成為 Metric Label:
user_id="每位使用者都不同"
request_id="每次請求都不同"
email="個資且幾乎每人不同"
full_url="/projects/每筆-ID"
這些值會產生大量 Time Series,稱為 High Cardinality。監控成本、記憶體與查詢速度都可能受到影響。
需要查詢單次 Request 時,應使用 Log 或 Trace,不要把 Request ID 放進 Metric Label。
OpenTelemetry 將一次請求經過的路徑稱為 Trace。Trace 由多個 Span 組成。
這次失敗的 Checkout 產生兩個 Span:
{
"traceId": "<same-trace-id>",
"spanId": "<payment-span-id>",
"parentSpanId": "<http-span-id>",
"name": "payment.authorize",
"status": "ERROR",
"errorType": "DEPENDENCY_TIMEOUT",
"durationMs": 36
}
{
"traceId": "<same-trace-id>",
"spanId": "<http-span-id>",
"parentSpanId": null,
"name": "HTTP GET /checkout",
"status": "ERROR",
"errorType": "DEPENDENCY_TIMEOUT",
"durationMs": 37
}
兩個 Span 使用相同 Trace ID。payment.authorize 的 Parent Span 是 HTTP Checkout。
從這份 Trace 可以看出:
Metrics 先告訴我們 Checkout 出現 503。Trace 再把原因縮小到付款依賴。
假設付款服務 Timeout,Checkout 開始回傳 503。
可以建立兩種告警:
Google SRE 建議優先針對使用者可見症狀通知值班人員。原因可能改變,症狀則直接代表使用者受到影響。
付款 Timeout 可以幫助排查,但不一定每次都需要叫醒工程師。如果系統已經成功 Retry,使用者沒有看到錯誤,就不應把每次短暫 Timeout 都升級成重大告警。
一個有效告警要回答:
「CPU 看起來有點高」通常不是完整告警。
Agent 不能只搜尋程式中是否出現 console.log 或 Monitoring SDK。
它應檢查:
一項可操作的 Finding 可以寫成:
問題:Checkout 失敗沒有可聚合的錯誤分類
證據:所有例外只輸出自由文字 "Something went wrong"
影響:團隊無法計算各錯誤原因的發生率,也無法建立穩定告警
修正:新增 errorType 與 route 欄位,並記錄 HTTP Status
驗證:模擬 Payment Timeout,確認 Metric 增加且 Log 可用 Trace ID 關聯
Agent 也要區分「缺少證據」與「確認沒有監控」。
Repository 沒有監控設定,不代表正式環境一定沒有監控。設定可能存在另一個 Infrastructure Repository 或雲端平台。Agent 應要求補充 Dashboard、Alert Policy 或部署設定,不能直接宣稱團隊毫無監控。
Logs、Metrics 與 Traces 不是三套互相取代的工具。
Metrics 告訴我們問題有多大。Logs 保存單次事件的上下文。Traces 顯示一次請求經過哪些工作,以及時間花在哪裡。
今天的付款逾時同時留下三種證據:
DEPENDENCY_TIMEOUT。payment.authorize Span 佔用大部分時間並失敗。明天,我們會處理 Timeout 與 Retry。如果錯誤處理方式不正確,一個短暫的外部故障可能被放大成整個系統的事故。
iThome鐵人賽