自定义指标需声明为包级变量并仅注册一次,且gin路由须显式挂载promhttp.handler();选对指标类型、规范标签命名与校验、确保命名与查询语义一致。

自定义指标在 Gin 中根本不会出现在 /metrics 里,除非你把它“注册”且“挂对地方”——90% 的问题出在这两步,而不是代码写得不够漂亮。
自定义指标必须声明为包级变量并只注册一次
把 prometheus.NewCounterVec 或 promauto.NewGaugeVec 写在 handler 函数里,每次请求都新建+注册,会直接 panic:duplicate metrics collector registration。Prometheus 客户端不支持 recover,进程就挂了。
- 所有指标变量(如
httpReqCounter、orderStatusGauge)必须是包级变量,不能是局部变量或函数内定义 - 注册逻辑统一放在
init()或main()开头,只执行一次;用prometheus.MustRegister(),别漏掉 - 如果用了
promauto.NewCounterVec,它内部已自动注册,不用再调MustRegister();但手写的NewCounterVec必须显式注册 - 多个包 import 同一个 metrics 文件?小心重复
MustRegister()—— 用init()做 guard,或改用单例模式初始化
Gin 路由必须显式挂载 promhttp.Handler()
http.DefaultServeMux 不会自动绑定 /metrics,Gin 的 router 更不会猜你要暴露什么。不挂,Prometheus 就拉不到任何自定义指标,只看到默认的 go_* 和 process_*。
- 正确写法:
router.GET("/metrics", gin.WrapH(promhttp.Handler())) - 路径必须和 Prometheus 配置里的
scrape_configs.job.metrics_path完全一致(默认是/metrics) - 如果用了自定义注册器(如
reg := prometheus.NewRegistry()),必须用promhttp.HandlerFor(reg, promhttp.HandlerOpts{}),否则你的指标不会出现 - 别在
promhttp.Handler()外再包一层自定义http.HandlerFunc—— 这会导致Accept头协商失败、gzip 不生效,甚至返回406 Not Acceptable
选错指标类型或滥用标签会让监控失真甚至崩溃
用 Counter 记当前在线用户数?用 Gauge 当累计请求数?这两类误用在生产环境极难定位,但后果明确:前者永远只涨不跌,后者聚合语义完全错乱。
-
Counter只适用于累计值(如http_requests_total),不能用于状态快照 -
GaugeVec才适合带维度的状态值(如app_order_status_total{status="paid",channel="ios"}),裸NewGauge无法区分维度 - 标签名必须全小写、下划线分隔(
tenant_id✅,tenant-id❌),值里别塞user_id="123456789"这种高基数字段,否则 Prometheus 存储压力暴增 -
WithLabelValues("paid", "ios")的参数顺序必须和[]string{"status", "channel"}定义顺序严格一致,错一位就 panic
更新指标值时容易忽略 label 校验和并发安全
在 handler 里直接调 gauge.WithLabelValues(status, channel).Set(val) 看似简单,但 status 或 channel 是从 query 参数取的,一旦传入空字符串、非法字符或超长值,就会触发 inconsistent label cardinality panic。
- 建议加白名单校验:
if _, ok := validStatuses[status]; !ok { return } - 高频更新场景避免反复调
WithLabelValues—— 它内部有 map 查找开销;可预缓存prometheus.Labels或子指标对象 -
GaugeVec用Set()设绝对值,用Add()设差值;别混用Inc()/Dec(),它不是Counter - 更新操作本身是线程安全的,但 label 构造过程不是 —— 所以校验必须在
WithLabelValues之前完成
最常被跳过的细节是:指标命名要和 Adapter 查询语句中的 seriesQuery 完全一致(包括大小写、下划线),少一个字符 HPA 就查不到;而 rate() 包裹才是 HPA 能理解的速率,不是原始 Counter 值。











