mirror of
https://github.com/fosrl/gerbil.git
synced 2026-05-07 04:09:58 -05:00
311 lines
11 KiB
Go
311 lines
11 KiB
Go
// Package prometheus implements the native Prometheus metrics backend for Gerbil.
|
|
//
|
|
// This backend uses the Prometheus Go client directly; it does NOT depend on the
|
|
// OpenTelemetry SDK. A dedicated Prometheus registry is used so that default
|
|
// Go/process metrics are not unintentionally included unless the caller opts in.
|
|
package prometheus
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
// Config holds Prometheus-backend configuration.
|
|
type Config struct {
|
|
// Path is the HTTP endpoint path (e.g. "/metrics").
|
|
Path string
|
|
|
|
// IncludeGoMetrics controls whether the standard Go runtime and process
|
|
// collectors are registered on the dedicated registry.
|
|
// Defaults to true if not explicitly set.
|
|
IncludeGoMetrics *bool
|
|
}
|
|
|
|
// Backend is the native Prometheus metrics backend.
|
|
// Metric instruments are created via the New* family of methods and stored
|
|
// in the backend-specific instrument types that implement the observability
|
|
// instrument interfaces.
|
|
type Backend struct {
|
|
cfg Config
|
|
registry *prometheus.Registry
|
|
handler http.Handler
|
|
droppedSamplesCounter prometheus.Counter
|
|
}
|
|
|
|
// New creates and initialises a Prometheus backend.
|
|
//
|
|
// cfg.Path sets the HTTP endpoint path (defaults to "/metrics" if empty).
|
|
// cfg.IncludeGoMetrics controls whether standard Go runtime and process metrics
|
|
// are included; defaults to true when nil.
|
|
//
|
|
// Returns an error if the registry cannot be created.
|
|
func New(cfg Config) (*Backend, error) {
|
|
if cfg.Path == "" {
|
|
cfg.Path = "/metrics"
|
|
}
|
|
|
|
registry := prometheus.NewRegistry()
|
|
droppedSamplesCounter := prometheus.NewCounter(prometheus.CounterOpts{
|
|
Name: "gerbil_dropped_metric_samples_total",
|
|
Help: "Total number of metric samples dropped due to invalid labels or unsupported label sets",
|
|
})
|
|
registry.MustRegister(droppedSamplesCounter)
|
|
|
|
// Include Go and process metrics by default.
|
|
includeGo := cfg.IncludeGoMetrics == nil || *cfg.IncludeGoMetrics
|
|
if includeGo {
|
|
registry.MustRegister(
|
|
collectors.NewGoCollector(),
|
|
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
|
)
|
|
}
|
|
|
|
handler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{
|
|
EnableOpenMetrics: false,
|
|
})
|
|
|
|
return &Backend{cfg: cfg, registry: registry, handler: handler, droppedSamplesCounter: droppedSamplesCounter}, nil
|
|
}
|
|
|
|
// HTTPHandler returns the Prometheus /metrics HTTP handler.
|
|
func (b *Backend) HTTPHandler() http.Handler {
|
|
return b.handler
|
|
}
|
|
|
|
// Shutdown is a no-op for the Prometheus backend.
|
|
// The registry does not maintain background goroutines.
|
|
func (b *Backend) Shutdown(_ context.Context) error {
|
|
_ = b
|
|
return nil
|
|
}
|
|
|
|
// NewCounter creates a Prometheus CounterVec registered on the backend's registry.
|
|
func (b *Backend) NewCounter(name, desc string, labelNames ...string) (*Counter, error) {
|
|
vec := prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: name,
|
|
Help: desc,
|
|
}, labelNames)
|
|
if err := b.registry.Register(vec); err != nil {
|
|
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
|
|
existing, ok := are.ExistingCollector.(*prometheus.CounterVec)
|
|
if !ok {
|
|
return nil, err
|
|
}
|
|
return &Counter{vec: existing, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &Counter{vec: vec, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
|
|
// NewUpDownCounter creates a Prometheus GaugeVec (Prometheus gauges are
|
|
// bidirectional) registered on the backend's registry.
|
|
func (b *Backend) NewUpDownCounter(name, desc string, labelNames ...string) (*UpDownCounter, error) {
|
|
vec := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Name: name,
|
|
Help: desc,
|
|
}, labelNames)
|
|
if err := b.registry.Register(vec); err != nil {
|
|
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
|
|
existing, ok := are.ExistingCollector.(*prometheus.GaugeVec)
|
|
if !ok {
|
|
return nil, err
|
|
}
|
|
return &UpDownCounter{vec: existing, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &UpDownCounter{vec: vec, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
|
|
// NewInt64Gauge creates a Prometheus GaugeVec registered on the backend's registry.
|
|
func (b *Backend) NewInt64Gauge(name, desc string, labelNames ...string) (*Int64Gauge, error) {
|
|
vec := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Name: name,
|
|
Help: desc,
|
|
}, labelNames)
|
|
if err := b.registry.Register(vec); err != nil {
|
|
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
|
|
existing, ok := are.ExistingCollector.(*prometheus.GaugeVec)
|
|
if !ok {
|
|
return nil, err
|
|
}
|
|
return &Int64Gauge{vec: existing, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &Int64Gauge{vec: vec, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
|
|
// NewFloat64Gauge creates a Prometheus GaugeVec registered on the backend's registry.
|
|
func (b *Backend) NewFloat64Gauge(name, desc string, labelNames ...string) (*Float64Gauge, error) {
|
|
vec := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Name: name,
|
|
Help: desc,
|
|
}, labelNames)
|
|
if err := b.registry.Register(vec); err != nil {
|
|
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
|
|
existing, ok := are.ExistingCollector.(*prometheus.GaugeVec)
|
|
if !ok {
|
|
return nil, err
|
|
}
|
|
return &Float64Gauge{vec: existing, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &Float64Gauge{vec: vec, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
|
|
// NewHistogram creates a Prometheus HistogramVec registered on the backend's registry.
|
|
func (b *Backend) NewHistogram(name, desc string, buckets []float64, labelNames ...string) (*Histogram, error) {
|
|
vec := prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
Name: name,
|
|
Help: desc,
|
|
Buckets: buckets,
|
|
}, labelNames)
|
|
if err := b.registry.Register(vec); err != nil {
|
|
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
|
|
existing, ok := are.ExistingCollector.(*prometheus.HistogramVec)
|
|
if !ok {
|
|
return nil, err
|
|
}
|
|
return &Histogram{vec: existing, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &Histogram{vec: vec, labelNames: append([]string(nil), labelNames...), droppedSamplesCounter: b.droppedSamplesCounter}, nil
|
|
}
|
|
|
|
// Counter is a native Prometheus counter instrument.
|
|
type Counter struct {
|
|
vec *prometheus.CounterVec
|
|
labelNames []string
|
|
droppedSamplesCounter prometheus.Counter
|
|
}
|
|
|
|
// Add increments the counter by value for the given labels.
|
|
//
|
|
// value must be non-negative. Negative values are ignored.
|
|
func (c *Counter) Add(_ context.Context, value int64, labels map[string]string) {
|
|
if value < 0 {
|
|
log.Printf("WARN: counter add called with negative value=%d labels=%v expected_labels=%v", value, labels, c.labelNames)
|
|
return
|
|
}
|
|
normalized, ok := normalizeLabels(c.labelNames, labels, c.droppedSamplesCounter)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer guardMetricPanic("counter", c.labelNames, labels)
|
|
c.vec.With(normalized).Add(float64(value))
|
|
}
|
|
|
|
// UpDownCounter is a native Prometheus gauge used as a bidirectional counter.
|
|
type UpDownCounter struct {
|
|
vec *prometheus.GaugeVec
|
|
labelNames []string
|
|
droppedSamplesCounter prometheus.Counter
|
|
}
|
|
|
|
// Add adjusts the gauge by value for the given labels.
|
|
func (u *UpDownCounter) Add(_ context.Context, value int64, labels map[string]string) {
|
|
normalized, ok := normalizeLabels(u.labelNames, labels, u.droppedSamplesCounter)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer guardMetricPanic("updown", u.labelNames, labels)
|
|
u.vec.With(normalized).Add(float64(value))
|
|
}
|
|
|
|
// Int64Gauge is a native Prometheus gauge recording integer snapshot values.
|
|
type Int64Gauge struct {
|
|
vec *prometheus.GaugeVec
|
|
labelNames []string
|
|
droppedSamplesCounter prometheus.Counter
|
|
}
|
|
|
|
// Record sets the gauge to value for the given labels.
|
|
func (g *Int64Gauge) Record(_ context.Context, value int64, labels map[string]string) {
|
|
normalized, ok := normalizeLabels(g.labelNames, labels, g.droppedSamplesCounter)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer guardMetricPanic("int64-gauge", g.labelNames, labels)
|
|
g.vec.With(normalized).Set(float64(value))
|
|
}
|
|
|
|
// Float64Gauge is a native Prometheus gauge recording float snapshot values.
|
|
type Float64Gauge struct {
|
|
vec *prometheus.GaugeVec
|
|
labelNames []string
|
|
droppedSamplesCounter prometheus.Counter
|
|
}
|
|
|
|
// Record sets the gauge to value for the given labels.
|
|
func (g *Float64Gauge) Record(_ context.Context, value float64, labels map[string]string) {
|
|
normalized, ok := normalizeLabels(g.labelNames, labels, g.droppedSamplesCounter)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer guardMetricPanic("float64-gauge", g.labelNames, labels)
|
|
g.vec.With(normalized).Set(value)
|
|
}
|
|
|
|
// Histogram is a native Prometheus histogram instrument.
|
|
type Histogram struct {
|
|
vec *prometheus.HistogramVec
|
|
labelNames []string
|
|
droppedSamplesCounter prometheus.Counter
|
|
}
|
|
|
|
// Record observes value for the given labels.
|
|
func (h *Histogram) Record(_ context.Context, value float64, labels map[string]string) {
|
|
normalized, ok := normalizeLabels(h.labelNames, labels, h.droppedSamplesCounter)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer guardMetricPanic("histogram", h.labelNames, labels)
|
|
h.vec.With(normalized).Observe(value)
|
|
}
|
|
|
|
func normalizeLabels(labelNames []string, labels map[string]string, droppedSamplesCounter prometheus.Counter) (prometheus.Labels, bool) {
|
|
if len(labelNames) == 0 {
|
|
if len(labels) > 0 {
|
|
if droppedSamplesCounter != nil {
|
|
droppedSamplesCounter.Inc()
|
|
}
|
|
log.Printf("WARN: dropping metric sample due to unexpected labels: got=%v expected=none", labels)
|
|
return nil, false
|
|
}
|
|
return nil, true
|
|
}
|
|
|
|
normalized := make(prometheus.Labels, len(labelNames))
|
|
for _, name := range labelNames {
|
|
normalized[name] = ""
|
|
}
|
|
|
|
for k, v := range labels {
|
|
if _, ok := normalized[k]; !ok {
|
|
if droppedSamplesCounter != nil {
|
|
droppedSamplesCounter.Inc()
|
|
}
|
|
log.Printf("WARN: dropping metric sample due to unexpected label key %q (expected=%v)", k, labelNames)
|
|
return nil, false
|
|
}
|
|
normalized[k] = v
|
|
}
|
|
|
|
return normalized, true
|
|
}
|
|
|
|
func guardMetricPanic(kind string, expected []string, labels map[string]string) {
|
|
if recovered := recover(); recovered != nil {
|
|
log.Printf("WARN: dropped %s metric sample due to label panic: expected=%v got=%v err=%v", kind, expected, labels, recovered)
|
|
}
|
|
}
|