关于 observability-and-instrumentation
Observability and Instrumentation 是一项软件开发技能,指导团队在生产功能开发时同步规划遥测。内容涵盖结构化日志、指标、分布式追踪、可执行告警和验证,并强调回答运维问题及控制敏感数据与指标基数。
主要功能
- 01将遥测信号对应到值班问题
- 02指导结构化日志与关联 ID
- 03说明 RED 和 USE 指标实践
- 04涵盖 OpenTelemetry 分布式追踪
- 05制定可执行告警与运行手册要求
- 06通过测试场景验证遥测
适用场景
- 01为生产功能添加可观测性
- 02为新服务或外部集成添加监测
- 03在代码审查中检查遥测与告警
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ afonsoft/ skills --skill observability-and-instrumentation安装后在 Claude Code 中输入/observability-and-instrumentation确认已加载。
附带文件references/observability-checklist.md
详细介绍
可观测性与仪表化
概述
无法观测的代码就无法运维。可观测性是指通过代码发出的遥测数据,从外部回答“系统正在做什么,为什么”的能力。仪表化不是发布后的附加项,而是与功能一同编写,就像测试一样。如果功能发布时没有遥测数据,用户报告的第一个 Bug 就会变成考古,而不是查询。
适用场景
- 构建任何将在生产环境运行的功能
- 添加新的服务、端点、后台任务或外部集成
- 生产事故诊断耗时过长(“我们无法判断发生了什么”)
- 设置或审核告警规则
- 审核添加了 I/O、重试、队列或跨服务调用的 PR
不适用于:
-
诊断当前正在发生的故障——请使用调试技能(可观测性让下次调试更快)
-
分析并优化已测得的性能瓶颈——请使用性能优化技能
-
发布日监控清单和回滚触发条件——请使用发布/上线技能;本技能涵盖为它们提供数据的仪表化
-
用户以英语请求或提及此技能(例如,“use /observability-and-instrumentation”、“run observability-and-instrumentation”)。
-
用户以葡萄牙语请求或提及此技能(例如,“use /observability-and-instrumentation”、“execute observability-and-instrumentation”)。
流程
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,并将其附加到每条日志、每个跨度和每次出站调用中。没有它,就无法从交错的日志中还原单个请求:
// 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();
});
切勿记录密钥、令牌、密码或完整的个人身份信息(PII)。 遥测管道是常见的数据泄露途径。请使用字段允许列表;不要记录整个请求正文。完整的安全检查清单请参见 ../code-review-and-quality/references/security-checklist.md。
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();
只在有意义的内部工作单元(例如 applyDiscounts、chargeProvider)周围添加手动 Span,并附上值班人员会用来筛选的属性。在每个异步边界传递上下文——例如 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%
基于原因的告警会在一切正常时触发,也会遗漏你未预料到的故障。基于症状的告警则会在用户确实受到影响时触发,无论原因是什么。
每条告警都必须符合以下规则:
- **必须可采取行动。**如果响应是“忽略它,它会自行恢复”,就删除这条告警。
- 链接到运行手册——即使只有三行也可以:说明告警含义、要运行的第一个查询、升级处理路径。
- 阈值和持续时间必须有依据,依据应来自 SLO 或历史数据,而不是猜测。
- 只使用两种严重级别:页面告警(面向用户,立即处理)和工单(服务降级,本周内处理)。第三个级别只会带来噪声,让人习惯于忽略所有告警。
7. 验证遥测本身
插桩也是代码,也可能出错。在宣布工作完成之前,触发相关路径并检查实际输出:
- 在预发布环境中强制触发错误 → 按
requestId在日志中找到该错误,并确认字段是结构化的(而不是[object Object]) - 发送测试流量 → 确认指标序列以预期标签出现,且数值合理
- 在追踪界面中跟踪一个跨服务的请求 → 确认没有中断的 Span
- 逐一触发每条新告警(暂时调低阈值)→ 确认告警发送到了正确的渠道,且运行手册链接有效
常见借口
| 借口 | 现实 |
|---|---|
| “等功能正常运行后再添加日志” | “之后”往往会变成“第一次发生事故之后”,而那是发现自己毫无观测能力时成本最高的时刻。边构建边添加监测。 |
| “日志越多,越容易观测” | 无结构的噪声会让事故处理变慢,而不是更快。三个可查询事件胜过三百行描述性日志。 |
“暂时用 console.log 就行” | 无结构的输出无法筛选、关联或设置告警。使用结构化日志记录器只需额外花五分钟。 |
| “出问题时看仪表板就行” | 如果没有明确的问题作为设计依据,仪表板展示的内容会包罗万象,唯独没有答案。从值班问题入手。 |
| “所有重要事项都设置告警,以后再调” | 嘈杂的告警会让人们学会忽略它。调整永远不会发生,真正的告警却会被漏掉。 |
| “用用户 ID 作为指标标签更方便调试” | 这也会让你的指标后端不堪重负。高基数查询应放在日志和追踪中。 |
| “我们只有两个服务,用追踪太小题大做了” | 两个服务就已经会产生日志无法解答的跨服务延迟问题。自动埋点会让成本微不足道。 |
危险信号
- 一个包含重试、队列或外部调用的功能 PR,却没有新增任何遥测数据
- 使用字符串插值构建日志行,而不是使用结构化字段
- 没有关联 ID/请求 ID——每条日志行都是孤立的
- 指标使用用户 ID、原始 URL 或错误消息文本作为标签(基数炸弹)
- 延迟只追踪平均值,没有百分位数
- 告警每天都会触发,人们确认后却不采取行动
- 对 CPU、内存等原因设置告警并通知人员,却没有监控面向用户的错误率
- 日志中出现密钥、令牌或完整请求正文
- 生产功能是否正常的唯一依据是“在我的机器上能运行”
验证
为功能添加埋点后,请确认:
- 已记录该功能的值班问题,并且每个问题都对应一项信号
- 所有日志输出均为结构化格式(JSON),具有稳定的事件名称,且每行都包含关联 ID
- 任何日志行中都没有密钥、令牌或未脱敏的 PII(抽查实际输出)
- 每个新增端点和每个外部依赖都有 RED 指标,且标签集合有界
- 延迟使用直方图;可以查询 p95/p99
- 在追踪 UI 中可以端到端跟踪单个请求,且没有断裂的 span
- 每个新告警都基于症状,带有运行手册链接,并且至少测试触发过一次
- 在预发布环境中模拟的故障仅通过遥测数据就能定位,无需阅读源代码
如需快速浏览此列表,包括发布前的埋点检查关卡,请参阅 references/observability-checklist.md。