mcpField
O
observability

observability

github.com/citypaul云与 DevOps75974 次安装4 周前更新
代码仓库

关于 observability

面向服务遥测、告警和 SLO 设计的工程技能,强调结构化宽事件,并提供 OpenTelemetry 指导及指标与采样的成本考量。

主要功能

  1. 01按请求记录结构化宽事件
  2. 02OpenTelemetry 埋点指导
  3. 03指标基数与采样指导
  4. 04SLI、SLO 和错误预算实践
  5. 05告警设计与遥测测试资料

适用场景

  1. 01为后端、Worker 或 API 添加埋点
  2. 02定义 SLO 与错误预算策略
  3. 03审查告警、采样或遥测成本

安装

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

$npx skills add https://github.com/citypaul/.dotfiles --skill observability

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

resources/
node-patterns.md
references.md
slo-alerting.md
testing-telemetry.md

详细介绍

可观测性

为尚未提出的问题添加监测能力。 监控用于验证你预测到的故障模式;可观测性则让你能够询问系统那些从未预料到的故障。有效的遥测应回答两个问题——出了什么问题(症状)以及为什么(原因)——而两者所需投入并不均衡:应将更多精力放在捕捉症状上,而不是罗列原因 (Google SRE,《监控分布式系统》)。

此技能涵盖遥测包含什么以及如何使用遥测。twelve-factor 技能负责日志的传输和格式(由平台捕获的进程流中的结构化记录、级别、时间戳)。hexagonal-architecture 技能负责在端口与适配器代码库中确定监测代码的位置。

深入阅读资源位于 resources/ 目录中。按需加载:

资源适用场景……
node-patterns.md将 OpenTelemetry 接入 Node/TypeScript 服务——NodeSDK 设置、--import 加载、宽事件中间件、日志与追踪关联、Collector 配置、语义约定速查表
slo-alerting.md定义 SLI/SLO、计算消耗速率、构建多窗口多消耗速率告警规则、编写运行手册
testing-telemetry.md为监测代码编写 Vitest 测试——内存导出器、断言宽事件字段、遥测端口的伪实现
references.md查阅此指南背后的理由或原始资料

适用场景

  • 为新服务或现有服务(后端、工作进程、API)添加监测
  • 定义 SLI/SLO 或错误预算策略
  • 设计、审查或精简告警
  • “我们看不到生产环境正在发生什么”——排查未知的未知问题
  • 审查遥测成本、指标基数或采样策略

不适用此技能: 诊断失败的 CI 运行(ci-debugging)、本地环境与生产环境的差异(production-parity-skill-builder)、优化 Core Web Vitals(在已安装的情况下使用 core-web-vitals)。


宽事件(规范日志行)

主张明确的默认做法:每个请求在每项服务中发出一个结构化、信息密集的事件。 这是 Stripe 的规范日志行——“在末尾记录一条包含许多关键特征的长日志行” (Stripe),也是现代可观测性核心的“宽度任意的结构化事件” (Charity Majors)。

实现机制:

  1. 请求进入服务时,中间件创建一个作用域限于该请求的累加器
  2. 业务逻辑和中间件在处理过程中添加字段
  3. 在请求结束时,通过 finally/teardown 逻辑仅发出一次事件——即使发生异常也必须保留该事件,因为这正是你需要它的时候

构造规则——只有一个出口,并通过抛出异常来验证。 对于基于 Promise 的处理程序,要在任何可能抛出异常的操作之前开启 try:包括解析请求和读取请求正文,而不只是业务调用。完成事件的唯一调用位置——设置结果属性、记录异常、结束 span 或发出记录——必须位于对应的 finally 中。用一个辅助函数完成这三项操作没有问题,但它只能在 finally 中调用,不能在其他地方调用。在成功路径中调用辅助函数,再配上一个只负责结束 span 或刷新数据的 finally,仍然会丢失所有抛出异常时的字段。在认定工作完成之前,运行一个让处理程序在其自身 catch 之外抛出异常的测试,并断言仍然记录了一个包含结果的事件。

事件中包含的内容:

类别字段示例
请求方法、规范化的路由模板(不可用时省略)、状态、耗时
身份用户/主体 ID、身份验证方法、API 密钥 ID
速率限制是否允许、配额、剩余额度
性能数据库查询次数、缓存命中次数、外部调用耗时
业务上下文触发的规则、拒绝原因、功能标志
错误错误代码、错误类别、重试次数
关联信息trace ID、请求 ID、构建/部署 ID

高基数标识符适合放在事件中,但须遵循隐私政策——请求 ID、构建 ID,以及经隐私审批或假名化的主体标识符,都能让事件更有用。高基数是事件的一项优势,却会让指标成本激增;但事件适用并不意味着可以发出个人数据(参见下文的「采样与成本经济学」和「遥测卫生」)。

这种做法为何优于分散的日志行:数据到达时就已关联好。你可以查询完整的行,而不必用正则表达式从片段中还原请求,再寄希望于结果正确。带有丰富属性的 OpenTelemetry 根 span,是这一模式的有效实现方式——通过 span 进行检测,可以同时满足宽事件和分布式追踪两派的需求。


坦诚看待支柱

行业惯用的说法是“三大支柱”:日志、指标、追踪。严肃的批评意见(Majors,Observability 1.0 vs 2.0)是,三大支柱意味着真相来源散落在许多工具中,每个请求会被存储多次,工程师需要手动关联数据,而且每个支柱都会增加成本。另一种做法是:使用单一真相来源——宽结构化事件,并在读取时从中派生指标、追踪和 SLO。

团队立场:

  • 宽事件是默认的埋点方式。 它们能避免重复埋点并保留更多未来提问的可能性,但导出、处理和存储仍需明确的遥测成本预算。
  • 指标仍有其价值,适用于低成本、长期保留的聚合数据和告警计算——但标签集必须严格设限。
  • 追踪是带有结构的宽事件——用于表示跨服务的父子因果关系。通过 OTel span 埋点即可兼得二者。

坦诚看待其局限: 在读取时派生所有数据,前提是后端能够大规模聚合原始事件。并非每种技术栈都具备这样的后端。不要盲目照搬任何一方的做法——无论如何都应发出宽事件(数据才是资产,后端可以更换),同时为仪表板和告警规则保留一小组有界指标。


OpenTelemetry:基底

OpenTelemetry(OTel)是用于生成遥测数据的厂商中立标准。采用它意味着埋点不受任何后端选择的限制。

组成部分:

  • 信号——追踪、指标、日志,全部通过 OTLP 导出
  • 资源属性——service.name 是必填项;它用于在每个后端中标识发出数据的服务
  • 语义约定——标准化的属性名称(http.request.method、db.system),让不同团队和工具之间的遥测数据可以互相关联(semconv)。语义约定中已定义的属性名称,绝不可自行另造。
  • 上下文传播——W3C traceparent 标头会在每次服务间调用中传递追踪 ID 和父 span ID,这正是分布式追踪得以存在的基础(上下文传播)。只要代码要构建发往另一服务的出站请求——无论通过厂商 SDK、生成的客户端还是手写标头——都应将活动上下文注入该请求的标头(propagation.inject(context.active(), headers)),而不是自行写入关联标头。 下游团队若要求“提供一些我们可以搜索的内容,以便将你的调用与我们的调用对应起来”,他们要的就是 traceparent,无论他们是否知道这个名称:x-trace-id、x-correlation-id,以及任何自定义键下的裸追踪 ID,都会丢失父 span ID 和采样标志,而且没有标准工具能将它们关联到任何内容
  • Collector——在服务旁运行的接收 → 处理 → 导出流水线。它是生产环境的推荐默认方案,可在不修改代码的情况下实现批处理、重试、脱敏和后端切换。如果部署简单,且有文档记录的直接导出设计能够明确满足缓冲、重试、背压、过滤和运维要求,也可以采用(Collector)

TypeScript 最小接入方案: 使用 @opentelemetry/sdk-node + @opentelemetry/api + @opentelemetry/auto-instrumentations-node、在应用代码之前通过 node --import ./instrumentation.mjs 加载埋点文件(或在开发期间使用经过审查并安装在代码仓库中的 TypeScript 运行器),以及 OTLP 导出器。自动埋点会免费生成 HTTP/框架/数据库 span;手动添加的属性则赋予追踪可查询的业务含义。初始化顺序很重要——如果在应用模块加载后才加载埋点,就会悄无声息地捕获不到任何内容。请参阅 resources/node-patterns.md。


采样与成本核算

基数路由规则

指标上的每种唯一标签组合都是一条独立时间序列。已有 1,000 条时间序列的指标新增 tenant_id(10,000 个取值)后,就会有 1,000 万条时间序列,而不是 11,000 条;每条活跃时间序列都会占用内存,而且大多数供应商按时间序列或自定义指标计费(Grafana 关于高基数问题的说明)。

规则:有界、低基数的维度放在指标上;无界、高基数的维度放在事件/跨度中。 只有将从请求中提取的标签规范化为固定的允许列表(例如已知的方法或结果类别)后,才可安全使用;将未知值映射为 _OTHER,或直接省略。绝不要使用原始的或无界的、由攻击者控制的值,例如 ID、路径、查询字符串、任意请求头或正文中的字段。有人问“我们能按客户细分这个指标吗?”时,答案是“这个问题应该交给事件存储来处理”。

采样阶梯
阶段时机方式
不采样低流量保留所有数据——采样是控制成本的工具,不是值得追求的目标
头部采样流量增长在跟踪开始时作出决策(根据跟踪 ID 进行概率采样),并保持一致传递,成本低廉——但无法保证捕获错误
尾部采样决策需要已完成跟踪的属性收到完整跟踪后再作出决策;可以优先保留选定的错误和异常值,但需要有状态的 Collector 层——涉及缓冲、扩缩容、运维成本,以及可能的供应商锁定

(OTel 采样概念)

绝不要对用于计算 SLO 的数据流进行静默采样。 如果无法避免采样,必须在 SLI 计算中将其纳入考量。应单独配置并验证错误跟踪的保留策略;实际保留哪些数据,取决于尾部采样策略和容量。


SLI、SLO、错误预算

以下定义摘自 Google SRE 书籍:SLI 是对服务级别经过审慎定义的定量度量;SLO 是该 SLI 的目标值;SLA 是附带后果的 SLO。错误预算等于 100% 减去 SLO,其目的在于为发布提供依据:将预算花在发布上,而不是一味追求用户反正无法区分的 100%。

SLI 选项(用于挑选的记忆口诀,而非硬性要求):

  • RED — 速率、错误、耗时 — 适用于每个由请求驱动的服务;明确作为用户体验的代理指标(Tom Wilkie)
  • USE — 利用率、饱和度、错误 — 适用于每种硬件资源;侧重基础设施(Brendan Gregg)
  • 四大黄金信号 — 延迟、流量、错误、饱和度 — 还有两个常被忽略的细节:单独跟踪失败请求的延迟(缓慢的 500 与快速的 500 代表不同的问题);使用直方图,因为平均值会掩盖长尾——在 1,000 rps、平均延迟为 100ms 时,1% 的请求很容易耗时 5s(SRE 书籍)

SLO 准则: 少设 SLO,并确保每个都简单到能用一句话解释清楚。将可用性、正确性和新鲜度定义为良好事件占有效事件的比例。将延迟目标定义为低于明确阈值的有效事件比例;诊断延迟分布时,使用百分位数而不是均值。内部目标应略严于公布的目标。不要过度达标——用户会逐渐依赖你实际提供的可靠性,而不是你承诺的可靠性。


告警:症状、页面、消耗率

针对症状发出页面告警,而非针对原因。“如果 MySQL 服务器宕机,用户会在意吗?不会,他们在意的是查询是否失败”(Rob Ewaschuk,《我的告警理念》——这份文档后来成为《SRE》一书第 6 章)。基于原因的数据应放在仪表板和工单中,而不是页面告警中。少数例外:迫在眉睫且确定无疑的原因(例如 4 小时后配额将耗尽)。

**每个页面告警都必须:**紧急、可采取行动、用户可见,并且需要人类智能来处理。如果页面告警的响应可以编写脚本,就编写脚本并删除该页面告警。衡量误报页面告警,然后修复或移除那些反复浪费响应人员注意力的告警。

默认的告警构造方式是多窗口、多消耗率告警(SRE 工作手册)。消耗率 = 相对于 SLO 消耗错误预算的速度(消耗率为 1 = 在周期结束时恰好耗尽预算)。对于 30 天内 99.9% 的 SLO:

严重程度消耗率长窗口短窗口已消耗预算
页面告警14.41 h5 m2%
页面告警66 h30 m5%
工单13 d6 h10%

短窗口(长窗口的 1/12)用于确认问题仍在发生,从而解决朴素阈值告警在重置时间上的缺陷。这是唯一能同时在精确率、召回率、检测时间和重置时间方面表现良好的构造方式——推导过程见 resources/slo-alerting.md。

每个页面告警都要链接到运行手册——内容简明,说明“此告警的含义和当前缓解措施”,而不是详尽的故障排查流程树。


结构化日志实践

twelve-factor 技能负责传输和格式(由平台采集的进程流中的结构化记录、可识别的级别、ISO 时间戳)。本节讨论的是内容规范。

级别检验(引自 Dave Cheney):“没人会读警告,因为按定义来说什么问题都没发生”,以及“如果你选择通过记录日志来处理错误,按定义来说它就不再是错误了。”保留标准的四个级别,但要对每条日志应用 Cheney 的检验:每条 warn 都必须有明确的读者;每条 error 日志都必须对应真正未处理的故障,而非已处理后又重复报告的故障。

**在边界处记录日志;在中间累积信息。**大多数请求内的 info 级别闲谈应改为宽事件中的字段,而不是单独的日志行。产生 40 条日志行的请求,应改为产生一个规范事件,加上少量真正独立的信息。

**关联:**每条日志记录都携带当前活动的跟踪 ID(W3C 跟踪上下文),这样日志就能免费关联到跟踪和规范事件。

PII 和机密信息规范:

  • 绝不在任何信号中输出密码、令牌、API 密钥、Cookie 或会话 ID
  • 只有在明确的运营需求和适用的隐私政策允许时,才输出个人标识符;应尽量减少或假名化,并实施适当的保留、访问和删除控制
  • 仅允许输出明确列出的字段——绝不序列化整个请求、用户或配置对象;JSON 序列化器会毫不犹豫地输出身份验证标头
  • 在应用程序的数据源处进行脱敏——流水线是第二道防线,而非第一道
  • 在受监管环境中,每条包含 PII 的日志行都会成为合规义务(保留、访问控制、删除权)

Effect 日志记录

在基于 Effect 的运行时中,优先使用由组合提供的 Logger Layers 实现原生日志记录和注释。移除通过传递 JavaScript 日志记录器回调和诊断包装器来重复实现这些功能的做法。如果只是重新实现这些能力,就应移除它们。按照下方的架构规则保留领域观察;原生日志记录并不意味着要在纯领域决策中引入日志记录器。

对于覆盖范围较广的事件,使用作用域终结器或可感知退出状态的等效机制,使其涵盖失败和中断,而不只是成功;在构建 Effect 时使用 JavaScript finally 无法观察它的执行。为该边界事件在程序内部累积安全注释。

采用时保留源数据脱敏和对接收端故障的隔离:JSON 输出并不能保证这两点。检查已安装的生产日志记录器,并通过实际操作进行测试,确保接收端故障既不会改变产品结果,也不会掩盖原始故障;如果原生行为需要隔离,请使用范围狭窄的接收端适配器。有关固定版本的 API 验证和运行时生命周期规则,请阅读 Effect runtime composition。

埋点应位于何处(架构位置)

在端口与适配器代码库中,可观测性代码有四个归属位置——四层模型(完整说明:hexagonal-architecture skill,resources/cross-cutting-concerns.md):

  1. 技术遥测 → 适配器。 请求/响应日志记录、SQL 耗时、重试、自动埋点。绝不放在领域代码中。
  2. 对领域有意义的观察 → 显式的驱动端口(Domain Probe)或领域事件。 当某个中间业务事实很重要(“触发了哪条定价规则”),或观察本身就是一项要求(支持日志记录、业务指标)时,可观测性后端应作为驱动参与方,通过按能力划分、不含严重性级别、即发即弃的端口接入——绝不能使用通用的 Logger 端口。如果已有领域事件,可观测性订阅器优于另设一个通道。
  3. 关联信息和宽事件组装 → 仅限中间件/适配器。 领域永远不会看到跟踪 ID。领域维度通过结果类型、探针或事件传递到宽事件中。
  4. 埋点是经过测试的行为。 探针是驱动端口;每个驱动端口都要有一个假实现(参见下方的“测试可观测性”)。

即使不采用六边形架构,也应遵循同一原则:业务逻辑返回数据并发布事实;边界层将其转换为遥测数据。


测试可观测性

埋点是一种行为,因此应像行为一样由测试驱动(参见 tdd 和 testing skills)。

  • 内存导出器——OTel SDK 自带此类导出器——可让 Vitest 在不发起网络调用的情况下,对每个 Span、属性和状态进行断言:“此请求会发出一个包含 pledge.rejection_reason 的规范事件”“此故障会将 Span 状态设为 error”。参见 resources/testing-telemetry.md。
  • 遥测端口要使用假实现,与任何驱动端口相同——记录型假实现会累积观察,测试则通过公共 API 对这些观察进行断言。示例:hexagonal-architecture skill,resources/testing-hex-arch.md。
  • 告警规则也是代码。 如果技术栈支持(Prometheus 规则单元测试、SLO 即代码工具),应使用合成数据测试燃烧速率表达式是否会触发。
  • 关于变异测试的说明: 未断言的探针/遥测调用就像一个不断产生存活变异体的温床——这本身就说明了为什么要对观察结果进行断言。

实际限制: 你无法通过有意义的单元测试验证采样百分比、Collector 管道或后端保留策略。应在预发布环境中使用真实的 Collector 验证这些内容——同时注意,采样配置本身也是一个一致性检查面(预发布环境采样率为 100%、生产环境为 1% 时,调试行为会有所不同)。


前端说明(不在 v1 范围内)

浏览器可观测性采用 RUM(真实用户监控),并以 Core Web Vitals 作为标准 SLI——在真实页面加载的第 75 百分位数处,LCP ≤ 2.5s、INP ≤ 200ms、CLS ≤ 0.1;实验室工具“不能替代现场测量”(web.dev)。此技能涵盖服务;关于面向用户的部分,请参阅已安装的 core-web-vitals 和 performance 技能。


反模式

#反模式错误原因
1指标上使用无界标签(用户 ID、原始 URL、容器 ID)基数爆炸——内存、查询时间和账单都会随序列数增长
2零散的日志行,而不是累积的宽事件上下文被拆散;调查变成对正则表达式的考古
3异常路径上跳过规范事件事件恰恰在最重要的时候消失;在 finally 中发出
4根据成因(CPU、磁盘、副本延迟)触发告警用户感受不到成因;针对症状的告警能以更少噪声捕捉更多问题
5没有运行手册,或无法采取任何操作的告警无法采取行动的告警会让人们学会忽略告警
6将整个对象序列化到日志中身份验证标头、令牌和 PII 会一并记录;仅允许列出的命名字段
7自行发明 semconv 已定义的属性名称毫无益处地破坏跨团队和跨工具的关联能力
8悄悄对计算 SLO 的数据流进行采样SLI 计算会沦为虚构;以 100% 的比例保留错误,或调整计算方式
9将调试用的 console.log 留作“插桩”无结构、无法查询、无法关联——应删除或提升为真正的字段。删除范围是文件,而不是请求:如果你正在编辑的文件中,在你未被要求处理的路径上也存在这类噪声,就把该文件中的所有这些内容都删掉——只清理一半会留下无人能查询、也不会有人回来处理的代码行
10每种信号都配置一个独立的供应商 Agent,而不是使用 OTel将插桩绑定到某个后端;OTel 让后端可以替换
11每当错误发生时就立即对其全部告警错误预算的存在意义,就是让低于燃烧率阈值的噪声不进入告警通知

边界

关注点负责人
日志传输、标准输出、JSON 结构、级别、时间戳twelve-factor
遥测中包含什么;宽事件、追踪、SLO、告警此技能
诊断失败的 CI 运行ci-debugging
遥测端口、Domain Probes、四层放置细节hexagonal-architecture
环境漂移(本地可运行,生产环境不可运行)production-parity-skill-builder
HTTP 错误响应正文(RFC 9457)api-design
CLI 使用遥测的同意机制cli-design
Core Web Vitals / 前端 RUMcore-web-vitals(外部技能,在已安装时使用)

验证清单

  • 每个请求恰好发出一个规范的宽事件,包括异常路径
  • 经隐私审批的高基数标识符(主体、请求、构建、租户)仅存在于事件/Span 中,绝不作为指标标签
  • 指标标签使用有界的规范化允许列表;未知请求值映射为 _OTHER 或予以省略,原始的攻击者可控值绝不能成为标签
  • 已设置 service.name 和资源属性;存在语义约定名称时使用这些名称
  • 跟踪上下文(W3C traceparent)会跨越每次服务跳转进行传播,并写入每条日志记录
  • OTel SDK 在应用代码加载前初始化(--import),并启用自动埋点
  • 生产遥测使用推荐的 Collector 路径;若有文档记录的直接导出例外,则该例外满足缓冲、重试、背压、过滤和运维要求
  • 采样和 Collector 容量满足既定的错误跟踪保留目标和丢失预算;若目标无法容忍采样造成的丢失,则使用无采样采集
  • 每个用户旅程最多设置少量基于比率的 SLO;明确延迟阈值和诊断百分位数,并制定错误预算策略
  • 分页告警以症状为依据并由多窗口消耗速率驱动;每个告警都关联一份运行手册
  • 对于无需立即人工处理的情况,不触发分页告警
  • 遥测不包含机密信息;个人标识符必须确有必要、经策略批准,并经过最小化处理或假名化;字段须采用允许列表并在数据源处进行脱敏
  • 埋点已通过测试覆盖(内存导出器或假探针断言)
  • 添加指标标签或新信号时,须提交书面的基数/成本估算

评论

支持 Markdown