erigon-pulse/metrics/set.go
Mark Holt 0bdca6c457
Metrics label fixes (#8339)
Fixes for label discrepancies in collector for summaries etc which have
a template which includes a quantile.

Initial native Prometheus client implementation of metrics - which is
currently turned off except for local testing and interface exports.
2023-10-02 17:19:02 +01:00

611 lines
14 KiB
Go

package metrics
import (
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
)
type namedMetric struct {
name string
metric prometheus.Metric
isAux bool
}
// Set is a set of metrics.
//
// Metrics belonging to a set are exported separately from global metrics.
//
// Set.WritePrometheus must be called for exporting metrics from the set.
type Set struct {
mu sync.Mutex
a []*namedMetric
m map[string]*namedMetric
}
var defaultSet = NewSet()
// NewSet creates new set of metrics.
//
// Pass the set to RegisterSet() function in order to export its metrics via global WritePrometheus() call.
func NewSet() *Set {
return &Set{
m: make(map[string]*namedMetric),
}
}
func (s *Set) Describe(ch chan<- *prometheus.Desc) {
lessFunc := func(i, j int) bool {
return s.a[i].name < s.a[j].name
}
s.mu.Lock()
if !sort.SliceIsSorted(s.a, lessFunc) {
sort.Slice(s.a, lessFunc)
}
sa := append([]*namedMetric(nil), s.a...)
s.mu.Unlock()
for _, nm := range sa {
ch <- nm.metric.Desc()
}
}
func (s *Set) Collect(ch chan<- prometheus.Metric) {
lessFunc := func(i, j int) bool {
return s.a[i].name < s.a[j].name
}
s.mu.Lock()
if !sort.SliceIsSorted(s.a, lessFunc) {
sort.Slice(s.a, lessFunc)
}
sa := append([]*namedMetric(nil), s.a...)
s.mu.Unlock()
for _, nm := range sa {
ch <- nm.metric
}
}
// NewHistogram creates and returns new histogram in s with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned histogram is safe to use from concurrent goroutines.
func (s *Set) NewHistogram(name string, help ...string) (prometheus.Histogram, error) {
h, err := NewHistogram(name, help...)
if err != nil {
return nil, err
}
s.registerMetric(name, h)
return h, nil
}
func NewHistogram(name string, help ...string) (prometheus.Histogram, error) {
name, labels, err := parseMetric(name)
if err != nil {
return nil, err
}
return prometheus.NewHistogram(prometheus.HistogramOpts{
Name: name,
ConstLabels: labels,
Help: strings.Join(help, " "),
}), nil
}
// GetOrCreateHistogram returns registered histogram in s with the given name
// or creates new histogram if s doesn't contain histogram with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned histogram is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewHistogram instead of GetOrCreateHistogram.
func (s *Set) GetOrCreateHistogram(name string, help ...string) prometheus.Histogram {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
metric, err := NewHistogram(name, help...)
if err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: metric,
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
h, ok := nm.metric.(prometheus.Histogram)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Histogram. It is %T", name, nm.metric))
}
return h
}
// NewCounter registers and returns new counter with the given name in the s.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned counter is safe to use from concurrent goroutines.
func (s *Set) NewCounter(name string, help ...string) (prometheus.Counter, error) {
c, err := NewCounter(name, help...)
if err != nil {
return nil, err
}
s.registerMetric(name, c)
return c, nil
}
func NewCounter(name string, help ...string) (prometheus.Counter, error) {
name, labels, err := parseMetric(name)
if err != nil {
return nil, err
}
return prometheus.NewCounter(prometheus.CounterOpts{
Name: name,
Help: strings.Join(help, " "),
ConstLabels: labels,
}), nil
}
// GetOrCreateCounter returns registered counter in s with the given name
// or creates new counter if s doesn't contain counter with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned counter is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewCounter instead of GetOrCreateCounter.
func (s *Set) GetOrCreateCounter(name string, help ...string) prometheus.Counter {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing counter.
metric, err := NewCounter(name, help...)
if err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: metric,
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
c, ok := nm.metric.(prometheus.Counter)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Counter. It is %T", name, nm.metric))
}
return c
}
// NewGauge registers and returns gauge with the given name in s, which calls f
// to obtain gauge value.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// f must be safe for concurrent calls.
//
// The returned gauge is safe to use from concurrent goroutines.
func (s *Set) NewGauge(name string, help ...string) (prometheus.Gauge, error) {
g, err := NewGauge(name, help...)
if err != nil {
return nil, err
}
s.registerMetric(name, g)
return g, nil
}
func NewGauge(name string, help ...string) (prometheus.Gauge, error) {
name, labels, err := parseMetric(name)
if err != nil {
return nil, err
}
return prometheus.NewGauge(prometheus.GaugeOpts{
Name: name,
Help: strings.Join(help, " "),
ConstLabels: labels,
}), nil
}
// GetOrCreateGaugeFunc returns registered gauge with the given name in s
// or creates new gauge if s doesn't contain gauge with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned gauge is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewGauge instead of GetOrCreateGauge.
func (s *Set) GetOrCreateGauge(name string, help ...string) prometheus.Gauge {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing gauge.
metric, err := NewGauge(name, help...)
if err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: metric,
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
g, ok := nm.metric.(prometheus.Gauge)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Gauge. It is %T", name, nm.metric))
}
return g
}
// NewGaugeFunc registers and returns gauge with the given name in s, which calls f
// to obtain gauge value.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// f must be safe for concurrent calls.
//
// The returned gauge is safe to use from concurrent goroutines.
func (s *Set) NewGaugeFunc(name string, f func() float64, help ...string) (prometheus.GaugeFunc, error) {
g, err := NewGaugeFunc(name, f, help...)
if err != nil {
return nil, err
}
s.registerMetric(name, g)
return g, nil
}
func NewGaugeFunc(name string, f func() float64, help ...string) (prometheus.GaugeFunc, error) {
if f == nil {
return nil, fmt.Errorf("BUG: f cannot be nil")
}
name, labels, err := parseMetric(name)
if err != nil {
return nil, err
}
return prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Name: name,
Help: strings.Join(help, " "),
ConstLabels: labels,
}, f), nil
}
// GetOrCreateGaugeFunc returns registered gauge with the given name in s
// or creates new gauge if s doesn't contain gauge with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned gauge is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewGauge instead of GetOrCreateGauge.
func (s *Set) GetOrCreateGaugeFunc(name string, f func() float64, help ...string) prometheus.GaugeFunc {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing gauge.
if f == nil {
panic(fmt.Errorf("BUG: f cannot be nil"))
}
metric, err := NewGaugeFunc(name, f, help...)
if err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: metric,
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
g, ok := nm.metric.(prometheus.GaugeFunc)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Gauge. It is %T", name, nm.metric))
}
return g
}
const defaultSummaryWindow = 5 * time.Minute
var defaultSummaryQuantiles = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.97: 0.003, 0.99: 0.001}
// NewSummary creates and returns new summary with the given name in s.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
func (s *Set) NewSummary(name string, help ...string) (prometheus.Summary, error) {
sm, err := NewSummary(name, defaultSummaryWindow, defaultSummaryQuantiles, help...)
if err != nil {
return nil, err
}
s.mu.Lock()
// defer will unlock in case of panic
// checks in tests
defer s.mu.Unlock()
s.registerMetric(name, sm)
return sm, nil
}
// NewSummary creates and returns new summary in s with the given name,
// window and quantiles.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
func NewSummary(name string, window time.Duration, quantiles map[float64]float64, help ...string) (prometheus.Summary, error) {
name, labels, err := parseMetric(name)
if err != nil {
return nil, err
}
return prometheus.NewSummary(prometheus.SummaryOpts{
Name: name,
ConstLabels: labels,
Objectives: quantiles,
MaxAge: window,
}), nil
}
// GetOrCreateSummary returns registered summary with the given name in s
// or creates new summary if s doesn't contain summary with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewSummary instead of GetOrCreateSummary.
func (s *Set) GetOrCreateSummary(name string, help ...string) prometheus.Summary {
return s.GetOrCreateSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles, help...)
}
// GetOrCreateSummaryExt returns registered summary with the given name,
// window and quantiles in s or creates new summary if s doesn't
// contain summary with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewSummaryExt instead of GetOrCreateSummaryExt.
func (s *Set) GetOrCreateSummaryExt(name string, window time.Duration, quantiles map[float64]float64, help ...string) prometheus.Summary {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing summary.
metric, err := NewSummary(name, window, quantiles, help...)
if err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: metric,
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
sm, ok := nm.metric.(prometheus.Summary)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Summary. It is %T", name, nm.metric))
}
return sm
}
func (s *Set) registerMetric(name string, m prometheus.Metric) {
if _, _, err := parseMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
s.mu.Lock()
// defer will unlock in case of panic
// checks in test
defer s.mu.Unlock()
s.mustRegisterLocked(name, m)
}
// mustRegisterLocked registers given metric with the given name.
//
// Panics if the given name was already registered before.
func (s *Set) mustRegisterLocked(name string, m prometheus.Metric) {
_, ok := s.m[name]
if !ok {
nm := &namedMetric{
name: name,
metric: m,
}
s.m[name] = nm
s.a = append(s.a, nm)
}
if ok {
panic(fmt.Errorf("BUG: metric %q is already registered", name))
}
}
// UnregisterMetric removes metric with the given name from s.
//
// True is returned if the metric has been removed.
// False is returned if the given metric is missing in s.
func (s *Set) UnregisterMetric(name string) bool {
s.mu.Lock()
defer s.mu.Unlock()
nm, ok := s.m[name]
if !ok {
return false
}
return s.unregisterMetricLocked(nm)
}
func (s *Set) unregisterMetricLocked(nm *namedMetric) bool {
name := nm.name
delete(s.m, name)
deleteFromList := func(metricName string) {
for i, nm := range s.a {
if nm.name == metricName {
s.a = append(s.a[:i], s.a[i+1:]...)
return
}
}
panic(fmt.Errorf("BUG: cannot find metric %q in the list of registered metrics", name))
}
// remove metric from s.a
deleteFromList(name)
return true
}
// UnregisterAllMetrics de-registers all metrics registered in s.
func (s *Set) UnregisterAllMetrics() {
metricNames := s.ListMetricNames()
for _, name := range metricNames {
s.UnregisterMetric(name)
}
}
// ListMetricNames returns sorted list of all the metrics in s.
func (s *Set) ListMetricNames() []string {
s.mu.Lock()
defer s.mu.Unlock()
metricNames := make([]string, 0, len(s.m))
for _, nm := range s.m {
if nm.isAux {
continue
}
metricNames = append(metricNames, nm.name)
}
sort.Strings(metricNames)
return metricNames
}