
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 日常可观测性——生产环境中始终开启的信号。涵盖使用 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 签名。
最佳实践总结
- 使用结构化日志与
log/slog——生产服务必须输出结构化日志(JSON),而非自由格式字符串 - 选择正确的日志级别——Debug 用于开发,Info 用于正常操作,Warn 用于降级状态,Error 用于需要关注的故障
- 带上下文记录日志——使用
slog.InfoContext(ctx, ...)将日志与追踪关联 - 延迟指标优先使用 Histogram 而非 Summary——Histogram 支持服务端聚合和百分位查询。每个 HTTP 端点必须有延迟和错误率指标。
- 在 Prometheus 中保持低标签基数——切勿使用无界值(用户 ID、完整 URL)作为标签值
- 使用 Histogram +
histogram_quantile()在 PromQL 中跟踪百分位数(P50、P90、P99、P99.9) - 在新项目上设置 OpenTelemetry 追踪——尽早配置 TracerProvider,然后在各处添加 span
- 为每个有意义的操作添加 span——服务方法、数据库查询、外部 API 调用、消息队列操作
- 随处传播 context——context 是跨服务边界传递 trace_id、span_id 和截止时间的载体
- 通过环境变量启用性能分析——无需重新部署即可切换 pprof 和持续性能分析的开关
- 关联信号——将 trace_id 注入日志,使用 exemplar 将指标链接到追踪
- 一个功能在可观测之前不算完成——声明指标、添加适当的日志、创建 span
- 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_quantilePromQL)、命名约定、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)
}
迁移遗留日志库
如果项目当前使用 zap、logrus 或 zerolog,请迁移到 log/slog。它是自 Go 1.21 以来的标准库日志记录器,具有稳定的 API,并且生态系统已围绕它整合。继续使用第三方日志库意味着维护额外的依赖而没有任何好处。
迁移策略:
- 使用
slog.SetDefault()将slog添加为新的日志记录器 - 在迁移期间使用桥接处理器将 slog 输出路由到现有日志库:samber/slog-zap、samber/slog-logrus、samber/slog-zerolog
- 逐步将所有
zap.L().Info(...)/logrus.Info(...)/log.Info().Msg(...)调用替换为slog.Info(...) - 完全迁移后,移除桥接处理器和旧日志库依赖
可观测性完成定义
一个功能在可观测之前不算生产就绪。在标记功能完成之前,请验证:
- [ ] 指标已声明——用于操作/错误的计数器、用于延迟的直方图、用于饱和度的仪表盘。每个指标变量在其声明上方有 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,
})





