关于 golang-observability
面向 Go 编码 Agent 的可观测性技能,涵盖代码埋点、拉取请求审查,以及对日志、指标、追踪、性能分析和 RUM 的代码库审计。
主要功能
- 01为 Go 代码添加五类可观测信号
- 02审查拉取请求中的埋点变更
- 03并行审计各类信号的覆盖情况
- 04关联日志、指标与追踪
适用场景
- 01为 Go 服务添加指标和 Span
- 02审查埋点拉取请求的信号覆盖
- 03审计现有代码库的可观测性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-observability安装后在 Claude Code 中输入/golang-observability确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/alerting.mddashboards.mdlogging.mdmetrics.mdprofiling.mdrum.mdtracing.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 签名。
最佳实践摘要
- 使用结构化日志,采用
log/slog——生产服务必须输出结构化日志(JSON),而非自由格式的字符串 - 选择合适的日志级别——开发时使用 Debug,正常运行时使用 Info,状态降级时使用 Warn,需要关注的故障使用 Error
- 使用上下文记录日志——使用
slog.InfoContext(ctx, ...)将日志与追踪关联起来 - 延迟指标优先使用 Histogram,而非 Summary——Histogram 支持服务端聚合和百分位查询。每个 HTTP 端点都必须具备延迟和错误率指标。
- 保持 Prometheus 标签基数较低——绝不可将无界值(用户 ID、完整 URL)用作标签值
- 跟踪百分位数(P50、P90、P99、P99.9),使用 Histogram 和 PromQL 中的
histogram_quantile() - 在新项目中设置 OpenTelemetry 追踪——尽早配置 TracerProvider,然后随处添加跨度
- 为每个有意义的操作添加跨度——服务方法、数据库查询、外部 API 调用、消息队列操作
- 在所有地方传递上下文——上下文是跨服务边界传递 trace_id、span_id 和截止时间的载体
- 通过环境变量启用性能分析——无需重新部署即可开关 pprof 和持续性能分析
- 关联信号——将 trace_id 注入日志,使用 exemplars 将指标与追踪关联起来
- 功能只有具备可观测性才算完成——声明指标、添加适当的日志、创建跨度
- awesome-prometheus-alerts 提供约 500 条开箱即用的告警规则,按技术分类,用于基础设施和依赖项监控
交叉引用
- → 有关唯一的错误处理规则,请参阅
samber/cc-skills-golang@golang-error-handlingskill。 - → 有关如何利用可观测性信号诊断生产问题,请参阅
samber/cc-skills-golang@golang-troubleshootingskill。 - → 有关如何保护 pprof 端点以及避免在日志中记录个人身份信息,请参阅
samber/cc-skills-golang@golang-securityskill。 - → 有关如何跨服务边界传递跟踪上下文,请参阅
samber/cc-skills-golang@golang-contextskill。 - → 有关如何通过命令行使用 Prometheus 查询和探索 PromQL 表达式,请参阅
samber/cc-skills@promql-cliskill。
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"
// 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 稳定,生态系统也已转向统一使用它。继续使用第三方日志记录器只会增加需要维护的依赖,没有任何好处。
迁移策略:
- 使用
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(而不是 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,
})