Skip to content

Instrument HTTP requests

Metrics.HTTPMiddleware records the RED signals — Rate, Errors, Duration — for every HTTP request, on the same registry that serves /metrics. Labels use the route template, never the raw URL, so cardinality stays bounded.

m, err := metrics.New(metrics.WithMiddleware(authMiddleware))
if err != nil { return err }

httpMetrics, err := m.HTTPMiddleware()
if err != nil { return err }

mux := http.NewServeMux()
mux.Handle("GET /users/{id}", httpMetrics(usersHandler)) // instrument a route
_ = m.MountOn(mux)                                        // and expose /metrics

Or wire it once across the whole server through go/transport's middleware chain:

srv, _ := transporthttp.NewServer(ctx, settings, mux,
    transporthttp.WithMiddleware(transithttp.NewChain(httpMetrics)))

The metrics produced

Metric Type Labels
http_requests_total counter method, route, status
http_request_duration_seconds histogram method, route, status
http_requests_in_flight gauge

route is the matched template (GET /users/{id}), read from http.Request.Pattern (Go 1.22+ ServeMux). /users/42 and /users/43 share one series.

Options

Option Effect
WithHTTPNamespace("myapp") prefix the metric names (myapp_http_requests_total)
WithDurationBuckets(...) override the histogram buckets (default prometheus.DefBuckets)
WithStatusClass() record status as its class (2xx) instead of the exact code
WithRouteLabelFunc(f) derive the route template yourself (non-stdlib routers)
WithUnmatchedRouteLabel("<other>") the bucket for requests that matched no route
WithHTTPConstLabels{...} stamp constant labels (e.g. version) on the HTTP metrics

Non-stdlib routers

If you don't use http.ServeMux, r.Pattern is empty and everything would fall into the <other> bucket. Supply WithRouteLabelFunc to read your router's matched route template — chi's chi.RouteContext(r.Context()).RoutePattern(), gin's c.FullPath(), etc. Return the template, never the raw path — see Cardinality safety.

Errors and duration

The status label captures the response code (via a lightweight ResponseWriter wrapper that preserves flushing/hijacking through http.ResponseController), so error rate is sum(rate(http_requests_total{status=~"5.."}[5m])). The histogram gives you latency quantiles per route. Add exemplars to jump from a latency spike straight to the trace.