Buffalo暴露/metrics端点必须单独起HTTP server绕过中间件栈,因promhttp.Handler()对响应头敏感;指标注册须收口至app.New()前一次执行;业务请求打点需自定义中间件,活跃连接数须用Gauge配对Inc/Dec。

Buffalo 框架暴露 /metrics 端点必须绕过默认路由中间件
Buffalo 默认用 app.Serve() 启动服务,所有请求都经过中间件链(如日志、CORS、CSRF),而 promhttp.Handler() 对响应头、状态码、Content-Type 极其敏感——一旦被中间件修改(比如加了 text/html 头或重定向),Prometheus 抓取就会失败,报错类似 expected 'text/plain; version=0.0.4' response, got 'text/html'。
正确做法是**单独起一个 HTTP server** 暴露指标,不走 Buffalo 的中间件栈:
- 新建 goroutine 调用
http.ListenAndServe(":9091", promhttp.Handler()),端口建议与业务端口分离(如:8080vs:9091) - 不要用
app.GET("/metrics", ...)或app.Use(...)注册,否则指标响应会被 Buffalo 中间件污染 - 如果必须复用同一端口,用
http.NewServeMux()显式挂载:mux.Handle("/metrics", promhttp.Handler()),再传给http.ListenAndServe(":8080", mux),但需确保mux不被 Buffalo 的app.Serve()覆盖
prometheus.MustRegister() 在 Buffalo 初始化阶段只能执行一次
Buffalo 的 actions/app.go 里常有多个 init() 函数(比如插件、中间件、模型包各自注册),若都调用 prometheus.MustRegister(httpReqCounter),启动时直接 panic:duplicate metrics collector registration attempted,且无法 recover。
解决方案是收口注册逻辑到单一入口:
- 在
actions/app.go顶部定义指标为包级变量:var httpReqCounter = prometheus.NewCounterVec(...) - 在
app.New()函数开头(即 Buffalo 应用实例化前)调用prometheus.MustRegister(httpReqCounter) - 避免在任何中间件、handler、model init 函数中重复注册;若用
promauto,确认只在一个地方 import 该包,因为promauto.NewCounterVec()内部已自动注册
HTTP 请求打点要用 Buffalo 中间件,不能依赖 promhttp.Handler()
promhttp.Handler() 只提供 Prometheus 自身的指标端点,**完全不统计你的业务请求**。要采集 http_requests_total 或 http_request_duration_seconds,必须写 Buffalo 中间件,在请求进入和退出时记录。
示例中间件写法(放在 actions/middleware.go):
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func MetricsMiddleware(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
start := time.Now()
err := next(c)
status := strconv.Itoa(c.Response().Status())
httpReqCounter.WithLabelValues(
c.Request().Method,
c.Request().URL.Path, // 注意:生产环境应模板化路径,如 "/api/users/{id}"
status,
).Inc()
httpReqDuration.WithLabelValues(
c.Request().Method,
status,
).Observe(time.Since(start).Seconds())
return err
}
}
然后在 app.Use(MetricsMiddleware) 中启用。注意:c.Request().URL.Path 是原始路径,含动态 ID,需结合 Buffalo 的 c.Param("id") 或路由名做归一化,否则 label 基数爆炸。
Gauge 统计活跃连接数时,goroutine 退出必须调用 .Dec()
常见错误是用 prometheus.NewCounter() 记“当前在线用户”,但 Counter 只能增不能减——goroutine 结束后指标卡在高位,监控完全失真。
正确做法是用 Gauge 并严格配对 .Inc()/.Dec():
- 定义:
var activeConnections = prometheus.NewGauge(prometheus.GaugeOpts{Name: "active_connections"}) - 在 WebSocket handler 或长连接入口处:
activeConnections.Inc() - 在 defer 中确保退出时扣减:
defer activeConnections.Dec() - 若 handler 可能 panic,需包裹 recover:
defer func() { if r := recover(); r != nil { activeConnections.Dec(); panic(r) } }()
这个配对逻辑极易遗漏,尤其在多层封装或异步回调中——一旦漏掉一次 .Dec(),指标就永远不准,且无日志提示,排查成本极高。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










