Community编程与开发github.com

可观测性与插桩

对代码进行插桩,使生产行为可见且可诊断。用于添加日志、指标、追踪或告警。用于发布任何在生产环境中运行并需要证据证明其正常工作的特性。也用于在生产问题报告后,现有数据无法说明问题根源时。

可观测性与插桩 是什么?

此技能为开发者提供了一套系统化的插桩指南,确保生产环境下的代码行为完全透明、可诊断。它强调可观测性不是事后补丁,而是与特性开发同步进行的基础工程实践——就像编写测试一样。技能覆盖了结构化日志、RED/USE指标体系、基于OpenTelemetry的分布式追踪以及基于症状而非原因的告警设计等核心领域,并提供了从定义问题到验证遥测数据的完整工作流。其核心价值在于,通过强制要求为每个特性定义“值班工程师会问的2-4个问题”,并让每个信号都服务于回答这些问题,避免了大量无用日志和指标噪音。技能还包含了丰富的常见误区辨析(如“用户ID作为指标标签”等)和实践检查清单,适用于Claude Code、Cursor等所有支持Markdown任务定义的主流AI编程助手,帮助开发者从一开始就构建出可运维的软件。

兼容平台~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 的新对话。

文档

可观测性与插桩 是做什么的?

概述

你无法观测的代码,就是无法运维的代码。可观测性是从外部,利用代码发射的遥测数据来回答“系统在做什么以及为什么”的能力。插桩不是上线后的附加项——它应该与特性开发同步完成,就像编写测试一样。如果一个特性在上线时没有遥测数据,那么用户报告的第一个 Bug 就会变成考古学,而不是一次数据库查询。

何时使用

  • 构建任何将在生产环境中运行的特性
  • 添加新的服务、端点、后台任务或外部集成
  • 生产事件花费了太长时间才诊断清楚——“我们不清楚发生了什么”
  • 设置或审查告警规则
  • 评审涉及 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.*` 用于网络字段。
export 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 头部——否则工作者会重新派生出入口点并猜测。一个仅仅与入口点相关联的字段是一个提示,而不是归因:任何可以调用该任务的东西都可以重现它。

永远不要记录密钥、令牌、密码或完整的个人身份信息。 这是来自 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],
});

基数(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)周围添加手动 Span,并附加值班人员会用来筛选的属性。在每一个异步边界(HTTP 头部、队列消息元数据)上传播上下文,否则追踪会在间隙处中断。默认以低速率进行基于头的头采样;如果后端支持尾部采样,则保留 100% 的错误。

6. 告警

对用户能感受到的症状进行告警,而不是原因:

症状(应触发分页):           原因(应放入仪表盘,不是分页):
错误率在 5 分钟内 > 1%         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 最新。 作为关闭每次使用 Runbook 的事件的部分,更新它——过时的 Runbook 会建立错误的信心。如果某一步是错误的或缺失的,在标记事件已解决之前修复它。

7. 验证遥测数据本身

插桩是代码;它可能出错。在宣布工作完成之前,触发这些路径并查看实际输出:

  • 在测试环境中强制产生一个错误→通过 requestId 在日志中找到它,确认字段是结构化的(而不是 [object Object])
  • 发送测试流量→确认指标序列出现,并带有预期的标签和合理的值
  • 在追踪 UI 中跟随一个请求穿越多个服务→没有断开的 Span
  • 每个新告警触发一次(暂时降低阈值)→确认它到达正确的通道并且 Runbook 链接有效

常见合理化

合理化现实
“等它正常工作后我再添加日志”“之后”变成了“第一次事件之后”,这是发现你处于盲区的最昂贵时刻。边构建边插桩。
“日志越多 = 可观测性越好”非结构化的噪音使事件处理更慢,而不是更快。三个可查询的事件胜过三百行散文。
“现在用 console.log 就行了”非结构化输出无法被过滤、关联或进行告警。结构化的日志器只多花你五分钟的一次性时间。
“出问题时我们看看仪表盘就行了”没有定义问题就构建的仪表盘会向你展示一切,唯独不展示答案。从值班人员的问题开始。
“对所有重要的东西进行告警,我们以后再调整”一个吵闹的寻呼机会训练人们忽略它。调整永远不会发生;但真正的分页反而错过了。
“把用户 ID 作为指标标签可以让调试更容易”这也会让你的指标后端崩溃。高基数的查询属于日志和追踪。
“对我们的两个服务来说,追踪是过度工程了”两个服务已经意味着跨服务延迟问题日志无法回答。自动插桩使得成本微不足道。

红旗警示

  • 一个包含重试、队列或外部调用,且没有任何新的遥测数据的功能 PR
  • 通过字符串插值而不是结构化字段构建的日志行
  • 没有关联 ID/请求 ID——每个日志行都是孤儿
  • 一个日志流同时由调度器、Webhook 和手动运行提供,但没有字段说明是哪一行产生了该行
  • 指标标签包含用户 ID、原始 URL 或错误消息文本(基数炸弹)
  • 延迟以平均值跟踪,没有百分位数
  • 每天触发且被确认但未采取行动的告警
  • 基于原因(CPU、内存)的告警向人类分页,而面向用户的错误率却未被监控
  • 密钥、令牌或完整的请求体出现在日志中
  • “在我机器上能用”作为生产特性健康的唯一证据

验证

在插桩一个特性后,确认:

  • 此特性的值班问题已写下,并且每个信号都映射到其中一个问题
  • 所有日志输出都是结构化的(JSON),具有稳定的事件名称,并且每行都有关联 ID
  • 每个由多个入口点写入的日志汇都带有一个入口点字段,该字段在运行开始时设置,并与关联 ID 一起传播,而不是在下游推断
  • 没有密钥、令牌或未经编辑的个人身份信息出现在任何日志行中(抽查实际输出)
  • 每个新端点以及每个外部依赖都存在 RED 指标,且标签集有界
  • 延迟是直方图;可查询 p95/p99
  • 可以在追踪 UI 中端到端地跟随单个请求,没有断开的 Span
  • 每个新告警都是基于症状的,有 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.

相关技能