对代码进行插桩,使生产行为可见且可诊断。在添加日志、指标、追踪或告警时使用。在发布任何在生产环境中运行且需要证据证明其正常工作的功能时使用。当报告了生产问题但无法从现有数据判断发生了什么时使用。
可观测性与插桩
概述
无法观测的代码就是无法运维的代码。可观测性是指从外部通过代码发出的遥测数据回答“系统在做什么以及为什么”的能力。插桩不是发布后的附加功能——它应该与功能同时编写,就像测试一样。如果一个功能在没有遥测的情况下发布,第一个用户报告的错误就会变成考古而不是查询。
何时使用
- 构建任何将在生产环境中运行的功能
- 添加新服务、端点、后台任务或外部集成
- 生产事件诊断耗时过长(“我们无法判断发生了什么”)
- 设置或审查告警规则
- 审查涉及 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();
});
永远不要记录机密、令牌、密码或完整的 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 请求持续时间',
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)周围添加手动 span,并附加值班人员可以过滤的属性。在每次异步边界传播上下文——HTTP 头、队列消息元数据——否则追踪会在间隙处中断。默认以低速率进行基于头的采样;如果后端支持尾部采样,则保留 100% 的错误。
6. 告警
对用户感受到的症状进行告警,而不是原因:
症状(值得分页): 原因(仪表盘,不是分页):
错误率 > 1% 持续 5 分钟 CPU 达到 85%
p99 延迟 > 2 秒 一个 Pod 重启
队列年龄 > 10 分钟 磁盘达到 70%
基于原因的告警会在没有问题时触发,并错过你未预测到的故障。基于症状的告警正好在用户受到影响时触发,无论原因是什么。
为你创建的每个告警制定规则:
- 它必须是可操作的。 如果响应是“忽略它,它会自愈”,则删除该告警。
- 它链接到一本 runbook——即使只有三行:含义、要运行的第一个查询、升级路径。
- 它有阈值和持续时间,由 SLO 或历史数据证明,而不是猜测。
- 只使用两个严重级别:分页(面向用户,立即行动)和工单(降级,本周行动)。第三级会成为噪音,训练人们忽略一切。
7. 验证遥测本身
插桩是代码;它可能出错。在宣布工作完成之前,触发路径并查看实际输出:
- 在预发布环境中强制一个错误 → 通过
requestId在日志中找到它,确认字段是结构化的(不是[object Object]) - 发送测试流量 → 确认指标序列出现,带有预期的标签和合理的值
- 在追踪 UI 中跟随一个请求跨服务 → 没有断开的 span
- 每个新告警触发一次(临时降低阈值)→ 确认它到达正确的渠道并且 runbook 链接有效
常见合理化说法
| 合理化说法 | 现实 |
|---|---|
| “我之后再加日志” | “之后”变成了“第一次事件之后”,这是发现你盲点的最昂贵时刻。边构建边插桩。 |
| “更多日志 = 更多可观测性” | 非结构化的噪音使事件处理更慢,而不是更快。三个可查询的事件胜过三百行散文。 |
| “console.log 暂时没问题” | 非结构化输出无法过滤、关联或告警。结构化日志只需多花五分钟。 |
| “出问题时我们看看仪表盘就行” | 没有定义问题而构建的仪表盘会显示一切,除了答案。从值班问题开始。 |
| “对所有重要的事情告警,我们之后调整” | 嘈杂的寻呼机会训练人们忽略它。调整永远不会发生;而真正重要的分页会被错过。 |
| “用户 ID 作为指标标签使调试更容易” | 它也会让你的指标后端崩溃。高基数查询属于日志和追踪。 |
| “追踪对我们的两个服务来说过于复杂” | 两个服务已经意味着日志无法回答的跨服务延迟问题。自动插桩使成本微不足道。 |
红旗
- 一个包含重试、队列或外部调用且没有新遥测的功能 PR
- 通过字符串插值而不是结构化字段构建的日志行
- 没有关联/请求 ID——每行日志都是孤儿
- 使用用户 ID、原始 URL 或错误消息文本作为标签的指标(基数炸弹)
- 延迟以平均值追踪,没有百分位数
- 每天触发并被确认而不采取行动的告警
- 基于原因(CPU、内存)分页人类,而面向用户的错误率未被监控
- 日志中出现机密、令牌或完整的请求体
- “在我的机器上能工作”作为生产功能健康的唯一证据
验证
在插桩一个功能后,确认:
- [ ] 该功能的值班问题已写下,并且每个信号映射到一个问题
- [ ] 所有日志输出都是结构化的(JSON),具有稳定的事件名称和每行的关联 ID
- [ ] 任何日志行中没有机密、令牌或未脱敏的 PII(抽查实际输出)
- [ ] 每个新端点和每个外部依赖都存在 RED 指标,且标签集有界
- [ ] 延迟是直方图;p95/p99 可查询
- [ ] 单个请求可以在追踪 UI 中端到端跟踪,没有断开的 span
- [ ] 每个新告警都是基于症状的,有 runbook 链接,并且已测试触发一次
- [ ] 在预发布环境中诱导的故障仅通过遥测定位,无需阅读源代码
有关此列表的快速概览,包括上线前插桩门禁,请参见 references/observability-checklist.md。






