go微服务自定义prometheus指标需五步全对:指标定义、显式注册(mustregister)、正确打点、合理标签设计、手动挂载/metrics端点;缺一步则grafana无数据。

Go 微服务里自定义 Prometheus 业务指标,不是“暴露 /metrics 就完事”,而是指标定义、注册、打点、标签设计、端点挂载五步全对才能进大盘——漏一步,Grafana 里就空着。
必须用 prometheus.MustRegister() 显式注册指标
定义一个 CounterVec 或 Gauge 只是创建内存对象,不调 MustRegister(),它压根不会出现在 /metrics 输出里。常见错误是只写变量声明、放在 handler 里动态注册,或多个包重复注册同名指标(直接 panic)。
- 所有自定义指标变量必须是包级变量(不能在函数内 new)
- 注册逻辑统一放
init()或main()开头,且只执行一次 - 若用了自定义 registry(如
reg := prometheus.NewRegistry()),后续必须用promhttp.HandlerFor(reg, promhttp.HandlerOpts{}),不能用默认promhttp.Handler()
Counter 和 Gauge 别混用:语义错,聚合就崩
订单总数、API 调用次数这类只增不减的累计量,必须用 Counter;当前活跃连接数、队列长度、内存使用量这类可增可减的瞬时值,必须用 Gauge。拿 Gauge 当 Counter 用(比如每次请求 Set(1)),会导致 P99 延迟统计失真、rate() 计算为负、Grafana 折线图乱跳。
-
Counter支持Inc()、Add(),不支持减,天然并发安全 -
Gauge支持Set()、Inc()、Dec(),goroutine 退出前记得Dec(),否则指标漂移 - 延迟分布必须用
Histogram,别用Gauge硬存单次耗时再算分位数——Histogram 内置桶和 count/sum,PromQL 才能跑histogram_quantile()
标签设计不当,Prometheus 会 OOM
业务维度标签(如 tenant_id、workflow_stage)能提升排查效率,但用户 ID、手机号、traceID 这类高基数字段塞进 label,会让指标数量爆炸,拖垮 Prometheus 存储和查询性能。
- 标签名强制小写+下划线,如
tenant_id,不能写tenant-id或TenantID - HTTP 路径中的动态 ID(如
/order/12345)必须归一化为/order/{id}再打点 - 用
CounterVec定义时,WithLabelValues("a", "b")的传参顺序必须和构造时[]string{"a","b"}完全一致,错一位就 panic - 高基数场景改用采样(如每千次记录一次)或聚合分类(如按
error_type分组计数,而非存完整 error message)
/metrics 端点必须手动挂载到 HTTP 路由
只调 MustRegister() 不挂路由,Prometheus 抓取返回 404 或空响应——90% 的“指标没数据”问题出在这步。Go 默认的 http.DefaultServeMux 不会自动绑定任何路径。
- 标准写法:
http.Handle("/metrics", promhttp.Handler()),且要在http.ListenAndServe()之前执行 - 如果用了 Gin/Echo 等框架,需显式把
promhttp.Handler()注册为 handler,不能依赖中间件自动注入 - 生产环境建议单独开 metrics 端口(如
:9091),避免业务流量冲击监控接口
真正难的不是写几行注册代码,而是想清楚“这个指标要回答什么问题”——比如“下单失败率突增”需要的是 http_requests_total{path="/order/create",code=~"5.."} / rate(http_requests_total{path="/order/create"}[5m]),这背后要求 path 和 code 标签都存在、且命名规范、基数可控。指标设计比编码更花时间,但一旦定下来,就别轻易改 label 名称或类型。











