直接用 promhttp.handler() 会丢指标,因其仅暴露默认注册器中的指标,未显式注册的业务指标不会被采集;需调用 prometheus.mustregister() 注册,且避免初始化顺序问题和注册器错配。

为什么直接用 promhttp.Handler() 会丢指标
因为默认的 promhttp.Handler() 只暴露注册在 prometheus.DefaultRegisterer 上的指标,而你新定义的 Counter、Gauge 如果没显式注册,Prometheus 抓取时就是空的。常见现象是:HTTP 路由能访问,返回 200,但 body 里只有 go_* 和 process_* 这类默认指标,你的业务指标完全不出现。
- 必须调用
prometheus.MustRegister()或reg.MustRegister()(如果你用了自定义注册器) - 不要在
init()里定义指标却忘了注册——Go 的包初始化顺序不可控,容易漏 - 如果用了多个注册器(比如为不同 endpoint 隔离指标),注意
promhttp.HandlerFor()要传对那个注册器,而不是默认的
如何安全地暴露 HTTP handler 并支持热重载配置
直接用 http.ListenAndServe(":9100", nil) + 默认 multiplexer 很危险:一旦其他包也调用 http.HandleFunc()(比如某些 SDK),就可能污染你的 /metrics 路由,甚至导致 panic。更麻烦的是,配置变更(如重载采集间隔、目标地址)需要重启进程,无法平滑更新。
- 用独立的
http.ServeMux,只挂你自己的路由:mux := http.NewServeMux() mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) - 把采集逻辑封装成函数,用
time.Ticker触发,指标更新走Set()/Inc()等方法,而非重建指标对象 - 配置变更通过 channel 或 atomic.Value 传递,避免锁竞争;重载时清空旧指标用
reg.Unregister()(注意:只能解注册 *之前注册过的*,不能传新对象)
Desc 构造和标签(label)设计踩坑点
错误写法:prometheus.NewCounterVec(prometheus.CounterOpts{...}, []string{"job", "instance"}) 看似正常,但如果你在采集时传了空字符串或重复 label 名,Prometheus 服务端会拒绝该样本(invalid metric name or label 错误出现在 target 状态页)。更隐蔽的问题是 label 值动态拼接时未做 sanitize,比如含空格、斜杠、大写字母。
- label 名必须是 ASCII 字母、数字或下划线,且不能以数字开头;值必须是 UTF-8 字符串,但建议只用字母、数字、下划线、连字符
- 避免在 label 中塞高基数字段(如 request_id、user_email),会导致 cardinality 爆炸,Prometheus 内存暴涨甚至 OOM
- 用
prometheus.Labels{"env": "prod", "role": "api"}传参,别手写 map[string]string —— 类型安全能提前发现 key 冲突
本地调试时 curl 拿不到指标?检查这几个地方
最常卡在这儿:代码跑起来了,curl http://localhost:9100/metrics 返回 404 或空响应,不是 exporter 写错了,而是 HTTP 层根本没挂上。
立即学习“go语言免费学习笔记(深入)”;
- 确认
http.ListenAndServe()的第二个参数不是nil,而是你构造的mux;nil表示用默认全局 mux,但你没往那里注册 handler - 检查是否监听了
127.0.0.1:9100却用localhost访问(IPv6 fallback 有时失败),改用curl http://127.0.0.1:9100/metrics直接验证 - 加一行日志:
log.Println("Exporter listening on :9100"),确保进程没 panic 在启动阶段;很多新手在MustRegister()时报错后静默退出
指标注册和 HTTP 暴露是两层事,分开验证才不容易糊弄自己。label 设计比写采集逻辑还关键,线上崩一次 cardinality 就够喝一壶。










