observability-and-instrumentation

observability-and-instrumentation

熱門

對程式碼進行儀器化,讓生產環境的行為可觀察且可診斷。在加入日誌、指標、追蹤或警示時使用。在部署任何會於生產環境中執行的功能,且需要證據證明其正常運作時使用。在回報生產問題但無法從現有資料判斷發生什麼事時使用。

7.9萬星標
8436分支
更新於 2026/7/12
SKILL.md
readonlyread-only
name
observability-and-instrumentation
description

Instruments code so production behavior is visible and diagnosable. Use when adding logging, metrics, tracing, or alerting. Use when shipping any feature that runs in production and you need evidence it works. Use when production issues are reported but you can't tell what happened from the available data.

可觀測性與儀器化

概述

無法觀察的程式碼就是無法維運的程式碼。可觀測性是從外部,透過程式碼發出的遙測資料,回答「系統在做什麼以及為什麼」的能力。儀器化不是上線後才追加的功能——它應該與功能同時撰寫,就像測試一樣。如果一個功能上線時沒有遙測,第一個使用者回報的錯誤就會變成考古題,而不是一個查詢就能解決的問題。

使用時機

  • 建置任何會在生產環境執行的功能
  • 加入新的服務、端點、背景任務或外部整合
  • 某次生產事故花了太長時間才診斷出來(「我們無法判斷發生什麼事」)
  • 設定或檢討警示規則
  • 審查涉及 I/O、重試、佇列或跨服務呼叫的 PR

不適用於:

  • 診斷正在發生的故障——請使用 debugging-and-error-recovery 技能(可觀測性正是讓該技能下次變快的關鍵)
  • 針對已測量到的緩慢進行效能分析與最佳化——請使用 performance-optimization 技能
  • 上線日的監控檢查清單與回滾觸發條件——請參閱 shipping-and-launch 技能;本技能涵蓋的是提供這些檢查清單資料的儀器化

流程

1. 在儀器化之前先定義「正常運作」

沒有問題的遙測只是雜訊。在加入任何儀器化之前,寫下 2–4 個值班工程師會針對此功能提出的問題:

功能:結帳付款重試
值班人員會問的問題:
1. 第一次嘗試成功與重試後成功的付款比例為何?
2. 當付款永久失敗時,原因是什麼?(供應商錯誤?逾時?驗證失敗?)
3. 付款供應商是否比平常慢?
→ 下方的每個訊號都必須能幫助回答其中一個問題。

如果你說不出問題,就代表你還沒準備好進行儀器化——你會記錄所有東西,但什麼也學不到。

2. 為每個問題選擇正確的訊號

訊號 回答的問題 成本特性 範例
結構化日誌 「這個特定案例發生了什麼事?」 每事件;隨流量成長 payment_failed 附帶供應商錯誤碼
指標 「整體而言,發生頻率/速度為何?」 每序列固定;查詢成本低 供應商呼叫的 p99 延遲
追蹤 「時間花在跨服務的哪個環節?」 每請求;通常取樣 一個慢速結帳,按跳點分解

經驗法則:指標告訴你有問題,追蹤告訴你在哪裡,日誌告訴你為什麼

3. 結構化日誌

記錄事件,而不是散文。每行日誌都是一個 JSON 物件,帶有穩定的事件名稱和機器可讀的欄位:

// 不好:字串插值——無法查詢、不一致
logger.info(`Payment ${id} failed for user ${userId} after ${n} retries`);

// 好:穩定的事件名稱 + 結構化欄位
logger.warn({
  event: 'payment_failed',
  paymentId: id,
  provider: 'stripe',
  errorCode: err.code,
  attempt: n,
}, 'payment failed');

日誌層級——一致使用:

層級 意義 值班人員動作
error 不變量被破壞;可能有人需要處理 調查
warn 降級但已處理(重試成功、使用備援) 觀察趨勢
info 重要的業務事件(訂單成立、工作完成)
debug 診斷細節 預設在生產環境關閉

關聯 ID 是強制性的。 在系統邊界產生(或接受)一個請求 ID,並將其附加到每行日誌、每個跨度以及每個對外呼叫。沒有它,你無法從交錯的日誌中重建單一請求:

// Express:每個請求一個子日誌器,ID 向下游傳遞
app.use((req, res, next) => {
  req.id = req.headers['x-request-id'] ?? crypto.randomUUID();
  req.log = logger.child({ requestId: req.id });
  res.setHeader('x-request-id', req.id);
  next();
});

絕不記錄機密、令牌、密碼或完整的個人識別資訊。 這是來自 security-and-hardening 技能的硬性規定——遙測管道是經典的資料外洩路徑。採用允許清單方式記錄欄位;不要記錄整個請求主體。

4. 指標

對於請求驅動的服務,在每個端點和每個外部依賴上進行 RED 儀器化:Rate(請求/秒)、Errors(失敗率)、Duration(延遲直方圖,非平均值)。對於資源(佇列、連線池、主機),使用 USEUtilization(使用率)、Saturation(飽和度)、Errors(錯誤率)。

與追蹤相同,供應商中立的路徑是 OpenTelemetry 指標 API(與步驟 5 相同的 SDK 和上下文)。以下範例使用 Prometheus 的 prom-client——這是一個常見的後端選擇,但不是唯一選擇;RED/USE 和基數規則無論如何都相同。

import { Histogram } from 'prom-client';

const httpDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration',
  labelNames: ['method', 'route', 'status_class'],  // '2xx', 不是 '200'
  buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});

基數是失敗模式。 每個唯一的標籤組合都是一個獨立的時間序列。標籤必須來自小型、固定的集合(路由模板、狀態類別、供應商名稱)。絕不使用使用者 ID、原始 URL、錯誤訊息或其他無界值作為標籤——這些屬於日誌和追蹤。

可作為標籤:    route="/api/tasks/:id"   status_class="5xx"   provider="stripe"
絕不能作為標籤:  user_id, email, request_id, 完整 URL, 錯誤訊息文字

永遠不要追蹤平均值,永遠追蹤百分位數:平均值會隱藏那 1% 體驗極差的使用者。使用直方圖並讀取 p50/p95/p99。

5. 分散式追蹤

使用 OpenTelemetry——它是供應商中立的標準,且自動儀器化涵蓋 HTTP、gRPC 和常見的資料庫客戶端,幾乎不需撰寫程式碼:

// tracing.ts — 必須在任何其他匯入之前匯入
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  serviceName: 'checkout-service',
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();

僅在有意義的內部工作單元(例如 applyDiscountschargeProvider)周圍加入手動跨度,並附加值班人員會用來篩選的屬性。在每個非同步邊界傳遞上下文——HTTP 標頭、佇列訊息中繼資料——否則追蹤會在間隙處中斷。預設以低取樣率進行基於頭的取樣;如果你的後端支援尾部取樣,則保留 100% 的錯誤。

6. 警示

針對使用者感受到的症狀發出警示,而不是針對原因:

症狀(值得發 page):          原因(儀表板,不發 page):
錯誤率 > 1% 持續 5 分鐘        CPU 使用率 85%
p99 延遲 > 2 秒                一個 Pod 重新啟動
佇列年齡 > 10 分鐘             磁碟使用率 70%

基於原因的警示會在沒有問題時觸發,並錯過你未預測到的故障。基於症狀的警示則恰好在使用者受影響時觸發,無論原因是什麼。

你建立的每個警示都應遵守以下規則:

  1. 必須是可操作的。 如果回應是「忽略它,它會自我修復」,就刪除該警示。
  2. 它必須連結到一本 runbook——即使只有三行:它的意義、要執行的第一個查詢、升級路徑。
  3. 它的閾值和持續時間必須有 SLO 或歷史資料的依據,而不是猜測。
  4. 只使用兩種嚴重性:page(影響使用者,立即行動)和 ticket(降級,本週內行動)。第三層級會變成雜訊,讓人們學會忽略所有警示。

7. 驗證遙測本身

儀器化是程式碼;它可能出錯。在宣告工作完成之前,觸發路徑並查看實際輸出:

  • 在暫存環境強制產生一個錯誤 → 透過 requestId 在日誌中找到它,確認欄位是結構化的(不是 [object Object]
  • 發送測試流量 → 確認指標序列出現,並帶有預期的標籤和合理的數值
  • 在追蹤 UI 中跟隨一個請求跨服務 → 沒有中斷的跨度
  • 每個新警示至少觸發一次(暫時調低閾值)→ 確認它到達正確的頻道,且 runbook 連結有效

常見的合理化藉口

合理化藉口 現實
「等它運作正常後我再加日誌」 「之後」變成「第一次事故之後」,這是最昂貴的發現自己盲點的時刻。邊建置邊儀器化。
「更多日誌 = 更好的可觀測性」 非結構化的雜訊只會讓事故處理變慢,而不是變快。三個可查詢的事件勝過三百行散文。
「先用 console.log 就好」 非結構化的輸出無法被過濾、關聯或發出警示。結構化日誌器只多花五分鐘,一次搞定。
「出問題時看儀表板就好」 沒有定義問題就建立的儀表板,會顯示所有東西,就是不顯示答案。從值班人員的問題開始。
「所有重要的東西都設警示,之後再調整」 吵雜的呼叫器會讓人學會忽略它。調整永遠不會發生,而真正重要的 page 反而被錯過。
「把使用者 ID 當作指標標籤可以讓除錯更容易」 這也會讓你的指標後端崩潰。高基數的查詢屬於日誌和追蹤。
「對我們兩個服務來說,追蹤小題大作」 兩個服務就已經會產生跨服務延遲問題,而日誌無法回答。自動儀器化讓成本變得微不足道。

紅旗

  • 一個功能 PR 包含重試、佇列或外部呼叫,卻沒有任何新的遙測
  • 日誌行是透過字串插值而非結構化欄位建構的
  • 沒有關聯 ID/請求 ID——每行日誌都是孤兒
  • 指標標籤使用了使用者 ID、原始 URL 或錯誤訊息文字(基數炸彈)
  • 延遲追蹤使用平均值而沒有百分位數
  • 警示每天觸發,卻被確認而不採取行動
  • 基於原因(CPU、記憶體)的警示在呼叫人類,而面向使用者的錯誤率卻沒有被監控
  • 日誌中出現機密、令牌或完整的請求主體
  • 「在我的機器上可以運作」是生產功能健康的唯一證據

驗證

在對功能進行儀器化後,確認:

  • [ ] 此功能的值班問題已寫下,且每個訊號都對應到其中一個問題
  • [ ] 所有日誌輸出都是結構化的(JSON),帶有穩定的事件名稱和每行的關聯 ID
  • [ ] 沒有任何日誌行包含機密、令牌或未脫敏的個人識別資訊(抽查實際輸出)
  • [ ] 每個新端點和每個外部依賴都有 RED 指標,且標籤集合是有界的
  • [ ] 延遲是直方圖;p95/p99 可查詢
  • [ ] 單一請求可以在追蹤 UI 中端到端追蹤,沒有中斷的跨度
  • [ ] 每個新警示都是基於症狀的,有 runbook 連結,且已測試觸發過一次
  • [ ] 在暫存環境中誘發的故障僅透過遙測就被定位,無需閱讀原始碼

如需此清單的快速一覽版本,包括上線前的儀器化閘道,請參閱 references/observability-checklist.md