go标准http.handler本身无监控能力,必须显式注册指标并暴露/metrics端点;仅包装handler不采集请求量、状态码等指标,需配合countervec、histogramvec及中间件逻辑实现完整监控。

为什么直接用 http.Handler 无法自动暴露指标
Go 的标准 http.Handler 接口本身不带任何监控能力,Prometheus 需要你显式收集并暴露指标(比如请求计数、延迟、状态码分布)。直接把原 handler 丢给 http.ServeMux 或 http.ListenAndServe,Prometheus 客户端根本不知道该抓什么数据。
所以关键不是“包装”,而是「在请求生命周期中插入指标采集逻辑」——通常通过中间件方式实现,即返回一个新的 http.Handler,它内部调用原始 handler,并在前后记录指标。
用 promhttp.InstrumentHandlerDuration 包装时要注意的三件事
这是最常用的函数之一,但它只统计处理时长,不自动记录请求量或状态码。容易误以为“加了就全有了”,结果发现 http_requests_total 指标压根没出现。
-
promhttp.InstrumentHandlerDuration必须配合一个prometheus.HistogramVec使用,且该 histogram 的labelNames至少包含"handler"(默认),否则会 panic - 它不会修改响应状态码,但如果你在 handler 内部提前写 header 或调用
w.WriteHeader(),而后续又写 body,可能触发http: multiple response.WriteHeader calls—— 这类错误会让指标统计中断,甚至 panic - 它不捕获 panic;如果被包装的 handler panic,HTTP 连接会直接断开,
Durationhistogram 不会记录那次请求,但你的服务可能已崩溃
一个真正可用的包装函数示例
下面这个函数同时记录请求数、延迟、状态码,且能 recover panic:
func WithPrometheusMetrics(next http.Handler) http.Handler {
counter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests.",
},
[]string{"handler", "method", "code"},
)
duration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds.",
},
[]string{"handler", "method", "code"},
)
prometheus.MustRegister(counter, duration)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := r.URL.Path // 或从 context/route 中提取更精确的 handler 名
start := time.Now()
// 包装响应 writer 以捕获状态码
wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(wrapped, r)
code := strconv.Itoa(wrapped.statusCode)
method := r.Method
counter.WithLabelValues(route, method, code).Inc()
duration.WithLabelValues(route, method, code).Observe(time.Since(start).Seconds())
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
注意:route 这里用的是 r.URL.Path,实际项目中建议从路由框架(如 chi、gorilla/mux)的 context 中取真实 handler 名,否则所有路径都算作不同 label,cardinality 爆炸。
别忘了暴露 /metrics 端点
光包装 handler 不够,你还得让 Prometheus 能 scrape 到指标。必须单独注册一个 handler:
http.Handle("/metrics", promhttp.Handler())
常见错误:把它和主路由注册在同一个 mux 里,但没注意顺序 —— 如果你先注册了 http.HandleFunc("/", ...) 这种兜底路由,/metrics 就永远收不到请求。确保 /metrics 注册在最前面,或者用独立的 mux 绑定到另一个端口(更推荐,避免干扰主流量)。
还有个坑:promhttp.Handler() 默认不压缩响应,大指标集可能拖慢 scrape;生产环境建议加一层 gzip wrapper,但注意不要对非 text/plain 响应误压缩。











