You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Golang中为带路径参数的URL采集Prometheus指标

解决Golang中prom-client对动态参数URL的指标合并问题

当你的Golang应用使用prom-client统计HTTP请求时长时,带动态userid的URL会被当作独立条目生成指标,导致指标条目冗余。要将所有/app/{userid}/details格式的请求合并到同一个指标下,核心思路是用路由模板或规范化后的统一路径替代原始请求路径作为path标签的值。

以下是几种常见场景的实现方案:

1. 使用gorilla/mux路由库的情况

gorilla/mux支持从请求上下文获取路由模板,直接用它作为指标的path标签:

import (
    "net/http"
    "strconv"
    "time"
    "github.com/gorilla/mux"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// 定义请求时长直方图指标
var httpRequestDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "Duration of HTTP requests.",
        Buckets: prometheus.DefBuckets,
    },
    []string{"method", "path", "status_code"},
)

func init() {
    prometheus.MustRegister(httpRequestDuration)
}

// 自定义中间件捕获路由模板并记录指标
func prometheusMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // 包装ResponseWriter以捕获响应状态码
        rw := &responseWriter{w, http.StatusOK}

        next.ServeHTTP(rw, r)

        // 获取路由模板路径
        route := mux.CurrentRoute(r)
        pathTemplate, err := route.GetPathTemplate()
        if err != nil {
            // 获取失败时 fallback 到原始路径
            pathTemplate = r.URL.Path
        }

        // 记录指标
        duration := time.Since(start).Seconds()
        httpRequestDuration.WithLabelValues(
            r.Method,
            pathTemplate,
            strconv.Itoa(rw.statusCode),
        ).Observe(duration)
    })
}

// 自定义ResponseWriter实现
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// 路由初始化示例
func main() {
    r := mux.NewRouter()
    r.Use(prometheusMiddleware)

    // 注册带动态参数的路由
    r.HandleFunc("/app/{userid}/details", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("User details retrieved"))
    })

    // 暴露metrics端点
    r.Handle("/metrics", promhttp.Handler())

    http.ListenAndServe(":8080", r)
}

2. 使用Gin框架的情况

Gin的Context提供了FullPath()方法,可直接获取路由模板,实现更简洁:

import (
    "strconv"
    "time"
    "github.com/gin-gonic/gin"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var httpRequestDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "Duration of HTTP requests.",
        Buckets: prometheus.DefBuckets,
    },
    []string{"method", "path", "status_code"},
)

func init() {
    prometheus.MustRegister(httpRequestDuration)
}

// Gin中间件实现
func prometheusMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()

        // 获取路由模板
        pathTemplate := c.FullPath()
        if pathTemplate == "" {
            pathTemplate = c.Request.URL.Path
        }

        // 计算时长并记录指标
        duration := time.Since(start).Seconds()
        statusCode := strconv.Itoa(c.Writer.Status())
        httpRequestDuration.WithLabelValues(
            c.Request.Method,
            pathTemplate,
            statusCode,
        ).Observe(duration)
    }
}

func main() {
    r := gin.Default()
    r.Use(prometheusMiddleware())

    // 注册路由
    r.GET("/app/:userid/details", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "User details"})
    })

    // 暴露metrics端点
    r.GET("/metrics", gin.WrapH(promhttp.Handler()))

    r.Run(":8080")
}

3. 无路由库时的正则替换方案

如果没有使用成熟的路由库,可以用正则表达式替换动态参数部分,生成统一路径:

import (
    "net/http"
    "regexp"
    "strconv"
    "time"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "Duration of HTTP requests.",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "path", "status_code"},
    )
    // 匹配/app/xxx/details格式的正则
    userPathRegex = regexp.MustCompile(`^/app/[^/]+/details$`)
)

func init() {
    prometheus.MustRegister(httpRequestDuration)
}

// 路径规范化函数
func normalizePath(path string) string {
    if userPathRegex.MatchString(path) {
        return "/app/{userid}/details"
    }
    return path
}

// 中间件实现
func prometheusMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{w, http.StatusOK}

        next.ServeHTTP(rw, r)

        // 规范化路径
        normalizedPath := normalizePath(r.URL.Path)

        duration := time.Since(start).Seconds()
        httpRequestDuration.WithLabelValues(
            r.Method,
            normalizedPath,
            strconv.Itoa(rw.statusCode),
        ).Observe(duration)
    })
}

// 同之前的responseWriter定义
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/app/", func(w http.ResponseWriter, r *http.Request) {
        // 简单处理动态路径请求
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("User details"))
    })

    // 包装中间件
    handler := prometheusMiddleware(mux)
    http.Handle("/", handler)
    http.Handle("/metrics", promhttp.Handler())

    http.ListenAndServe(":8080", nil)
}

以上方案都会将所有/app/xxx/details格式的请求合并到path="/app/{userid}/details"的指标条目下,避免指标爆炸。

内容的提问来源于stack exchange,提问作者Priyank Chheda

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 02:12:36