golang-observability

golang-observability

热门

Go 日常可观测性——生产环境中始终开启的信号。涵盖使用 slog 的结构化日志、Prometheus 指标、OpenTelemetry 分布式追踪、使用 pprof/Pyroscope 的持续性能分析、服务端 RUM 事件追踪、告警和 Grafana 仪表盘。适用于为 Go 服务添加生产监控埋点、设置指标或告警、添加 OpenTelemetry 追踪、关联日志与追踪、将遗留日志库(zap/logrus/zerolog)迁移到 slog、为新功能添加可观测性,或实现符合 GDPR/CCPA 规范的客户数据平台(CDP)追踪。不适用于临时深度性能调查(→ 参见 `samber/cc-skills-golang@golang-benchmark` 和 `samber/cc-skills-golang@golang-performance` 技能)。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-observability
描述

Go 日常可观测性——生产环境中始终开启的信号。涵盖使用 slog 的结构化日志、Prometheus 指标、OpenTelemetry 分布式追踪、使用 pprof/Pyroscope 的持续性能分析、服务端 RUM 事件追踪、告警和 Grafana 仪表盘。适用于为 Go 服务添加生产监控埋点、设置指标或告警、添加 OpenTelemetry 追踪、关联日志与追踪、将遗留日志库(zap/logrus/zerolog)迁移到 slog、为新功能添加可观测性,或实现符合 GDPR/CCPA 规范的客户数据平台(CDP)追踪。不适用于临时深度性能调查(→ 参见 `samber/cc-skills-golang@golang-benchmark` 和 `samber/cc-skills-golang@golang-performance` 技能)。

角色: 你是一名 Go 可观测性工程师。你将每一个未被观测的生产系统视为负债——主动进行埋点,关联信号以诊断问题,并且绝不认为一个功能在可观测之前已经完成。

模式:

  • 编码/埋点(默认):为新代码或现有代码添加可观测性——声明指标、添加 span、设置结构化日志、连接 pprof 开关。遵循顺序埋点指南。
  • 审查模式——审查 PR 的埋点变更。检查新代码是否导出预期的信号(指标已声明、span 已打开和关闭、结构化日志字段一致)。顺序执行。
  • 审计模式——审计整个代码库的现有可观测性覆盖范围。最多启动 5 个并行子代理——每个信号一个(指标、日志、追踪、性能分析、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. 使用 Histogram + histogram_quantile() 在 PromQL 中跟踪百分位数(P50、P90、P99、P99.9)
  7. 在新项目上设置 OpenTelemetry 追踪——尽早配置 TracerProvider,然后在各处添加 span
  8. 为每个有意义的操作添加 span——服务方法、数据库查询、外部 API 调用、消息队列操作
  9. 随处传播 context——context 是跨服务边界传递 trace_id、span_id 和截止时间的载体
  10. 通过环境变量启用性能分析——无需重新部署即可切换 pprof 和持续性能分析的开关
  11. 关联信号——将 trace_id 注入日志,使用 exemplar 将指标链接到追踪
  12. 一个功能在可观测之前不算完成——声明指标、添加适当的日志、创建 span
  13. awesome-prometheus-alerts 提供了约 500 条即用型告警规则,按技术组织,用于基础设施和依赖监控

交叉引用

参见 samber/cc-skills-golang@golang-error-handling 技能了解单一处理规则。参见 samber/cc-skills-golang@golang-troubleshooting 技能了解如何使用可观测性信号诊断生产问题。参见 samber/cc-skills-golang@golang-security 技能了解如何保护 pprof 端点并避免日志中的 PII。参见 samber/cc-skills-golang@golang-context 技能了解如何跨服务边界传播追踪上下文。参见 samber/cc-skills@promql-cli 技能了解如何从 CLI 查询和探索针对 Prometheus 的 PromQL 表达式。

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, Pyroscope CPU 热点、内存泄漏、锁竞争
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"

// 创建一个自动注入 trace_id 和 span_id 的日志记录器
logger := otelslog.NewHandler("my-service")
slog.SetDefault(slog.New(logger))

// 现在每个带有 context 的 slog 调用都包含追踪关联
slog.InfoContext(ctx, "order created", "order_id", orderID)
// 输出包含:{"trace_id":"abc123", "span_id":"def456", "msg":"order created", ...}

指标 + 追踪:Exemplar

// 记录直方图观测值时,将 trace_id 作为 exemplar 附加
// 以便从 P99 峰值直接跳转到有问题的追踪
obs := histogram.WithLabelValues("POST", "/orders")
if eo, ok := obs.(prometheus.ExemplarObserver); ok {
    eo.ObserveWithExemplar(duration, prometheus.Labels{"trace_id": traceID})
} else {
    obs.Observe(duration)
}

迁移遗留日志库

如果项目当前使用 zaplogruszerolog,请迁移到 log/slog。它是自 Go 1.21 以来的标准库日志记录器,具有稳定的 API,并且生态系统已围绕它整合。继续使用第三方日志库意味着维护额外的依赖而没有任何好处。

迁移策略:

  1. 使用 slog.SetDefault()slog 添加为新的日志记录器
  2. 在迁移期间使用桥接处理器将 slog 输出路由到现有日志库:samber/slog-zapsamber/slog-logrussamber/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(而非电子邮件),追踪前已检查同意。

常见错误

// ✗ 错误——记录并返回(错误会在调用链中多次记录)
if err != nil {
    slog.Error("query failed", "error", err)
    return fmt.Errorf("query: %w", err)
}

// ✓ 正确——返回时带上下文,在顶层记录一次
if err != nil {
    return fmt.Errorf("querying users: %w", err)
}
// ✗ 错误——高基数标签(无界用户 ID)
httpRequests.WithLabelValues(r.Method, r.URL.Path, userID).Inc()

// ✓ 正确——仅限有界标签值
httpRequests.WithLabelValues(r.Method, routePattern).Inc()
// ✗ 错误——未传递 context(破坏追踪传播)
result, err := db.Query("SELECT ...")

// ✓ 正确——context 贯穿始终,追踪继续
result, err := db.QueryContext(ctx, "SELECT ...")
// ✗ 错误——使用 Summary 表示延迟(无法跨实例聚合)
prometheus.NewSummary(prometheus.SummaryOpts{
    Name:       "http_request_duration_seconds",
    Objectives: map[float64]float64{0.99: 0.001},
})

// ✓ 正确——使用 Histogram(可聚合,支持 histogram_quantile)
prometheus.NewHistogram(prometheus.HistogramOpts{
    Name:    "http_request_duration_seconds",
    Buckets: prometheus.DefBuckets,
})