package metrics import ( "strconv" "time" "github.com/labstack/echo/v5" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( httpRequests = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "http_requests_total", Help: "HTTP requests by method, route and status code.", }, []string{"method", "route", "code"}) httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "HTTP request latency by method and route. Song downloads include the transfer.", // Up to 30 s, since songs can take long to send to slow clients. Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 30}, }, []string{"method", "route"}) ) // unmatchedRoute labels requests that matched no route, so raw URLs never become labels. const unmatchedRoute = "unmatched" // Middleware records the count and latency of requests per route. The route label is the // registered pattern, like /api/v1/songs/:uuid, never the URL with its IDs and query. // Register it outside Recover, so requests that panic are counted as 500. func Middleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { start := time.Now() err := next(c) route := c.Path() if route == "" { route = unmatchedRoute } _, status := echo.ResolveResponseStatus(c.Response(), err) method := c.Request().Method httpRequests.WithLabelValues(method, route, strconv.Itoa(status)).Inc() httpDuration.WithLabelValues(method, route).Observe(time.Since(start).Seconds()) return err } } }