feat(v2/metrics): support OpenMetrics exposition (and exemplars) on Metrics.Handler()
Summary
labkit/v2/metrics.Metrics.Handler() builds its HTTP handler as:
promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{Registry: m.registry})with EnableOpenMetrics unset. As a result the endpoint never negotiates the OpenMetrics exposition format: even when a scraper sends Accept: application/openmetrics-text, promhttp falls back to the classic Prometheus text format (text/plain; version=0.0.4) and drops exemplars recorded via ObserveWithExemplar.
The v1 labkit/monitoring package exposed OpenMetrics at /metrics; the v2 metrics package is a regression in this respect.
Why it matters
Exemplars (a histogram sample tagged with the active trace_id) are how an operator jumps from an aggregate latency spike to a specific distributed trace. client_golang only serializes them when the handler is built with promhttp.HandlerOpts{EnableOpenMetrics: true}. Because metrics.Config/Handler() expose no such option, a service that records exemplars cannot surface them through LabKit's handler — it must hand-roll promhttp.HandlerFor(m.Gatherer(), promhttp.HandlerOpts{EnableOpenMetrics: true}), bypassing Handler() / MountOn() and the httpserver probe-listener auto-mount (which hardcodes cfg.Metrics.Handler() at GET /-/metrics, with no override hook).
Concrete downstream case: the GitLab Artifact Registry metrics framework (gitlab-org/ops/artifact-registry#56 (closed)) records exemplars on its HTTP and DB latency histograms but cannot expose them via the probe /-/metrics, so it currently has to bypass Handler() entirely.
Proposed change
Add an EnableOpenMetrics bool to metrics.Config and plumb it into Handler():
type Config struct {
// ... existing fields ...
// EnableOpenMetrics serves the OpenMetrics exposition format when the
// scraper negotiates it (Accept: application/openmetrics-text). This is
// required for exemplar serialization. Defaults to false (classic
// Prometheus text exposition only).
EnableOpenMetrics bool
}
func (m *Metrics) Handler() http.Handler {
m.handlerOnce.Do(func() {
m.handler = promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{
Registry: m.registry,
EnableOpenMetrics: m.enableOpenMetrics,
})
})
return m.handler
}Default false preserves current behavior exactly; opting in is a single config field, and Handler() / MountOn() / the httpserver probe-listener auto-mount remain usable for services that record exemplars.
References
- Current implementation:
v2/metrics/metrics.go—Handler()andConfig client_golangpromhttp.HandlerOpts.EnableOpenMetrics- OpenMetrics specification (exemplars): https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md
- v1 parity:
labkit/monitoringexposed OpenMetrics at/metrics