Community程式設計與開發github.com

addyosmani/observability-and-instrumentation

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

observability-and-instrumentation 是什麼?

這個技能(Observability and Instrumentation)的核心原則是:無法觀測的程式碼就是無法營運的程式碼。它指導開發者在建立功能的同時,撰寫結構化日誌、指標和分散式追蹤,而不是在功能上線後才補上。技能強調「先定義問題再儀器化」的流程,要求開發者先寫下值班工程師會問的 2-4 個問題,然後為每個問題選擇正確的信號(日誌、指標或追蹤)。它也涵蓋了 RED(速率、錯誤、持續時間)和 USE(使用率、飽和度、錯誤)指標模型、使用 OpenTelemetry 進行自動儀器化、以症狀為基礎(而非原因)的警報規則、以及必要的 runbook 撰寫方式。這個技能適用於 Claude Code、Cursor 等 AI 代理平台,支援 TypeScript/Node.js 生態系統,並提供從儀器化到驗證的完整流程。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/addyosmani/agent-skills/tree/main/skills/observability-and-instrumentation

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

可觀測性與儀器化

概述

無法觀測的程式碼就是無法營運的程式碼。可觀測性是從外部,利用程式碼發出的遙測資料,來回答「系統在做什麼以及為什麼」的能力。儀器化不是上線後的附加功能,它應該與功能本身一起被撰寫,就像測試一樣。如果一個功能上線時沒有遙測,那麼第一個使用者回報的錯誤就會變成一場考古,而非一次查詢。

使用時機

  • 建構任何將會在生產環境中運行的功能
  • 加入新的服務、端點、背景任務或外部整合
  • 生產事故花了太長時間才診斷出來(「我們無法判斷發生了什麼事」)
  • 設定或檢視警報規則
  • 審查一個涉及 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,並將其附加到每一行日誌、每個跨度(span)以及每個對外呼叫中。沒有它,你無法從交錯的日誌中重建單一請求:

// 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();
});

當多個進入點寫入同一個日誌時,命名該進入點。 關聯 ID 識別一次執行;它沒有說明是哪個程式碼路徑啟動了它。由排程器、重播端點和手動 CLI 執行到達的同一任務,會在同一個日誌接收器中產生無法區分的行。因此,歸因一行日誌只能靠排除法——交叉比對排程器的歷史記錄、行程表、部署日誌——而這個論點只在這些外部記錄仍然存在時才成立。在執行開始處,在關聯 ID 旁邊標註進入點,並以相同的方式傳播兩者:

// 每個進入點一個輔助函式:執行本身的日誌記錄器攜帶兩個欄位。
// 使用 `entryPoint`,而不是 `source`——ECS 保留 `source.*` 給網路欄位。
const runLog = (entryPoint: 'scheduler' | 'replay_endpoint' | 'cli', runId: string) =>
  logger.child({ entryPoint, requestId: runId });

// 排程器 tick        -> runLog('scheduler', crypto.randomUUID())
// POST /jobs/:id/replay -> runLog('replay_endpoint', req.id)
// CLI 呼叫            -> runLog('cli', process.env.RUN_ID ?? crypto.randomUUID())

這兩個欄位都必須跨越與關聯 ID 相同的邊界——佇列中繼資料、HTTP 標頭——否則工作人員會重新推斷進入點並進行猜測。一個僅與進入點相關聯的欄位只是一個提示,而不是歸因:任何可以呼叫該任務的事物都可以重現它。

絕不在日誌中記錄祕密、令牌、密碼或完整的個人識別資訊(PII)。 這是來自 security-and-hardening 技能的嚴格規則——遙測管線是經典的資料洩漏途徑。請使用允許清單(allowlist)來記錄欄位;不要記錄整個請求主體。

4. 指標

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

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

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],
});

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

可作為標籤:    route="/api/tasks/:id"   status_class="5xx"   provider="stripe"
絕不能作為標籤:  user_id, email, request_id, full URL, error message text

永遠追蹤百分位數,而不是平均值:平均值會隱藏那 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. 警報

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

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

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

為你建立的每個警報制定規則:

  1. 它必須是可操作的。 如果應對方式是「忽略它,它會自動修復」,請刪除該警報。
  2. 它必須連結到一份 runbook——即使只有三行:它的含義、要執行的第一個查詢、升級管道。
  3. 它必須有一個由 SLO 或歷史數據證明的閾值和持續時間,而不是猜測。
  4. 只使用兩個嚴重性等級:傳呼(影響使用者,立即行動)和工單(功能降級,本週內行動)。第三個層級會變成雜訊,讓人們學會忽略所有東西。

撰寫 Runbook

上述規則 2 要求每個警報都必須連結到一份 runbook。Runbook 的任務是回答三個問題,而不需要讀者思考:發生了什麼事、首先要檢查什麼、以及如果無法解決該找誰。儲存在 docs/runbooks/ 中,以警報名稱命名。

最小可行 runbook(三行):

# Runbook:/api/tasks 高錯誤率
**含義:** 資料庫連線池可能已耗盡,或是一次不好的部署。
**首先檢查:** `SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';`
  — 如果計數超過池限制,請參閱步驟 2。(請根據你的資料庫替換等效指令。)
**升級管道:** #db-oncall 或工程值班輪值。

何時需要擴展到三行以上: 僅在單靠第一個檢查不足以做出決定時才加入步驟。一份涵蓋最常見三種原因的、有五個步驟的 runbook,勝過一份涵蓋所有邊緣情況、有二十個步驟、但會被略讀的文件。

保持 runbook 時效性。 在關閉每個使用過它的 incident 時,將更新 runbook 作為結案的一部份——過時的 runbook 會建立虛假信心。如果某個步驟是錯誤的或遺漏的,請在將 incident 標記為已解決之前修正它。

7. 驗證遙測本身

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

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

常見的合理化藉口

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

危險信號

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

驗證

在對一個功能進行儀器化之後,請確認:

  • 該功能的值班問題已被寫下,並且每個信號都對應到其中一個問題
  • 所有日誌輸出都是結構化的(JSON),每行都帶有穩定的事件名稱和關聯 ID
  • 每個由一個以上進入點寫入的日誌接收器,都帶有一個進入點欄位,該欄位在執行開始處設定,並與關聯 ID 一起傳播,而不是在下游推斷
  • 任何日誌行中都沒有祕密、令牌或未經脫敏處理的 PII(抽檢實際輸出)
  • 每個新端點和每個外部依賴都存在 RED 指標,且標籤集合是有限的
  • 延遲是以直方圖呈現;p95/p99 是可查詢的
  • 可以在追蹤 UI 中端到端地追蹤一個請求,沒有中斷的跨度
  • 每個新警報都是基於症狀的、有 runbook 連結,並且已經過一次測試觸發
  • 透過遙測資料(而非閱讀原始碼)在暫存環境中定位了一個誘發的故障

有關此清單的快速參考版本,包括上線前的儀器化門檻,請參閱 ../../references/observability-checklist.md。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

addyosmani/agent-skills

Builds production-quality, accessible, responsive user-facing UIs. Use when building or modifying interfaces and pages, creating components, implementing layouts, meeting WCAG accessibility requirements, managing state, or when the output needs to look and feel production-quality rather than AI-generated.

addyosmani/agent-skills

Prepares production launches. Use when preparing to deploy to production, or when asking what needs to be in place before shipping. Use when you need a pre-launch checklist, when setting up monitoring, when planning a staged rollout, or when you need a rollback strategy.

addyosmani/agent-skills

Guides stable API and interface design. Use when designing APIs, module boundaries, or any public interface. Use when creating REST or GraphQL endpoints, defining type contracts between modules, or establishing boundaries between frontend and backend.

addyosmani/browser-testing-with-devtools

Tests in real browsers via Chrome DevTools MCP. Use when building or debugging anything that runs in a browser. Use when you need to inspect the DOM, capture console errors, analyze network requests, profile performance, or verify visual output with real runtime data. Requires the chrome-devtools MCP server to be configured.

addyosmani/ci-cd-and-automation

Automates CI/CD pipeline setup. Use when setting up or modifying build and deployment pipelines. Use when you need to automate quality gates, configure test runners in CI, or establish deployment strategies.

addyosmani/code-review-and-quality

Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.

addyosmani/code-simplification

Simplifies code for clarity. Use when refactoring code for clarity without changing behavior. Use when code works but is harder to read, maintain, or extend than it should be. Use when reviewing code that has accumulated unnecessary complexity.

addyosmani/constraint-driven-development

Establishes a project's quality bar as a written contract and stops agents quietly lowering it. Interviews the user on which dimensions matter, supplies sane default thresholds when they have no number in mind, records everything in CONSTRAINTS.md, and watches the diff for a weakened bar — new @ts-ignore or eslint-disable suppressions, skipped or deleted tests, assertions stripped out, unimplemented stubs, thresholds edited down. Use when no quality bar is written down, when the user says "set up constraints" or "define our standards", when the user wants dimensions they care about — accessibility, web performance, coverage — set up as enforced constraints, when an agent keeps silencing checks or skipping tests to get to green, when you need a coverage or performance threshold and don't know what number to pick, or when an agent writes more code than anyone will read.

addyosmani/debugging-and-error-recovery

Guides systematic root-cause debugging. Use when tests fail, builds break, behavior doesn't match expectations, or you encounter any unexpected error. Use when you need a systematic approach to finding and fixing the root cause rather than guessing.

addyosmani/deprecation-and-migration

Manages deprecation and migration. Use when removing old systems, APIs, or features. Use when migrating users from one implementation to another. Use when deciding whether to maintain or sunset existing code.

addyosmani/documentation-and-adrs

Records decisions and documentation. Use when making architectural decisions, changing public APIs, shipping features, or when you need to record context that future engineers and agents will need to understand the codebase.

addyosmani/doubt-driven-development

Subjects every non-trivial decision to a fresh-context adversarial review before it stands. Use when correctness matters more than speed, when working in unfamiliar code, when stakes are high (production, security-sensitive logic, irreversible operations), or any time a confident output would be cheaper to verify now than to debug later.

addyosmani/git-workflow-and-versioning

Structures git workflow practices. Use when making any code change. Use when committing, branching, resolving conflicts, or when you need to organize work across multiple parallel streams.

addyosmani/idea-refine

Refines raw ideas into sharp, actionable concepts through structured divergent and convergent thinking. Use when an idea is still vague, when you need to stress-test assumptions before committing to a plan, or when you want to expand options before converging on one. Triggers on "ideate", "refine this idea", or "stress-test my plan".

addyosmani/incremental-implementation

Delivers changes incrementally. Use when implementing any feature or change that touches more than one file. Use when you're about to write a large amount of code at once, or when a task feels too big to land in one step.

addyosmani/interview-me

Extracts what the user actually wants instead of what they think they should want. Achieves this through one-question-at-a-time interview until ~95% confidence about the underlying intent. Use when an ask is underspecified ("build me X" without "for whom" or "why now"), when the user explicitly invokes ("interview me", "grill me", "are we sure?", "stress-test my thinking"), or when you catch yourself silently filling in ambiguous requirements before any plan, spec, or code exists.

addyosmani/performance-optimization

Optimizes application performance. Use when performance requirements exist, when you suspect performance regressions, or when Core Web Vitals or load times need improvement. Use when profiling reveals bottlenecks that need fixing.

addyosmani/planning-and-task-breakdown

Breaks work into ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable tasks. Use when a task feels too large to start, when you need to estimate scope, or when parallel work is possible.

addyosmani/security-and-hardening

Hardens code against vulnerabilities. Use when handling user input, authentication, data storage, or external integrations. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services.

addyosmani/source-driven-development

Grounds every implementation decision in official documentation. Use when you want authoritative, source-cited code free from outdated patterns. Use when building with any framework or library where correctness matters.

相關技能