首页
/ agent-skills observability-and-instrumentation 技能详解:从 On-Call 问题到遥测验证的生产级埋点工作流

agent-skills observability-and-instrumentation 技能详解:从 On-Call 问题到遥测验证的生产级埋点工作流

2026-09-06 23:00:10作者:何举烈Damon

在 agent-skills 仓库中,observability-and-instrumentation 是 Ship 阶段的核心技能之一,它把"代码上线即可观测"拆解为七步可执行流程:先写下 on-call 会问的问题,再按问题挑选日志、指标或追踪信号,最后验证遥测本身是否可用。读完本篇,你将掌握该技能完整的工作流、结构化日志与相关 ID 的实现要点、RED/USE 指标与基数控制规则、OpenTelemetry 追踪配置,以及症状导向告警的设立标准,并能用仓库自带的评价用例与共享检查清单在自己的项目中复刻这套流程。

技能定位:什么场景激活,什么场景明确排除

技能定义文件为 skills/observability-and-instrumentation/SKILL.md,其 YAML frontmatter 声明了激活条件:

---
name: observability-and-instrumentation
description: Instruments code so production behavior is visible and diagnosable. Use when adding logging, metrics, tracing, or alerting. Use when shipping any feature that runs in production and you need evidence it works. Use when production issues are reported but you can't tell what happened from the available data.
---

这个 description 遵循 docs/skill-anatomy.md 的规范:第三人称说明"做什么"加多个 "Use when" 触发条件,让 Agent 在系统提示中就能判断何时加载全文。技能的核心理念只有一句话:你无法观测的代码,就无法运维。埋点不是上线后的补丁,而是和测试一样与功能同时编写——如果一个功能带着零遥测上线,第一个用户报障就会变成"考古"而不是"查询"。

应激活的场景(来自 SKILL.md 的 "When to Use"):

  • 正在构建任何将运行在生产环境的功能
  • 新增服务、端点、后台任务或外部集成
  • 生产事故诊断耗时过长("我们说不清当时发生了什么")
  • 正在建立或审查告警规则
  • 评审一个新增 I/O、重试、队列或跨服务调用的 PR

明确排除的场景(技能边界划分,避免与其他技能职责重叠):

场景 应转向的技能
正在诊断一个刚刚发生的故障 debugging-and-error-recovery(见 skills/debugging-and-error-recovery/SKILL.md)——本技能的价值是让那次诊断下次变快
对已量化的慢速做剖析与优化 performance-optimization(见 skills/performance-optimization/SKILL.md)
上线日的监控清单与回滚触发器 shipping-and-launch(见 skills/shipping-and-launch/SKILL.md)——本技能只覆盖供给那些清单的埋点

这种"NOT for"划分是 agent-skills 包的典型设计:每个技能负责一段生命周期,互相引用而不重复内容。

第一步:先定义"working",再开始埋点

技能流程的第一条原则:没有问题的遥测就是噪声。在添加任何埋点之前,写下 2–4 个 on-call 工程师会针对该功能提出的问题。SKILL.md 给出的标准模板:

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.

判据很直接:如果你写不出这些问题,说明你还没准备好埋点——你会记录一切,却学不到任何东西。

这条原则在仓库的评价夹具里有真实落样。evals/fixtures/observability-and-instrumentation/operations.md 就是为"支付重试"功能写好的 on-call 问题清单,并附带数据边界规则:

On-call must be able to answer:

  • Are retries recovering transient gateway failures?
  • Which gateway and failure class is driving exhaustion?
  • Is one payment being charged more than once?
  • Which customer-visible payments need intervention now?

Payment and attempt IDs are safe correlation identifiers. Card numbers, customer email addresses, and raw gateway responses must never be logged.

对应的评价用例 evals/cases/observability-and-instrumentation.json 中,提示词为 "Instrument a new payment-retry feature so on-call can operate it.",其期望输出第一条就是 "On-call questions are written before instrumentation is added"——先写问题、后加信号被固化为可机检的验收项。

第二步:为每个问题挑选正确的信号类型

SKILL.md 给出了三类信号的选型表:

信号 回答的问题 成本模型 示例
结构化日志 "这个具体 case 发生了什么?" 按事件计费;随流量增长 带 provider 错误码的 payment_failed
指标 "整体有多频繁/多快?" 每条时间序列固定开销;查询便宜 provider 调用的 p99 延迟
分布式追踪 "跨服务的时间花在哪?" 按请求计费;通常采样 一次慢 checkout,按跳数拆分

经验法则一句话记牢:指标告诉你"出问题了"(that),追踪告诉你"在哪"(where),日志告诉你"为什么"(why)。每个信号都必须映射回第一步的某个问题,否则就是纯噪声。

第三步:结构化日志——事件而非散文

核心要求:记录事件,而不是散文。每一行日志都是带稳定事件名和机器可读字段的 JSON 对象。SKILL.md 的对照示例(见 skills/observability-and-instrumentation/SKILL.md#L56-L68):

// 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');

这一点在评价夹具中同样有"反面教材":evals/fixtures/observability-and-instrumentation/payment-retry.js 中的 retryPayment 函数只有 console.log(retry ${attempt} failed: ${error.message}) 一行日志——字符串插值、无事件名、无关联 ID、无网关与错误码字段,正好命中技能后面所有 Red Flags,是 Agent 练习整改的标准素材。

日志级别:一致地使用

级别 含义 On-call 动作
error 不变量被破坏;可能需要人介入 立即调查
warn 降级但已处理(重试成功、走了 fallback) 观察趋势
info 重要业务事件(订单创建、任务完成)
debug 诊断细节 生产环境默认关闭

相关 ID:强制项

在系统边界生成(或接收)一个 request ID,并把它附加到每一行日志、每一个 span、每一次出站调用。没有它,你无法从交错的日志中重建单次请求。SKILL.md 给出的 Express 实现:

// 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 以同样方式传播。技能提供的统一 helper:

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

两个要点值得注意:字段刻意命名为 entryPoint 而非 source,是因为 ECS(Elastic Common Schema)把 source.* 保留给网络字段;另外,entryPoint 必须和相关 ID 一样穿越所有边界(队列元数据、HTTP 头),否则 worker 会自行"重新推导"入口——一个只是与入口相关的字段只是提示而非归因,因为任何能调用该任务的东西都能复现它。

硬规则:绝不记录秘密与完整 PII

这是从 security-and-hardening 技能继承的硬约束——遥测管道是经典的数据泄露路径。字段用白名单,不要记录整个请求体。对应地,operations.md 夹具中"卡号、客户邮箱、原始网关响应绝不可记录,支付/尝试 ID 才是安全的关联标识符"正是这条规则在具体功能上的实例化。

第四步:指标——RED/USE 与基数控制

对请求驱动的服务,在每个端点和每个外部依赖上埋 RED:Rate(每秒请求数)、Errors(失败率)、Duration(延迟直方图,而非平均值);对资源(队列、连接池、主机)用 USE:Utilization、Saturation、Errors。

SKILL.md 说明指标 API 同样有厂商中立的 OpenTelemetry 路径(与第五步共用同一 SDK 与 context),下面的 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],
});

基数是指标系统的失效模式。每一个唯一标签组合就是一条独立时间序列。标签必须来自小而固定的集合(路由模板、状态码类、provider 名)。绝不把用户 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。

第五步:分布式追踪——OpenTelemetry 自动埋点

使用 OpenTelemetry——它是厂商中立标准,自动埋点几乎零代码即可覆盖 HTTP、gRPC 和常见数据库客户端。SKILL.md 的初始化代码(skills/observability-and-instrumentation/SKILL.md#L138-L148):

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

三条配套规则:

  1. 手动 span 只加在有意义的内部工作单元周围(如 applyDiscountschargeProvider),并附上 on-call 会用来过滤的 attribute;
  2. 上下文必须穿越每一个异步边界——HTTP 头、队列消息元数据——否则追踪在缺口处断裂;
  3. 采样策略:默认低比例 head-based 采样;如果后端支持 tail sampling,保留 100% 的错误。

第六步:告警——只告警用户能感知的症状

原则:对用户能感知的症状告警,而不是对原因告警。SKILL.md 的对照表:

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. 链接到 runbook——哪怕只有三行:它意味着什么、先跑哪条查询、升级路径。
  3. 阈值与持续时间必须有依据——来自 SLO 或历史数据,而非猜测。
  4. 只用两个严重级别:page(用户可感知,立即行动)和 ticket(降级,本周内处理)。第三个级别会变成噪声,训练出"对一切视而不见"的人。

第七步:验证遥测本身

埋点是代码,代码就会写错。技能要求在宣布完工前,主动触发路径并检查真实输出:

  • 在 staging 强制制造一个错误 → 通过 requestId 在日志中找到它,确认字段是结构化的(而非 [object Object]);
  • 发送测试流量 → 确认指标序列以预期标签出现且数值合理;
  • 在追踪 UI 中跟一次请求跨服务走完 → 没有断掉的 span;
  • 把每条新告警临时降低阈值各触发一次 → 确认它到达正确的通道,且 runbook 链接有效。

常见自我合理化与反驳

技能最具辨识度的部分是"反合理化"表,专门拦截 Agent(和人)跳过步骤的借口:

合理化借口 现实
"等它跑通了再加日志" "之后"会变成"第一次事故之后",那是发现自己是盲的最贵的时刻。边建边埋点。
"日志越多 = 可观测性越强" 非结构化的噪声让事故处理更慢而不是更快。三条可查询事件胜过三百行散文。
"console.log 先用着" 非结构化输出无法过滤、关联或告警。结构化 logger 一次性多花五分钟。
"出事了看 dashboard 就行" 没有从问题出发的 dashboard 显示的是"除了答案之外的一切"。从 on-call 问题开始。
"所有重要的都告警,之后再调" 嘈杂的 pager 会训练人们忽略它。调优永远不会发生,被漏掉的真 page 会发生。
"用户 ID 做指标标签方便排查" 它同时会让你的指标后端倒下。高基数查询属于日志和追踪。
"我们才两个服务,追踪是过度设计" 两个服务就意味着存在日志回答不了的跨服务延迟问题,而自动埋点让成本可以忽略。

Red Flags:评审时的检查信号

SKILL.md 列出的可观测行为红旗,可直接用于 PR 评审:

  • 带重试、队列或外部调用的功能 PR,却没有新增任何遥测
  • 日志行靠字符串插值构建,而非结构化字段
  • 没有相关/请求 ID——每一行日志都是孤儿
  • 调度器、webhook 和手动运行写入同一流,却没有字段说明哪一行出自哪个入口
  • 指标标签用用户 ID、原始 URL 或错误信息文本(基数炸弹)
  • 延迟只有平均值,没有百分位
  • 每天触发、被无动作确认的告警
  • 对着原因(CPU、内存)呼叫人类,而用户侧错误率无人监控
  • 日志中出现秘密、token 或完整请求体
  • "在我机器上是好的"作为生产功能健康的唯一证据

验收清单(Verification)

技能以九条退出标准收尾,全部要求证据:

  • [ ] 该功能的 on-call 问题已写下,且每个信号都映射到其中一个问题
  • [ ] 所有日志输出为结构化 JSON,带稳定事件名,每行都有相关 ID
  • [ ] 凡由多个入口写入的日志流都带入口字段,在运行启动处设置并随相关 ID 传播,而非下游推断
  • [ ] 任何日志行中都没有秘密、token 或未脱敏 PII(抽查真实输出)
  • [ ] 每个新端点和每个外部依赖都有 RED 指标,标签集合有界
  • [ ] 延迟是直方图,p95/p99 可查询
  • [ ] 单个请求可以在追踪 UI 中端到端跟随,无断链 span
  • [ ] 每条新告警都是症状导向的,带 runbook 链接,且被试触发过
  • [ ] 在 staging 制造的一次故障仅靠遥测就定位到了,没有读源码

仓库内的配套资源:共享检查清单与评价用例

共享清单:references/observability-checklist.md

该技能在 Verification 末尾指向 references/observability-checklist.md——仓库根目录 references/ 下的共享清单,是上述流程的"一瞥版"。它按七个部分组织:On-Call Questions(起点)、Structured Logging(九项)、Metrics(八项)、Distributed Tracing(七项)、Alerting(七项)、Dashboards(四项)、Verify the Telemetry(四项),外加一个上线门禁(Pre-Launch Gate):

  • 结构化日志已流入日志汇聚器
  • 每个新端点和依赖的 RED 指标已在 dashboard 可见
  • 至少一条症状导向告警已配置、带 runbook 且试触发过
  • 一个请求可以跨越它经过的所有服务被追踪
  • on-call 知道 runbook 在哪

注意 docs/skill-anatomy.md 对此的设计解释:被多个技能共用的清单刻意放在仓库根 references/,而不是某个技能目录内,避免"每个技能复制一份"或"某个技能独占"造成的漂移。README 也提示:单技能安装(只拷贝 skills/<name>/)不会带上 references/,此时需要整仓集成或手动拷贝所需清单,这个可移植性缺口在仓库 issue #361 中跟踪。

评价夹具:可复现的练习素材

evals/fixtures/observability-and-instrumentation/ 目录提供了一对配套素材:payment-retry.js 是一个"待改造"的重试函数(14 行,唯一日志是插值字符串的 console.log),operations.md 是该功能的 on-call 问题与 PII 边界。配合 evals/cases/observability-and-instrumentation.json 中的触发语料(如 "Add structured logging and metrics to the checkout service"、"Set up tracing so we can follow a request across services")与四条期望(先写问题、结构化事件日志、避免无界基数、症状导向告警),这套夹具让"技能是否被正确执行"成为可自动评估的命题。仓库的 scripts/run-evals.js 即用于驱动这些用例。

安装与校验

README.md 的说明,该技能可通过 skills CLI 单独安装(npx skills add addyosmani/agent-skills --skill observability-and-instrumentation)或整包安装后由各 Agent 自动发现;技能的格式本身可用 scripts/validate-skills.js 对全部技能做结构校验(规则实现在 scripts/lib/skill-lint.js)。

小结

observability-and-instrumentation 技能把"让生产行为可见"压缩成一条强约束链条:先写 on-call 问题 → 按问题选信号(metrics = that, traces = where, logs = why)→ 结构化日志 + 相关 ID + 入口字段 → RED/USE 指标与有界标签 → OpenTelemetry 自动追踪 + 边界上下文传播 → 症状导向、双级别告警 → 触发并验证遥测本身。它同时是 debugging-and-error-recovery 提速的前提、shipping-and-launch 监控清单的数据来源,并通过 references/observability-checklist.md 提供可勾选的一瞥版门禁。对于使用 AI 编码 Agent 的团队,这套技能的价值在于把资深工程师"边建边埋点、遥测必须可验证"的直觉,固化成 Agent 每一步都必须执行、且有退出标准的工作流。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388