生产环境golang服务监控需闭环指标采集、传输、存储、告警、可视化五环节;必须用独立registry暴露/metrics,透传opentelemetry context,结构化日志带traceid,/healthz检查强依赖并缓存结果。

生产环境 Golang 服务监控不是“加个 /metrics 就完事”,而是指标采集、传输、存储、告警、可视化五个环节必须闭环,任一环缺失都会导致故障响应延迟或误判。
如何暴露符合 Prometheus 规范的 /metrics 端点
这是最基础也最容易出错的一环。很多人直接用 promhttp.Handler() 暴露默认 Go 运行时指标,但生产服务必须补充业务指标,且不能污染默认命名空间。
- 必须调用
prometheus.NewRegistry()创建独立注册器,避免与prometheus.DefaultRegisterer冲突(尤其在测试或嵌入式场景下) - 自定义指标命名需带业务前缀,如
user_login_total而非login_total,防止多服务混用时标签冲突 -
Counter和Histogram必须在init()或服务启动早期注册,否则promhttp.Handler()无法识别 - 不要在 HTTP handler 内部动态创建指标(如按用户 ID 新建
Gauge),会引发内存泄漏和 cardinality 爆炸
示例关键片段:
reg := prometheus.NewRegistry()
reg.MustRegister(
prometheus.NewGoCollector(),
prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}),
userLoginTotal, // 已提前定义并注册的 CounterVec
apiDuration, // 已提前定义并注册的 HistogramVec
)
然后绑定到路由:http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
为什么 http.Request.Context() 必须透传 OpenTelemetry Span
微服务调用链断裂的主因不是没埋点,而是上下文丢失。Golang 的 context.Context 不自动跨 goroutine 传播,也不自动注入 HTTP Header,不手动处理就会导致 Jaeger/Tempo 中只看到单跳 Span。
- 用
otelhttp.NewHandler()包裹你的http.ServeMux,它会自动从traceparentHeader 解析并创建 Span - 跨 goroutine(如异步日志、后台任务)时,必须显式用
ctx = trace.ContextWithSpan(ctx, span)注入,再传给新 goroutine - 调用下游 HTTP 服务前,必须用
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))注入 Header,否则链路中断 - 别依赖
context.Background()启动子 Span——它没有 traceID,所有 Span 都变成根 Span
生产日志必须结构化且带 traceID 字段
非结构化日志在 Loki 或 Grafana 中无法关联追踪,fmt.Printf 或 log.Println 输出的纯文本等于放弃可观测性。
- 用
zap替代标准库log,初始化时启用zap.AddCaller()和zap.AddStacktrace(zapcore.WarnLevel) - 所有日志 Entry 必须通过
logger.With(zap.String("trace_id", traceID))注入 traceID,该值从otel.SpanContext().TraceID().String()获取 - 禁止在日志中拼接字符串(如
"user " + uid + " failed"),必须用字段方式:logger.Error("user login failed", zap.String("user_id", uid), zap.Error(err)) - 日志级别要严格:
Error仅用于真正需要人工介入的异常;Warn用于可恢复但需关注的情况(如重试成功);Info限于关键路径(如请求进入、认证完成、DB 查询返回)
健康检查接口 /healthz 不能只返回 {"healthy": true}
这个端点是 Kubernetes Liveness/Readiness Probe 的唯一依据,返回假阳性会引发滚动更新失败或流量误切。
- 必须检查**所有强依赖**:数据库连接、Redis 连通性、下游核心服务 HTTP 可达性(带超时,如
500ms) - 检查逻辑要缓存结果,避免每次请求都发网络调用(可用
sync.Map存最后成功时间戳,10 秒内重复检查直接返回缓存) - 返回体必须包含失败详情,例如:
{"healthy": false, "reasons": ["redis: dial timeout", "db: no route to host"]},方便运维快速定位 - 不要在
/healthz中执行耗时操作(如全量配置重载、大表扫描),它应在≤200ms内返回
真正难的不是写代码,而是让每个指标有明确的 SLO 定义、每条日志能回答“谁在什么条件下触发了什么行为”、每次 trace 能还原真实用户路径——这些没法靠工具自动生成,得在写第一行 handler 时就想清楚。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











