必须弃用 iris-prometheus 默认中间件,因其直方图分桶不合理致P99失真、路径未归一化引发内存爆炸、缺service_name/env等关键标签无法下钻。

不能直接用 iris-prometheus 中间件上生产,它默认直方图分桶不合理、路径未归一化、缺关键标签,三处硬伤会导致 P99 失真、内存爆炸、无法下钻。
为什么 iris-prometheus 默认中间件必须弃用
它暴露的 http_request_duration_seconds 直方图用的是 prometheus.DefBuckets([0.005, 0.01, 0.025, ...]),而你的 API 实际 P95 耗时在 100ms 左右,结果指标显示 P99 = 0.005s,完全失真;原始路径如 /api/v1/users/123 直接当 path 标签值写入,user_id=123 这类动态段会无限生成新时间序列,几分钟吃光内存;不支持注入 service_name、env 等必需标签,Grafana 里根本没法按服务或环境切片。
必须手动注册指标 + 显式打点
iris-contrib/prometheus 并非官方维护,但社区已收敛出稳定模式:指标定义为包级变量 → 启动早期(app.Run() 前)调用 prometheus.MustRegister() 注册一次 → 中间件只负责 .Inc() 或 .Observe(),绝不重复注册。
- 导入
github.com/prometheus/client_golang/prometheus和github.com/prometheus/client_golang/prometheus/promhttp - 定义
httpReqCounter用CounterVec,标签含method、path、status;定义httpReqDuration用Histogram,显式传入合理分桶:Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5} - 所有指标注册必须在
main()或初始化函数中完成,禁止出现在 handler 或中间件函数体内
路径归一化和标签白名单怎么做
c.Request().URL.Path 返回的是带真实 ID 的原始路径,不能直接塞进 label。必须用 c.Params 遍历参数名,结合 c.Route().Path(返回 /api/v1/users/:id 这类模板)做字符串替换,得到 /api/v1/users/{id}。
- 敏感 query 参数(如
auth_token、callback_url)必须白名单过滤,不在名单里的 key 全部丢弃,否则 label 组合爆炸 - 封装
normalizePath(c *iris.Context)工具函数,统一处理所有路由;示例打点:httpReqCounter.WithLabelValues(c.Method(), normalizePath(c), strconv.Itoa(c.GetStatusCode())).Inc() - 别在中间件里拼 JSON 或做耗时格式化——只存原始数值,用原子操作更新,比如
metrics.TotalRequests.Add(1)
如何正确暴露 /metrics 端点
不能用 app.Get("/metrics", ...) 或 iris.StaticWeb,它们绕过中间件链且不设 Content-Type。必须用 promhttp.Handler(),并确保它绑定到 HTTP 服务的底层 http.ServeMux 或等效机制。
- 推荐方式:
app.Any("/metrics", iris.FromStd(promhttp.Handler())),这是 Iris 官方推荐的 std 包桥接方式 - 若用自定义 registry(如避免污染全局),需显式传入:
promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) - 务必加基础鉴权,比如只允许内网 IP 访问,或用
iris.BasicAuth限制账号,严禁暴露在公网
最易忽略的是:指标变量若定义在函数内、没被全局引用持有,Go 编译器可能将其 GC 回收,导致 /metrics 返回空内容且无报错;还有 Counter 误用于活跃连接数——它不能减,得换 Gauge 并确保每个 goroutine 退出前调用 .Dec()。











