對程式碼進行儀器化,讓生產環境的行為可觀察且可診斷。在加入日誌、指標、追蹤或警示時使用。在部署任何會於生產環境中執行的功能,且需要證據證明其正常運作時使用。在回報生產問題但無法從現有資料判斷發生什麼事時使用。
可觀測性與儀器化
概述
無法觀察的程式碼就是無法維運的程式碼。可觀測性是從外部,透過程式碼發出的遙測資料,回答「系統在做什麼以及為什麼」的能力。儀器化不是上線後才追加的功能——它應該與功能同時撰寫,就像測試一樣。如果一個功能上線時沒有遙測,第一個使用者回報的錯誤就會變成考古題,而不是一個查詢就能解決的問題。
使用時機
- 建置任何會在生產環境執行的功能
- 加入新的服務、端點、背景任務或外部整合
- 某次生產事故花了太長時間才診斷出來(「我們無法判斷發生什麼事」)
- 設定或檢討警示規則
- 審查涉及 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(延遲直方圖,非平均值)。對於資源(佇列、連線池、主機),使用 USE:Utilization(使用率)、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();
僅在有意義的內部工作單元(例如 applyDiscounts、chargeProvider)周圍加入手動跨度,並附加值班人員會用來篩選的屬性。在每個非同步邊界傳遞上下文——HTTP 標頭、佇列訊息中繼資料——否則追蹤會在間隙處中斷。預設以低取樣率進行基於頭的取樣;如果你的後端支援尾部取樣,則保留 100% 的錯誤。
6. 警示
針對使用者感受到的症狀發出警示,而不是針對原因:
症狀(值得發 page): 原因(儀表板,不發 page):
錯誤率 > 1% 持續 5 分鐘 CPU 使用率 85%
p99 延遲 > 2 秒 一個 Pod 重新啟動
佇列年齡 > 10 分鐘 磁碟使用率 70%
基於原因的警示會在沒有問題時觸發,並錯過你未預測到的故障。基於症狀的警示則恰好在使用者受影響時觸發,無論原因是什麼。
你建立的每個警示都應遵守以下規則:
- 必須是可操作的。 如果回應是「忽略它,它會自我修復」,就刪除該警示。
- 它必須連結到一本 runbook——即使只有三行:它的意義、要執行的第一個查詢、升級路徑。
- 它的閾值和持續時間必須有 SLO 或歷史資料的依據,而不是猜測。
- 只使用兩種嚴重性: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。




