根本原因是未挂载promhttp.handler()到路由,gin不自动注册监控端点;mustregister仅注册指标不暴露路径,需用gin.wraph(promhttp.handler())显式挂载/metrics路由。

为什么/metrics返回404或空响应
根本原因不是指标没定义,而是没把 promhttp.Handler() 挂到 HTTP 路由上。Gin 不会自动注册任何监控端点,prometheus.MustRegister() 只是把指标注册进默认注册器,不等于暴露路径。
必须显式挂载:
- 用
gin.WrapH(promhttp.Handler())包装后注册到路由,比如r.GET("/metrics", gin.WrapH(promhttp.Handler())) - 路径名(如
/metrics)要和 Prometheus 配置里的scrape_configs.metrics_path完全一致 - 别写成
r.Handle("GET", "/metrics", ...)—— Gin 的Handle方法不支持 HTTP 方法过滤,要用GET/Any等明确方法
自定义指标 panic:重复注册或字段缺失
两种高频 panic 场景:一是同名指标多次 MustRegister(),二是初始化 NewCounterVec 时漏掉 Name 或 Help。
正确做法:
-
Name必须只含小写字母、数字、下划线、冒号;Help不能为空字符串,例如:prometheus.CounterOpts{Name: "http_requests_total", Help: "Total HTTP requests"} - 所有指标变量声明为包级变量,注册统一放在
init()函数里,避免 handler 中重复 new + register - 若用
promauto.NewCounterVec,它内部自动注册,不用再调MustRegister();但若用prometheus.NewCounterVec,就必须手动注册一次
Gin 中统计活跃连接数,该用 Counter 还是 Gauge
用 Counter 会永远上涨,完全失真。活跃连接数是瞬时值,必须用 Gauge。
关键细节:
- 每个新连接 goroutine 启动时调
gauge.Inc(),退出前务必调gauge.Dec() - 别在 handler 里直接
gauge.Set(1)—— 这会覆盖其他连接状态,失去计数意义 - 如果连接生命周期跨多个 goroutine(比如 WebSocket 升级后交由另一个协程管理),Dec() 必须在真正关闭连接的那处执行,不能靠 defer 在入口 goroutine 里写
带业务标签的指标怎么防基数爆炸
标签(label)加得随意,会导致时间序列数量指数级增长,撑爆 Prometheus 内存和查询性能。
安全实践:
- 路径中动态 ID(如
/user/123)必须归一化为/user/{id},再作为 label 值传入.WithLabelValues("user/{id}") - 用户 ID、手机号、traceID 这类高基数字段,禁止直接打标;改用
CounterVec按错误类型、状态码等低基数维度分类 - 标签键名强制小写+下划线,如
tenant_id,不能写TenantID或tenant-id,否则不同写法会被当成不同指标
最易被忽略的一点:指标注册和路由挂载必须发生在 HTTP server 启动之前。如果先 router.Run() 再补挂 /metrics,服务已监听,新路由不会生效——重启才是唯一解。











