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
MountOn returns nothing — it panics only if the mux rejects the pattern, which
WithMetricsPath can cause. See Failure modes.
Or wire it once across the whole server through go/transport's middleware chain:
srv, err := transporthttp.Register(ctx, "api", controller, logger, mux, settings,
transporthttp.WithMiddleware(transithttp.NewChain(httpMetrics)))
WithMiddleware returns a RegisterOption, not a ServerOption, so it has to go
through Register. NewServer accepts ServerOption values only and will not
compile with it.
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.
Instrument the whole server once, not each route twice¶
HTTPMiddleware registers its three instruments on the registry the first time it is
called. Calling it again on the same Metrics returns:
Call it once and apply the returned middleware wherever you need it — to a whole
chain, or to individual routes. If you genuinely want a second, separate set (an admin
listener, say), give it its own namespace: m.HTTPMiddleware(metrics.WithHTTPNamespace("admin"))
succeeds because the metric names differ.
What it does not measure¶
Server-side requests only. There is no instrumented http.RoundTripper and no
http_client_* series, so calls your service makes to other services are not covered
— client_golang's own promhttp.InstrumentRoundTripper* helpers register on
m.Registry() if you want them. Request and response sizes are not recorded either.
See Limitations.
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.