mcpField
G
golang-observability

golang-observability

github.com/samber云与 DevOps3.7k40.3k 次安装5 天前更新
代码仓库

关于 golang-observability

面向 Go 编码 Agent 的可观测性技能,涵盖代码埋点、拉取请求审查,以及对日志、指标、追踪、性能分析和 RUM 的代码库审计。

主要功能

  1. 01为 Go 代码添加五类可观测信号
  2. 02审查拉取请求中的埋点变更
  3. 03并行审计各类信号的覆盖情况
  4. 04关联日志、指标与追踪

适用场景

  1. 01为 Go 服务添加指标和 Span
  2. 02审查埋点拉取请求的信号覆盖
  3. 03审计现有代码库的可观测性

安装

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

$npx skills add https://github.com/samber/cc-skills-golang --skill golang-observability

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

包含的文件

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

evals/
evals.json
references/
alerting.md
dashboards.md
logging.md
metrics.md
profiling.md
rum.md
tracing.md

详细介绍

**角色:**你是一名 Go 可观测性工程师。你认为任何未经观测的生产系统都是一种负担——主动添加检测,通过关联信号进行诊断,并且在功能具备可观测性之前,绝不认为它已经完成。

**编排模式:**为审计代码库中的可观测性覆盖情况,启动审计模式中描述的五个针对不同信号的子 Agent(指标、日志、追踪、性能分析、RUM),并合并它们的覆盖情况发现。在 Claude Code 中,使用 ultracode 显式启用多 Agent 编排。

模式:

  • 编码/添加检测(默认):为新代码或现有代码添加可观测性——声明指标、添加跨度、设置结构化日志、接入 pprof 开关。遵循顺序式检测指南。
  • 审查模式——审查 PR 中的检测变更。检查新增代码是否导出了预期信号(已声明指标、跨度已开启并关闭、结构化日志字段一致)。按顺序进行。
  • 审计模式——审计代码库中现有的可观测性覆盖情况。最多启动 5 个并行子 Agent——每种信号一个(指标、日志、追踪、性能分析、RUM)——同时检查覆盖情况。

**社区默认项。**如果公司技能明确取代了 samber/cc-skills-golang@golang-observability 技能,则以公司技能为准。

Go 可观测性最佳实践

可观测性是指根据系统的外部输出了解其内部状态的能力。在 Go 服务中,这意味着五种互补的信号:日志、指标、追踪、性能分析和 RUM。每种信号都能回答不同的问题,结合起来则能全面掌握系统行为和用户体验。

使用可观测性库(Prometheus 客户端、OpenTelemetry SDK、供应商集成)时,请参阅该库的官方文档和代码示例,了解当前的 API 签名。

最佳实践摘要

  1. 使用结构化日志,采用 log/slog——生产服务必须输出结构化日志(JSON),而非自由格式的字符串
  2. 选择合适的日志级别——开发时使用 Debug,正常运行时使用 Info,状态降级时使用 Warn,需要关注的故障使用 Error
  3. 使用上下文记录日志——使用 slog.InfoContext(ctx, ...) 将日志与追踪关联起来
  4. 延迟指标优先使用 Histogram,而非 Summary——Histogram 支持服务端聚合和百分位查询。每个 HTTP 端点都必须具备延迟和错误率指标。
  5. 保持 Prometheus 标签基数较低——绝不可将无界值(用户 ID、完整 URL)用作标签值
  6. 跟踪百分位数(P50、P90、P99、P99.9),使用 Histogram 和 PromQL 中的 histogram_quantile()
  7. 在新项目中设置 OpenTelemetry 追踪——尽早配置 TracerProvider,然后随处添加跨度
  8. 为每个有意义的操作添加跨度——服务方法、数据库查询、外部 API 调用、消息队列操作
  9. 在所有地方传递上下文——上下文是跨服务边界传递 trace_id、span_id 和截止时间的载体
  10. 通过环境变量启用性能分析——无需重新部署即可开关 pprof 和持续性能分析
  11. 关联信号——将 trace_id 注入日志,使用 exemplars 将指标与追踪关联起来
  12. 功能只有具备可观测性才算完成——声明指标、添加适当的日志、创建跨度
  13. awesome-prometheus-alerts 提供约 500 条开箱即用的告警规则,按技术分类,用于基础设施和依赖项监控

交叉引用

  • → 有关唯一的错误处理规则,请参阅 samber/cc-skills-golang@golang-error-handling skill。
  • → 有关如何利用可观测性信号诊断生产问题,请参阅 samber/cc-skills-golang@golang-troubleshooting skill。
  • → 有关如何保护 pprof 端点以及避免在日志中记录个人身份信息,请参阅 samber/cc-skills-golang@golang-security skill。
  • → 有关如何跨服务边界传递跟踪上下文,请参阅 samber/cc-skills-golang@golang-context skill。
  • → 有关如何通过命令行使用 Prometheus 查询和探索 PromQL 表达式,请参阅 samber/cc-skills@promql-cli skill。
Go 1.26+:slog 多处理器

对于将日志简单分发到多个 slog 处理器的场景,优先使用标准库 slog.NewMultiHandler,再考虑添加第三方处理器组合依赖。

logger := slog.New(slog.NewMultiHandler(
    slog.NewJSONHandler(os.Stdout, nil),
    auditHandler,
))

仅当标准库的处理器组合无法满足需求时,才使用第三方 slog 处理器库。

五种信号

信号解答的问题工具使用时机
日志发生了什么?log/slog离散事件、错误、审计跟踪
指标数量有多少/速度有多快?Prometheus 客户端聚合测量、告警、SLO
跟踪时间花在哪里了?OpenTelemetry跨服务的请求流、延迟细分
性能剖析为什么速度慢/占用内存?pprof、PyroscopeCPU 热点、内存泄漏、锁竞争
真实用户监控(RUM)用户体验如何?PostHog、Segment产品分析、转化漏斗、会话回放

详细指南

每种信号都有专门的指南,提供完整的代码示例、配置模式和成本分析:

  • 结构化日志 — 说明结构化日志为何对大规模日志聚合至关重要。涵盖 log/slog 设置、日志级别(Debug/Info/Warn/Error)及其适用场景、使用 trace ID 关联请求、通过 slog.InfoContext 传递上下文、请求级属性、slog 生态系统(处理程序、格式化程序、中间件),以及从 zap/logrus/zerolog 迁移的策略。

  • 指标采集 — Prometheus 客户端设置和四种指标类型(表示变化率的 Counter、表示快照的 Gauge、用于聚合延迟的 Histogram)。深入介绍:为什么 Histogram 优于 Summary(支持服务端聚合和 histogram_quantile PromQL)、命名约定、将 PromQL 查询写在指标声明上方以便查找的注释约定、适用于生产环境的 PromQL 示例、多窗口 SLO 燃烧率告警,以及高基数标签问题(无界值,如用户 ID,为何会损害性能)。

  • 分布式追踪 — 何时以及如何使用 OpenTelemetry SDK 跨服务追踪请求流。涵盖 Span(创建、属性、状态记录)、用于 HTTP 插桩的 otelhttp 中间件、使用 span.RecordError() 记录错误、追踪采样(为何无法在大规模场景下采集所有数据)、跨服务边界传递追踪上下文,以及成本优化。

  • 性能分析 — 使用 pprof 按需进行性能分析(CPU、堆、goroutine、互斥锁、阻塞配置文件),包括如何在生产环境中启用、通过身份验证保护,以及无需重新部署即可通过环境变量切换。使用 Pyroscope 进行持续性能分析,持续了解性能状况。介绍各类性能分析的成本影响及缓解策略。

  • 真实用户监控 — 了解用户实际如何体验你的服务。涵盖产品分析(事件追踪、转化漏斗)、客户数据平台集成,以及关键合规要求:GDPR/CCPA 同意检查、数据主体权利(用户删除端点)和追踪隐私检查清单。还包括服务端事件追踪(PostHog、Segment)和身份标识键最佳实践。

  • 告警 — 主动发现问题。涵盖四个黄金信号(延迟、流量、错误、饱和度)、awesome-prometheus-alerts 按技术提供约 500 条可直接使用的规则、Go 运行时告警(goroutine 泄漏、GC 压力、OOM 风险)、严重级别,以及导致告警失效的常见错误(使用 irate 而不是 rate、未设置 for: 持续时间以避免告警抖动)。

  • Grafana 仪表板 — 用于 Go 运行时监控的预构建仪表板(堆分配、GC 暂停频率、goroutine 数量、CPU)。说明要安装的标准仪表板、如何针对你的服务进行自定义,以及每个仪表板分别能解答哪些运维问题。

关联信号

信号关联起来时最有用。日志中的 trace_id 可让你从一条日志跳转到完整的请求追踪。在指标上添加 exemplar,可将延迟峰值关联到导致该峰值的具体追踪。

日志 + 追踪:otelslog 桥接
import "go.opentelemetry.io/contrib/bridges/otelslog"

// Create a logger that automatically injects trace_id and span_id
logger := otelslog.NewHandler("my-service")
slog.SetDefault(slog.New(logger))

// Now every slog call with context includes trace correlation
slog.InfoContext(ctx, "order created", "order_id", orderID)
// Output includes: {"trace_id":"abc123", "span_id":"def456", "msg":"order created", ...}
指标 + 追踪:Exemplar
// When recording a histogram observation, attach the trace_id as an exemplar
// so you can jump from a P99 spike directly to the offending trace
obs := histogram.WithLabelValues("POST", "/orders")
if eo, ok := obs.(prometheus.ExemplarObserver); ok {
    eo.ObserveWithExemplar(duration, prometheus.Labels{"trace_id": traceID})
} else {
    obs.Observe(duration)
}

迁移旧版日志记录器

如果项目目前使用 zap、logrus 或 zerolog,请迁移到 log/slog。从 Go 1.21 起,它就是标准库中的日志记录器,API 稳定,生态系统也已转向统一使用它。继续使用第三方日志记录器只会增加需要维护的依赖,没有任何好处。

迁移策略:

  1. 使用 slog.SetDefault() 将 slog 设为新的日志记录器
  2. 迁移期间使用桥接处理器,通过现有日志记录器路由 slog 输出:samber/slog-zap、samber/slog-logrus、samber/slog-zerolog
  3. 逐步将所有 zap.L().Info(...) / logrus.Info(...) / log.Info().Msg(...) 调用替换为 slog.Info(...)
  4. 完成迁移后,移除桥接处理器和旧日志记录器依赖

可观测性完成标准

功能具备可观测性后,才能用于生产环境。将功能标记为完成之前,请确认:

  • 已声明指标 — 为操作和错误定义计数器,为延迟定义直方图,为饱和度定义仪表。每个指标变量的声明上方都有 PromQL 查询和告警规则注释。
  • 日志记录规范 — 使用 slog 记录结构化键值对,使用带上下文的变体(slog.InfoContext),日志中不包含个人身份信息(PII),错误必须选择记录或返回(绝不能两者都做)。
  • 已创建 Span — 每个服务方法、数据库查询和外部 API 调用都有带相关属性的 Span,并使用 span.RecordError() 记录错误。
  • 已配置仪表板和告警 — 将指标注释中的 PromQL 配置到 Grafana 仪表板和 Prometheus 告警规则中。常见基础设施依赖项的即用型告警规则可在 awesome-prometheus-alerts 获取。
  • 已跟踪 RUM 事件 — 在服务端跟踪关键业务事件(PostHog/Segment),身份标识键使用 user_id(而不是 email),跟踪前检查用户是否已同意。

常见错误

// ✗ Bad — log AND return (error gets logged multiple times up the chain)
if err != nil {
    slog.Error("query failed", "error", err)
    return fmt.Errorf("query: %w", err)
}

// ✓ Good — return with context, log once at the top level
if err != nil {
    return fmt.Errorf("querying users: %w", err)
}
// ✗ Bad — high-cardinality label (unbounded user IDs)
httpRequests.WithLabelValues(r.Method, r.URL.Path, userID).Inc()

// ✓ Good — bounded label values only
httpRequests.WithLabelValues(r.Method, routePattern).Inc()
// ✗ Bad — not passing context (breaks trace propagation)
result, err := db.Query("SELECT ...")

// ✓ Good — context flows through, trace continues
result, err := db.QueryContext(ctx, "SELECT ...")
// ✗ Bad — using Summary for latency (can't aggregate across instances)
prometheus.NewSummary(prometheus.SummaryOpts{
    Name:       "http_request_duration_seconds",
    Objectives: map[float64]float64{0.99: 0.001},
})

// ✓ Good — use Histogram (aggregatable, supports histogram_quantile)
prometheus.NewHistogram(prometheus.HistogramOpts{
    Name:    "http_request_duration_seconds",
    Buckets: prometheus.DefBuckets,
})

评论

支持 Markdown