Failure modes: what happens when it is wrong¶
Every way this module reports a problem — the errors it returns, the one warning it logs, and the two places it panics — with the trigger and the fix. Message text is quoted from a real run.
The one warning it logs¶
level=WARN msg="metrics endpoint mounted without guard middleware; ensure it is bound to loopback or fronted by auth" path=/metrics
Trigger. MountOn (and therefore Register) was called with no WithMiddleware
option.
What it means. The module cannot see your listener's bind address, so it cannot tell a loopback-bound service from an open port. It warns on the only signal it has.
When to ignore it. If the listener is bound to 127.0.0.1 or an internal address,
the posture is already safe and the warning is advisory. It is emitted at WARN
because the failure it guards against is an unauthenticated metrics or pprof port on
the public internet.
How to silence it. Pass the middleware you intended to pass. There is no
suppression option — supplying any middleware stops it. Route it through your own
logger with WithLogger; it goes to slog.Default() otherwise.
If you passed middleware and still see it, the option did not reach the
constructor — check you are not building the Metrics in one place and adding options
in another.
Errors returned by New and Register¶
New returns a wrapped error and no Metrics; Register returns the same error
without mounting anything.
| Message | Trigger |
|---|---|
register go collector: duplicate metrics collector registration attempted |
A registry passed to WithRegistry already carries a Go collector. |
register build-info collector: ... |
Something else already registered build_info on the supplied registry. |
register collector: ... |
A collector passed to WithCollectors clashes with one already present, or two of them describe the same metric. |
All three come from prometheus.Registry.Register, so the underlying cause is always
the same: two collectors describing the same fully-qualified metric name with the same
label dimensions. Supply a clean registry, or drop the duplicate.
A registry you share with the rest of your app¶
WithRegistry is designed for this, but the runtime collectors go on afterwards. If
your app already registered collectors.NewGoCollector() on that registry, add
WithoutRuntimeCollectors() so the module does not try again.
Errors returned by the instrument constructors¶
HTTPMiddleware, NewServerMetrics, NewOperations, NewDB, NewCache,
NewCircuitBreaker, NewQueue and DB.RegisterPool all return an error rather than
panicking, and the cause is nearly always a second registration of the same
instrument set:
| Call | Message on a repeat |
|---|---|
HTTPMiddleware |
register HTTP instrument: duplicate metrics collector registration attempted |
NewServerMetrics |
register gRPC instrument: duplicate metrics collector registration attempted |
NewOperations |
register operation instruments: duplicate metrics collector registration attempted |
NewDB (same name) |
register db instruments: duplicate metrics collector registration attempted |
NewCache (same name) |
register cache instruments: duplicate metrics collector registration attempted |
NewCircuitBreaker (same name) |
register circuit-breaker instruments: duplicate metrics collector registration attempted |
NewQueue (same name) |
register queue instruments: duplicate metrics collector registration attempted |
DB.RegisterPool |
register db pool collector: duplicate metrics collector registration attempted |
Two things make a repeat legal:
- A different identifying name.
NewDB("orders")andNewDB("billing")coexist — thedblabel distinguishes them. - A different namespace.
HTTPMiddleware()andHTTPMiddleware(WithHTTPNamespace("admin"))both succeed, because the namespace changes the metric names. The same applies to every helper.
RegisterPool can be called once per DB; a second call is a duplicate registration
even with an identical callback.
Ignoring these errors is the most common cause of a missing metric. Every one of
these constructors returns nil alongside the error, so db, _ := m.NewDB("orders")
hands you a nil pointer that panics on first use — and _, _ = m.HTTPMiddleware()
discards middleware that was never registered. Handle the error, or nothing you
instrument through the result will appear.
Panics, and the two ways to cause one¶
The module itself never panics. Both cases below come from http.ServeMux
rejecting a pattern, at mount time rather than at request time.
A metrics path with no leading slash¶
WithMetricsPath is not validated. The value is concatenated straight into a
"GET " + path mux pattern, so it must begin with /.
Mounting two Metrics on one mux at the same path¶
panic: pattern "GET /metrics" ... conflicts with pattern "GET /metrics":
GET /metrics matches the same requests as GET /metrics
Two Metrics instances mean two private registries, and a mux cannot serve both at
/metrics. Either mount one of them at a different WithMetricsPath, or — usually
better — build one Metrics and register everything on it.
Silent misconfiguration, with no error at all¶
These produce no error and no warning. They are the ones worth checking first when something is missing.
A pprof path without a trailing slash¶
WithPprofPath documents that the prefix must end in /, and nothing enforces it.
The sub-handlers are mounted by string concatenation, so WithPprofPath("/debug/pprof")
gives you:
| Request | Result |
|---|---|
GET /debug/pprof |
200 — the index |
GET /debug/pprofcmdline |
200 — the cmdline handler, at a path nobody would guess |
GET /debug/pprof/cmdline |
404 |
go tool pprof will fail against such an endpoint. Keep the trailing slash.
A route label from a non-stdlib router¶
http.Request.Pattern is populated by http.ServeMux. With chi, gin, echo or any
other router it is empty, so every request lands in the single <other> bucket and
per-route latency is lost. This is safe — it does not explode cardinality — but it is
useless. Supply WithRouteLabelFunc reading your router's matched template.
Exemplars recorded but never displayed¶
Exemplars ride on the OpenMetrics exposition format. A plain curl gets the
Prometheus text format and shows none, whether or not they were recorded. Ask for
OpenMetrics explicitly:
Prometheus itself also needs --enable-feature=exemplar-storage to keep them.
Metrics registered somewhere this module does not serve¶
promauto.NewCounter and prometheus.MustRegister write to the global default
registry. This module never touches the global registry — every Metrics owns a
private one — so those series will not appear on its /metrics. Register them
through WithCollectors, WithRegistry, or m.Registry().
Non-GET requests to /metrics¶
The endpoint is mounted as GET /metrics, which in http.ServeMux also matches
HEAD. Anything else is rejected by the mux, not by this module:
| Method | Response |
|---|---|
GET |
200 |
HEAD |
200 |
POST |
405, Allow: GET, HEAD |
OPTIONS |
405, Allow: GET, HEAD |
Values that resolve to a placeholder rather than failing¶
build_info never has an empty label. Anything unresolved becomes the literal string
unknown:
version="(devel)"— Go's own value for a main module built from source. PassWithBuildInfoor install viago install module@versionto get a real one.revision="unknown"— novcs.revisionin the build.go rundoes not stamp one, and neither doesgo build -buildvcs=falseor a build from outside a VCS checkout (many container builds).
Related¶
- Metrics reference — including a list of reasons a series can be missing.
- Options reference — defaults and validation for each option.
- Guard the endpoint — the two safe postures behind the warning.