mcpField
O
observability-and-instrumentation

observability-and-instrumentation

github.com/addyosmani云与 DevOps111.5k56.6k 次安装2 周前更新
代码仓库

关于 observability-and-instrumentation

面向开发团队的生产可观测性指导技能,帮助在构建功能时设计日志、指标、追踪和告警,并验证遥测数据。重点是从值班问题出发选择信号、控制指标基数并维护可操作的告警。

主要功能

  1. 01按值班问题定义遥测目标
  2. 02结构化日志与关联 ID
  3. 03RED、USE 指标设计
  4. 04OpenTelemetry 分布式追踪
  5. 05症状导向告警与运行手册
  6. 06验证遥测数据与告警

适用场景

  1. 01为新服务或后台任务添加监测
  2. 02改进生产故障诊断能力
  3. 03审查含重试或跨服务调用的改动

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/addyosmani/agent-skills --skill observability-and-instrumentation

安装后在 Claude Code 中输入/observability-and-instrumentation确认已加载。

详细介绍

可观测性与埋点

概述

无法观测的代码,就无法运维。可观测性是指利用代码发出的遥测数据,从外部回答“系统正在做什么,为什么?”的能力。埋点不是上线后的附加工作,而是与功能一同编写,就像测试一样。如果功能上线时没有遥测数据,第一个由用户报告的 Bug 就会变成考古,而不是查询。

适用场景

  • 构建任何将在生产环境运行的功能
  • 新增服务、端点、后台任务或外部集成
  • 生产事故花了太久才诊断出来(“我们无法判断发生了什么”)
  • 设置或审查告警规则
  • 审查涉及 I/O、重试、队列或跨服务调用的 PR

不适用于:

  • 诊断当前正在发生的故障——使用 debugging-and-error-recovery 技能(可观测性会让下次使用该技能时更快)
  • 分析并优化已测得的性能问题——使用 performance-optimization 技能
  • 发布日监控清单和回滚触发条件——参阅 shipping-and-launch 技能;本技能涵盖为这些工作提供数据的埋点

流程

1. 埋点前先定义“正常运行”的标准

没有问题要回答的遥测数据只是噪声。添加任何埋点之前,先写下值班工程师会针对此功能提出的 2–4 个问题:

FEATURE: checkout payment retry
QUESTIONS ON-CALL WILL ASK:
1. What fraction of payments succeed on first attempt vs after retry?
2. When a payment fails permanently, why? (provider error? timeout? validation?)
3. Is the payment provider slower than usual?
→ Every signal below must help answer one of these.

如果你说不出这些问题,就还没准备好埋点——否则只会记录一切,却一无所获。

2. 为每个问题选择合适的信号
信号可回答的问题成本特征示例
结构化日志“这个特定案例中发生了什么?”按事件计费;随流量增长带有提供商错误代码的 payment_failed
指标“总体而言,发生频率是多少/速度有多快?”每个序列的成本固定;查询成本低提供商调用的 p99 延迟
追踪“时间都花在了哪些服务上?”按请求计费;通常会采样一次缓慢的结账操作,按调用节点拆分

经验法则:指标告诉你是否出了问题,追踪告诉你哪里出了问题,日志告诉你为什么出了问题。

3. 结构化日志

记录事件,而不是叙述文字。每条日志行都是一个 JSON 对象,包含稳定的事件名称和机器可读字段:

// BAD: string interpolation — unqueryable, inconsistent
logger.info(`Payment ${id} failed for user ${userId} after ${n} retries`);

// GOOD: stable event name + structured fields
logger.warn({
  event: 'payment_failed',
  paymentId: id,
  provider: 'stripe',
  errorCode: err.code,
  attempt: n,
}, 'payment failed');

日志级别——保持使用一致:

级别含义值班处理
error不变量遭到破坏;可能需要有人采取行动调查
warn状态降级但已处理(重试成功、使用了回退方案)留意趋势
info重要的业务事件(下单、任务完成)无
debug诊断细节默认在生产环境中关闭

关联 ID 是强制要求。 在系统边界生成(或接收)请求 ID,并将其附加到每条日志行、每个 span 和每个出站调用中。没有它,你就无法从交错的日志中还原单个请求:

// Express: child logger per request, ID propagated downstream
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 旁记录入口点,并以相同方式传播两者:

// One helper for every entry point: the run's own logger carries both fields.
// `entryPoint`, not `source` — ECS reserves `source.*` for network fields.
export const runLog = (entryPoint: 'scheduler' | 'replay_endpoint' | 'cli', runId: string) =>
  logger.child({ entryPoint, requestId: runId });

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

这两个字段都必须跨越与关联 ID 相同的边界——例如队列元数据、HTTP 标头——否则工作进程会重新推导入口点并猜测。仅与某个入口点相关联的字段只是提示,而非归属依据:任何能够调用该任务的对象都能复现它。

绝不记录机密、令牌、密码或完整的个人身份信息(PII)。 这是 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', not '200'
  buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});

**基数是故障根源。**每种唯一的标签组合都是一个独立的时间序列。标签必须来自范围较小且固定的集合(路由模板、状态类别、提供商名称)。切勿使用用户 ID、原始 URL、错误消息或其他无界值作为标签——这些内容应放在日志和追踪中。

OK as label:    route="/api/tasks/:id"   status_class="5xx"   provider="stripe"
NEVER a label:  user_id, email, request_id, full URL, error message text

永远不要追踪平均值,始终追踪百分位数:平均值会掩盖那 1% 体验糟糕的用户。使用直方图并查看 p50/p95/p99。

5. 分布式追踪

使用 OpenTelemetry——它是与厂商无关的标准,并且自动插桩几乎无需编写代码,就能覆盖 HTTP、gRPC 和常见的数据库客户端:

// tracing.ts — must be imported before anything else
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

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

仅在有意义的内部工作单元周围添加手动 span(例如,applyDiscounts、chargeProvider),并附加值班人员会用来筛选的属性。在每个异步边界之间传播上下文——HTTP 标头、队列消息元数据——否则追踪会在断点处中断。默认使用较低采样率进行基于头部的采样;如果后端支持基于尾部的采样,则保留 100% 的错误。

6. 告警

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

SYMPTOM (page-worthy):           CAUSE (dashboard, not a page):
error rate > 1% for 5 min        CPU at 85%
p99 latency > 2s                 one pod restarted
queue age > 10 min               disk at 70%

基于原因的告警会在没有问题时触发,也会漏掉你未曾预料到的故障。基于症状的告警会在用户恰好受到影响时触发,无论原因是什么。

创建的每条告警都必须遵守以下规则:

  1. **必须可执行。**如果响应是“忽略它,它会自行恢复”,就删除这条告警。
  2. 链接到运行手册——哪怕只有三行:告警含义、要运行的第一个查询、升级处理路径。
  3. 设定阈值和持续时间,并以 SLO 或历史数据为依据,而不是凭猜测。
  4. 只使用两个严重级别:page(面向用户的问题,立即处理)和 ticket(服务降级,本周内处理)。第三个级别会沦为噪声,让人们习惯于忽略所有告警。
编写运行手册

上面的规则 2 要求每条告警都链接到运行手册。运行手册的作用是让读者无需思考,就能找到三个问题的答案:发生了什么、首先要检查什么,以及如果问题仍未解决该联系谁。将文件存放在 docs/runbooks/ 中,并以告警名称命名。

最小可用运行手册(三行):

# Runbook: High Error Rate on /api/tasks
**Means:** DB connection pool likely exhausted, or a bad deploy.
**First check:** `SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';`
  — if count > pool limit, see Step 2. (Swap in the equivalent for your database.)
**Escalate to:** #db-oncall or engineering on-call rotation.

**何时扩展到三行以上:**仅当第一项检查不足以作出判断时,才添加步骤。涵盖三个最常见原因的五步运行手册,胜过涵盖所有边缘情况、却让人只能草草浏览的二十步文档。

**保持运行手册及时更新。**每次处理完使用过该运行手册的事故后,都要更新它——过时的运行手册会造成虚假的信心。如果某个步骤有误或有所遗漏,应先修正,再将事故标记为已解决。

7. 验证遥测本身

插桩也是代码,可能出错。在宣布工作完成之前,触发相关路径并检查实际输出:

  • 在预发布环境中强制触发错误 → 按 requestId 在日志中找到该错误,并确认字段是结构化的(而不是 [object Object])
  • 发送测试流量 → 确认指标序列已出现,且标签符合预期、数值合理
  • 在追踪 UI 中跟踪一个请求经过多个服务 → 确认没有中断的 span
  • 各触发一次新告警(暂时降低阈值)→ 确认告警发送到了正确渠道,且运行手册链接有效

常见借口

借口事实
“等它能用了再加日志”“之后”往往会变成“第一次出故障之后”,而那是发现自己毫无观测能力的代价最高的时刻。构建时就添加监测。
“日志越多,越容易观测”无结构的噪声只会让故障处理变慢,而不是更快。3 条可查询的事件胜过 300 行文字。
“暂时用 console.log 就行”无结构的输出无法筛选、关联或触发告警。结构化日志记录器只需多花 5 分钟,就能一次性解决问题。
“出问题时看看仪表板就行”没有明确问题作为设计依据的仪表板,什么都能展示,就是给不出答案。先从值班问题入手。
“所有重要情况都设告警,以后再调整”告警器太吵会让人习惯性忽略。调整永远不会发生;真正的告警却会被漏掉。
“用用户 ID 作为指标标签更方便排查问题”这也会让你的指标后端不堪重负。高基数查询应该放在日志和追踪中。
“我们只有两个服务,用追踪太小题大做了”两个服务已经足以产生无法通过日志回答的跨服务延迟问题。自动监测可以让成本微乎其微。

危险信号

  • 一个包含重试、队列或外部调用的功能 PR,却没有新增任何遥测数据
  • 日志行通过字符串插值拼接,而非使用结构化字段
  • 没有关联 ID/请求 ID——每条日志都是孤立的
  • 调度器、Webhook 和手动运行产生的日志进入同一个日志流,却没有字段标明是哪一种入口生成了该日志
  • 指标标签中包含用户 ID、原始 URL 或错误消息文本(基数炸弹)
  • 只跟踪平均延迟,没有百分位数
  • 告警每天触发,却只是被确认而没有采取行动
  • 因 CPU、内存等原因触发告警并通知人工值班,但没有监测面向用户的错误率
  • 日志中出现密钥、令牌或完整请求正文
  • 证明生产环境功能正常的唯一依据是“它在我的机器上能跑”

验证

为功能添加监测后,请确认:

  • 已写下该功能的值班问题,且每个问题都对应一个信号
  • 所有日志输出均为结构化格式(JSON),具有稳定的事件名称,并且每行都带有关联 ID
  • 每个由多个入口写入的日志接收端都带有入口点字段;该字段在运行开始时设置,并与关联 ID 一起传递,而不是在下游推断
  • 任何日志行中都没有密钥、令牌或未经脱敏的 PII(抽查实际输出)
  • 每个新增端点和每个外部依赖都有 RED 指标,且标签集合有界
  • 延迟使用直方图记录;p95/p99 可查询
  • 可在追踪界面中端到端跟踪单个请求,且跨度不中断
  • 每个新告警都基于症状,附有运行手册链接,并且至少测试触发过一次
  • 在预发布环境中人为引发故障后,仅凭遥测数据即可定位,无需阅读源代码

如需快速浏览此列表(包括发布前监测检查关卡),请参阅 ../../references/observability-checklist.md。

评论

支持 Markdown