Files
releases/internal/prometheus/prometheus.go
T
Michael Munch 12b7290697 distributedtracing: export Prometheus metrics via OTLP (#8450)
* distributedtracing: export Prometheus metrics via OTLP

Add support for pushing OPA's existing Prometheus metrics to an
OpenTelemetry collector via OTLP, eliminating the need for a dedicated
scraper sidecar. Uses the OTel Prometheus bridge to read from OPA's
prometheus.Registry and export through an OTLP metric exporter (gRPC
or HTTP), reusing the same address and TLS configuration as traces.

New config fields: distributed_tracing.metrics (bool, default false)
and distributed_tracing.metrics_export_interval_ms (int, default 60000).

Fixes #7591

Signed-off-by: Michael Munch <mm.munk@gmail.com>

* metricsexport: decouple metrics export into top-level config section

Extract metrics export from distributed_tracing into its own
metrics_export config section with independent type (otlp/grpc,
otlp/http), address, and TLS settings. This allows exporting
Prometheus metrics via OTLP without enabling tracing, and to a
different endpoint than traces.

- Extract shared TLS helpers into internal/tlsutil
- Add MetricsExport field to top-level Config
- Create internal/metricsexport package with Init, config parsing
- Remove metrics fields from distributedtracing
- Update runtime to call metricsexport.Init separately
- Move e2e tests to v1/test/e2e/metricsexport
- Add Metrics Export section to configuration docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Munch <mm.munk@gmail.com>

* ci: retrigger checks

Signed-off-by: Michael Munch <mm.munk@gmail.com>

* go.mod: upgrade dependencies downgraded during rebase

Modules like containerd, go-sqlbuilder, OpenTelemetry, and golang.org/x/*
were at older versions than main after a rebase. Upgrade them to match or
exceed main.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Munch <mm.munk@gmail.com>

* Update internal/distributedtracing/distributedtracing_test.go

Signed-off-by: Michael Munch <mm.munk@gmail.com>

---------

Signed-off-by: Michael Munch <mm.munk@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:09:51 +00:00

229 lines
6.4 KiB
Go

// Copyright 2019 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package prometheus
import (
"bufio"
"encoding/json"
"fmt"
"math"
"net"
"net/http"
"runtime"
"strconv"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/open-policy-agent/opa/v1/metrics"
)
// Provider wraps a metrics.Metrics provider with a Prometheus registry that can
// instrument the HTTP server's handlers.
type Provider struct {
registry *prometheus.Registry
durationHistogram *prometheus.HistogramVec
cancellationCounters *prometheus.CounterVec
inner metrics.Metrics
logger loggerFunc
}
type loggerFunc func(attrs map[string]any, f string, a ...any)
// New returns a new Provider object.
func New(inner metrics.Metrics, logger loggerFunc, httpRequestBuckets []float64) *Provider {
registry := prometheus.NewRegistry()
registry.MustRegister(collector())
durationHistogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "A histogram of duration for requests.",
Buckets: httpRequestBuckets,
},
[]string{"code", "handler", "method"},
)
registry.MustRegister(durationHistogram)
cancellationCounters := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_request_cancellations",
Help: "A count of cancelled requests.",
},
[]string{"code", "handler", "method"},
)
registry.MustRegister(cancellationCounters)
return &Provider{
registry: registry,
durationHistogram: durationHistogram,
cancellationCounters: cancellationCounters,
inner: inner,
logger: logger,
}
}
// Gatherer returns the underlying prometheus.Gatherer for exporting metrics via OTLP.
func (p *Provider) Gatherer() prometheus.Gatherer {
return p.registry
}
// RegisterEndpoints registers `/metrics` endpoint
func (p *Provider) RegisterEndpoints(registrar func(path, method string, handler http.Handler)) {
registrar("/metrics/alloc_bytes", http.MethodGet, http.HandlerFunc(allocHandler))
registrar("/metrics", http.MethodGet, promhttp.HandlerFor(p.registry, promhttp.HandlerOpts{}))
}
// InstrumentHandler returned wrapped HTTP handler with added prometheus instrumentation
func (p *Provider) InstrumentHandler(handler http.Handler, label string) http.Handler {
durationCollector := p.durationHistogram.MustCurryWith(prometheus.Labels{"handler": label})
cancellationsCollector := p.cancellationCounters.MustCurryWith(prometheus.Labels{"handler": label})
return promhttp.InstrumentHandlerDuration(durationCollector, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
csrw := &captureStatusResponseWriter{ResponseWriter: w, status: http.StatusOK}
var rw http.ResponseWriter
if h, ok := w.(http.Hijacker); ok {
rw = &hijacker{ResponseWriter: csrw, hijacker: h}
} else {
rw = csrw
}
handler.ServeHTTP(rw, r)
if r.Context().Err() != nil {
cancellationsCollector.With(prometheus.Labels{"code": strconv.Itoa(csrw.status), "method": r.Method}).Inc()
}
}))
}
// Info returns attributes that describe the metric provider.
func (*Provider) Info() metrics.Info {
return metrics.Info{
Name: "prometheus",
}
}
// All returns the union of the inner metric provider and the underlying
// prometheus registry.
func (p *Provider) All() map[string]any {
all := p.inner.All()
families, err := p.registry.Gather()
if err != nil && p.logger != nil {
p.logger(map[string]any{
"err": err,
}, "Failed to gather metrics from Prometheus registry.")
}
for _, f := range families {
all[f.GetName()] = wrap{family: f}
}
return all
}
type wrap struct{ family proto.Message }
func (w wrap) MarshalJSON() ([]byte, error) {
return protojson.Marshal(w.family)
}
// MarshalJSON returns a JSON representation of the unioned metrics.
func (p *Provider) MarshalJSON() ([]byte, error) {
return json.Marshal(p.All())
}
// Timer returns a named timer.
func (p *Provider) Timer(name string) metrics.Timer {
return p.inner.Timer(name)
}
// Counter returns a named counter.
func (p *Provider) Counter(name string) metrics.Counter {
return p.inner.Counter(name)
}
// Histogram returns a named histogram.
func (p *Provider) Histogram(name string) metrics.Histogram {
return p.inner.Histogram(name)
}
// Clear resets the inner metric provider. The Prometheus registry does not
// expose an interface to clear the metrics so this call has no affect on
// metrics tracked by Prometheus.
func (p *Provider) Clear() {
p.inner.Clear()
}
// Register register the collectors on OPA prometheus registry
func (p *Provider) Register(c prometheus.Collector) error {
return p.registry.Register(c)
}
// MustRegister register the collectors on OPA prometheus registry and panics when an error occurs
func (p *Provider) MustRegister(cs ...prometheus.Collector) {
p.registry.MustRegister(cs...)
}
// Unregister unregister the collectors on OPA prometheus registry
func (p *Provider) Unregister(c prometheus.Collector) bool {
return p.registry.Unregister(c)
}
type captureStatusResponseWriter struct {
http.ResponseWriter
status int
}
type hijacker struct {
http.ResponseWriter
hijacker http.Hijacker
}
func (h *hijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return h.hijacker.Hijack()
}
func (c *captureStatusResponseWriter) WriteHeader(statusCode int) {
c.ResponseWriter.WriteHeader(statusCode)
c.status = statusCode
}
var _ http.Flusher = (*captureStatusResponseWriter)(nil)
func (c *captureStatusResponseWriter) Flush() {
if h, ok := c.ResponseWriter.(http.Flusher); ok {
h.Flush()
}
}
func prettyByteSize(b uint64) string {
bf := float64(b)
for _, unit := range []string{"", "K", "M", "G", "T", "P", "E", "Z"} {
if math.Abs(bf) < 1000.0 {
return fmt.Sprintf("%3.1f%sB", bf, unit)
}
bf /= 1000.0
}
return fmt.Sprintf("%.1fYiB", bf)
}
func allocHandler(rsp http.ResponseWriter, req *http.Request) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
total := m.HeapInuse + m.StackInuse + m.MCacheInuse + m.MSpanInuse
var alloc string
if req.URL.RawQuery != "" && req.URL.Query().Get("pretty") == "true" {
alloc = prettyByteSize(total)
} else {
alloc = strconv.FormatUint(total, 10)
}
rsp.WriteHeader(200)
_, _ = fmt.Fprintln(rsp, alloc)
}