diff --git a/metric/controller.go b/metric/controller.go index 2d212fcc1..0fcf16e2b 100644 --- a/metric/controller.go +++ b/metric/controller.go @@ -22,7 +22,7 @@ type Ctl struct { register *prometheus.Registry holder *Holder - metrics map[string]prometheus.Collector + metrics map[string]any metricMaxLabelValueLength int mu sync.RWMutex } @@ -31,7 +31,7 @@ func NewCtl(subsystem string, registry *prometheus.Registry, metricHoldDuration ctl := &Ctl{ subsystem: subsystem, register: registry, - metrics: make(map[string]prometheus.Collector), + metrics: make(map[string]any), metricMaxLabelValueLength: metricMaxLabelValueLength, } @@ -59,89 +59,129 @@ func (mc *Ctl) AddToHolder(mv heldMetricVec) { } func (mc *Ctl) RegisterCounter(name, help string) *Counter { - counter := prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: PromNamespace, - Subsystem: mc.subsystem, - Name: name, - Help: help, - }) - - return newCounter(mc.registerMetric(name, counter).(prometheus.Counter)) + return registerWrapper(mc, name, + func() prometheus.Collector { + return prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: PromNamespace, + Subsystem: mc.subsystem, + Name: name, + Help: help, + }) + }, + func(c prometheus.Collector) *Counter { + return newCounter(c.(prometheus.Counter)) + }, + ) } func (mc *Ctl) RegisterCounterVec(name, help string, labels ...string) *CounterVec { - counterVec := prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: PromNamespace, - Subsystem: mc.subsystem, - Name: name, - Help: help, - }, labels) - - return newCounterVec(mc.registerMetric(name, counterVec).(*prometheus.CounterVec), mc.metricMaxLabelValueLength) + return registerWrapper(mc, name, + func() prometheus.Collector { + return prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: PromNamespace, + Subsystem: mc.subsystem, + Name: name, + Help: help, + }, labels) + }, + func(c prometheus.Collector) *CounterVec { + return newCounterVec(c.(*prometheus.CounterVec), mc.metricMaxLabelValueLength) + }, + ) } func (mc *Ctl) RegisterGauge(name, help string) *Gauge { - gauge := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: PromNamespace, - Subsystem: mc.subsystem, - Name: name, - Help: help, - }) - - return newGauge(mc.registerMetric(name, gauge).(prometheus.Gauge)) + return registerWrapper(mc, name, + func() prometheus.Collector { + return prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: PromNamespace, + Subsystem: mc.subsystem, + Name: name, + Help: help, + }) + }, + func(c prometheus.Collector) *Gauge { + return newGauge(c.(prometheus.Gauge)) + }, + ) } func (mc *Ctl) RegisterGaugeVec(name, help string, labels ...string) *GaugeVec { - gaugeVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: PromNamespace, - Subsystem: mc.subsystem, - Name: name, - Help: help, - }, labels) - - return newGaugeVec(mc.registerMetric(name, gaugeVec).(*prometheus.GaugeVec), mc.metricMaxLabelValueLength) + return registerWrapper(mc, name, + func() prometheus.Collector { + return prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: PromNamespace, + Subsystem: mc.subsystem, + Name: name, + Help: help, + }, labels) + }, + func(c prometheus.Collector) *GaugeVec { + return newGaugeVec(c.(*prometheus.GaugeVec), mc.metricMaxLabelValueLength) + }, + ) } func (mc *Ctl) RegisterHistogram(name, help string, buckets []float64) *Histogram { - histogram := prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: PromNamespace, - Subsystem: mc.subsystem, - Name: name, - Help: help, - Buckets: buckets, - }) - - return newHistogram(mc.registerMetric(name, histogram).(prometheus.Histogram)) + return registerWrapper(mc, name, + func() prometheus.Collector { + return prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: PromNamespace, + Subsystem: mc.subsystem, + Name: name, + Help: help, + Buckets: buckets, + }) + }, + func(c prometheus.Collector) *Histogram { + return newHistogram(c.(prometheus.Histogram)) + }, + ) } func (mc *Ctl) RegisterHistogramVec(name, help string, buckets []float64, labels ...string) *HistogramVec { - histogramVec := prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: PromNamespace, - Subsystem: mc.subsystem, - Name: name, - Help: help, - Buckets: buckets, - }, labels) - - return newHistogramVec(mc.registerMetric(name, histogramVec).(*prometheus.HistogramVec), mc.metricMaxLabelValueLength) + return registerWrapper(mc, name, + func() prometheus.Collector { + return prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: PromNamespace, + Subsystem: mc.subsystem, + Name: name, + Help: help, + Buckets: buckets, + }, labels) + }, + func(c prometheus.Collector) *HistogramVec { + return newHistogramVec(c.(*prometheus.HistogramVec), mc.metricMaxLabelValueLength) + }, + ) } -func (mc *Ctl) registerMetric(name string, newMetric prometheus.Collector) prometheus.Collector { +// registerWrapper returns a cached wrapper by name or creates, registers and caches a new one. +// The prometheus collector is created lazily only on a cache miss. +func registerWrapper[W any]( + mc *Ctl, + name string, + newCollector func() prometheus.Collector, + wrap func(prometheus.Collector) W, +) W { mc.mu.RLock() - metric, has := mc.metrics[name] + cached, has := mc.metrics[name] mc.mu.RUnlock() if has { - return metric + return cached.(W) } mc.mu.Lock() defer mc.mu.Unlock() - metric, has = mc.metrics[name] - if !has { - metric = newMetric - mc.metrics[name] = metric - mc.register.MustRegister(metric) + + if cached, has = mc.metrics[name]; has { + return cached.(W) } - return metric + collector := newCollector() + mc.register.MustRegister(collector) + + wrapper := wrap(collector) + mc.metrics[name] = wrapper + return wrapper } diff --git a/metric/counter.go b/metric/counter.go index e7a3ebb51..1e948ce39 100644 --- a/metric/counter.go +++ b/metric/counter.go @@ -17,6 +17,11 @@ func newCounter(c prometheus.Counter) *Counter { } } +//nolint:unused +func (c *Counter) getHeldMetric() *heldMetric[prometheus.Counter] { + return c.heldMetric +} + func (c *Counter) Inc() { c.metric.Inc() c.updateUsage() @@ -33,21 +38,24 @@ func (c *Counter) ToFloat64() float64 { } type CounterVec struct { - store *heldMetricsStore[prometheus.Counter] + store *heldMetricsStore[prometheus.Counter, *Counter] vec *prometheus.CounterVec } func newCounterVec(cv *prometheus.CounterVec, maxLabelValueLength int) *CounterVec { return &CounterVec{ - vec: cv, - store: newHeldMetricsStore[prometheus.Counter](maxLabelValueLength), + vec: cv, + store: newHeldMetricsStore( + maxLabelValueLength, + func(hm *heldMetric[prometheus.Counter]) *Counter { + return &Counter{heldMetric: hm} + }, + ), } } func (cv *CounterVec) WithLabelValues(lvs ...string) *Counter { - return &Counter{ - heldMetric: cv.store.GetOrCreate(lvs, cv.vec.WithLabelValues), - } + return cv.store.GetOrCreate(lvs, cv.vec.WithLabelValues) } func (cv *CounterVec) DeleteLabelValues(lvs ...string) bool { diff --git a/metric/gauge.go b/metric/gauge.go index 2d3a55161..2770ea5e3 100644 --- a/metric/gauge.go +++ b/metric/gauge.go @@ -17,6 +17,11 @@ func newGauge(c prometheus.Gauge) *Gauge { } } +//nolint:unused +func (g *Gauge) getHeldMetric() *heldMetric[prometheus.Gauge] { + return g.heldMetric +} + func (g *Gauge) Set(v float64) { g.metric.Set(v) g.updateUsage() @@ -43,30 +48,33 @@ func (g *Gauge) Sub(v float64) { } // should only be used in tests -func (c *Gauge) ToFloat64() float64 { - return testutil.ToFloat64(c.metric) +func (g *Gauge) ToFloat64() float64 { + return testutil.ToFloat64(g.metric) } type GaugeVec struct { - store *heldMetricsStore[prometheus.Gauge] + store *heldMetricsStore[prometheus.Gauge, *Gauge] vec *prometheus.GaugeVec } func newGaugeVec(gv *prometheus.GaugeVec, maxLabelValueLength int) *GaugeVec { return &GaugeVec{ - vec: gv, - store: newHeldMetricsStore[prometheus.Gauge](maxLabelValueLength), + vec: gv, + store: newHeldMetricsStore( + maxLabelValueLength, + func(hm *heldMetric[prometheus.Gauge]) *Gauge { + return &Gauge{heldMetric: hm} + }, + ), } } func (gv *GaugeVec) WithLabelValues(lvs ...string) *Gauge { - return &Gauge{ - heldMetric: gv.store.GetOrCreate(lvs, gv.vec.WithLabelValues), - } + return gv.store.GetOrCreate(lvs, gv.vec.WithLabelValues) } -func (cv *GaugeVec) DeleteLabelValues(lvs ...string) bool { - return cv.store.Delete(lvs, cv.vec) +func (gv *GaugeVec) DeleteLabelValues(lvs ...string) bool { + return gv.store.Delete(lvs, gv.vec) } func (gv *GaugeVec) DeleteOldMetrics(holdDuration time.Duration) { diff --git a/metric/histogram.go b/metric/histogram.go index 5b9312171..1861778c2 100644 --- a/metric/histogram.go +++ b/metric/histogram.go @@ -16,33 +16,41 @@ func newHistogram(c prometheus.Histogram) *Histogram { } } +//nolint:unused +func (h *Histogram) getHeldMetric() *heldMetric[prometheus.Histogram] { + return h.heldMetric +} + func (h *Histogram) Observe(v float64) { h.metric.Observe(v) h.updateUsage() } type HistogramVec struct { - store *heldMetricsStore[prometheus.Histogram] + store *heldMetricsStore[prometheus.Histogram, *Histogram] vec *prometheus.HistogramVec } func newHistogramVec(hv *prometheus.HistogramVec, maxLabelValueLength int) *HistogramVec { return &HistogramVec{ - vec: hv, - store: newHeldMetricsStore[prometheus.Histogram](maxLabelValueLength), + vec: hv, + store: newHeldMetricsStore( + maxLabelValueLength, + func(hm *heldMetric[prometheus.Histogram]) *Histogram { + return &Histogram{heldMetric: hm} + }, + ), } } func (hv *HistogramVec) WithLabelValues(lvs ...string) *Histogram { - return &Histogram{ - heldMetric: hv.store.GetOrCreate(lvs, func(s ...string) prometheus.Histogram { - return hv.vec.WithLabelValues(s...).(prometheus.Histogram) - }), - } + return hv.store.GetOrCreate(lvs, func(s ...string) prometheus.Histogram { + return hv.vec.WithLabelValues(s...).(prometheus.Histogram) + }) } -func (cv *HistogramVec) DeleteLabelValues(lvs ...string) bool { - return cv.store.Delete(lvs, cv.vec) +func (hv *HistogramVec) DeleteLabelValues(lvs ...string) bool { + return hv.store.Delete(lvs, hv.vec) } func (hv *HistogramVec) DeleteOldMetrics(holdDuration time.Duration) { diff --git a/metric/metric.go b/metric/metric.go index 76c0d05ed..a0ebd086d 100644 --- a/metric/metric.go +++ b/metric/metric.go @@ -40,78 +40,95 @@ func (h *heldMetric[T]) updateUsage() { } } -type heldMetricsStore[T prometheus.Metric] struct { +// heldMetricWrapper is implemented by all metric wrappers (Counter, Gauge, Histogram). +// It gives the store access to the common held data while letting it cache and return +// the concrete wrapper directly (without re-allocating on each WithLabelValues call). +type heldMetricWrapper[T prometheus.Metric] interface { + getHeldMetric() *heldMetric[T] +} + +type heldMetricsStore[T prometheus.Metric, W heldMetricWrapper[T]] struct { mu sync.RWMutex - metricsByHash map[uint64][]*heldMetric[T] + metricsByHash map[uint64][]W metricMaxLabelValueLength int + newWrapper func(*heldMetric[T]) W } -func newHeldMetricsStore[T prometheus.Metric](metricMaxLabelValueLength int) *heldMetricsStore[T] { - return &heldMetricsStore[T]{ +func newHeldMetricsStore[T prometheus.Metric, W heldMetricWrapper[T]]( + metricMaxLabelValueLength int, + newWrapper func(*heldMetric[T]) W, +) *heldMetricsStore[T, W] { + return &heldMetricsStore[T, W]{ mu: sync.RWMutex{}, - metricsByHash: make(map[uint64][]*heldMetric[T]), + metricsByHash: make(map[uint64][]W), metricMaxLabelValueLength: metricMaxLabelValueLength, + newWrapper: newWrapper, } } -func (h *heldMetricsStore[T]) GetOrCreate(labels []string, newPromMetric func(...string) T) *heldMetric[T] { +func (h *heldMetricsStore[T, W]) GetOrCreate(labels []string, newPromMetric func(...string) T) W { h.truncateLabels(labels) hash := computeStringsHash(labels) - // fast path - metric exists + // fast path - wrapper exists h.mu.RLock() - hMetric, ok := h.getHeldMetricByHash(labels, hash) + w, ok := h.getByHash(labels, hash) h.mu.RUnlock() if ok { - return hMetric + return w } - // slow path - create new metric + // slow path - create new wrapper return h.tryCreate(labels, hash, newPromMetric) } -func (h *heldMetricsStore[T]) Delete(labels []string, deleter metricDeleter) bool { +func (h *heldMetricsStore[T, W]) Delete(labels []string, deleter metricDeleter) bool { h.truncateLabels(labels) hash := computeStringsHash(labels) h.mu.Lock() defer h.mu.Unlock() - hMetrics, ok := h.metricsByHash[hash] + wrappers, ok := h.metricsByHash[hash] if !ok { return false } - i := findHeldMetricIndex(hMetrics, labels) + i := h.findIndex(wrappers, labels) if i == -1 { return false } deleter.DeleteLabelValues(labels...) - *hMetrics[i] = heldMetric[T]{} - hMetrics = append(hMetrics[:i], hMetrics[i+1:]...) + *wrappers[i].getHeldMetric() = heldMetric[T]{} + wrappers = append(wrappers[:i], wrappers[i+1:]...) - if len(hMetrics) == 0 { + if len(wrappers) == 0 { delete(h.metricsByHash, hash) + } else { + h.metricsByHash[hash] = wrappers } return ok } -func (h *heldMetricsStore[T]) getHeldMetricByHash(labels []string, hash uint64) (*heldMetric[T], bool) { - hMetrics, ok := h.metricsByHash[hash] +func (h *heldMetricsStore[T, W]) getByHash(labels []string, hash uint64) (W, bool) { + wrappers, ok := h.metricsByHash[hash] if !ok { - return nil, false + var zero W + return zero, false } - if len(hMetrics) == 1 { - return hMetrics[0], true + if len(wrappers) == 1 { + return wrappers[0], true } - if i := findHeldMetricIndex(hMetrics, labels); i != -1 { - return hMetrics[i], true + if i := h.findIndex(wrappers, labels); i != -1 { + return wrappers[i], true } - return nil, false + + var zero W + return zero, false } -func (h *heldMetricsStore[T]) tryCreate(labels []string, hash uint64, newPromMetric func(...string) T) *heldMetric[T] { +func (h *heldMetricsStore[T, W]) tryCreate(labels []string, hash uint64, newPromMetric func(...string) T) W { // copy labels because they are unsafe converted bytes // TODO: replace with [][]byte to make it explicit labelsCopy := make([]string, len(labels)) @@ -125,45 +142,56 @@ func (h *heldMetricsStore[T]) tryCreate(labels []string, hash uint64, newPromMet h.mu.Lock() defer h.mu.Unlock() - hMetric, ok := h.getHeldMetricByHash(labels, hash) - if ok { - return hMetric + if w, ok := h.getByHash(labels, hash); ok { + return w } - hMetric = newHeldMetric(labels, metric) - h.metricsByHash[hash] = append(h.metricsByHash[hash], hMetric) - return hMetric + w := h.newWrapper(newHeldMetric(labels, metric)) + h.metricsByHash[hash] = append(h.metricsByHash[hash], w) + return w } type metricDeleter interface { DeleteLabelValues(...string) bool } -func (h *heldMetricsStore[T]) DeleteOldMetrics(holdDuration time.Duration, deleter metricDeleter) { +func (h *heldMetricsStore[T, W]) DeleteOldMetrics(holdDuration time.Duration, deleter metricDeleter) { now := xtime.GetInaccurateUnixNano() h.mu.Lock() defer h.mu.Unlock() - for hash, hMetrics := range h.metricsByHash { - releasedMetrics := slices.DeleteFunc(hMetrics, func(hMetric *heldMetric[T]) bool { - lastUsage := hMetric.lastUsage.Load() + for hash, wrappers := range h.metricsByHash { + releasedMetrics := slices.DeleteFunc(wrappers, func(w W) bool { + hm := w.getHeldMetric() + lastUsage := hm.lastUsage.Load() diff := now - lastUsage isObsolete := diff > holdDuration.Nanoseconds() if isObsolete { - deleter.DeleteLabelValues(hMetric.labels...) - *hMetric = heldMetric[T]{} // release objects in the structure + deleter.DeleteLabelValues(hm.labels...) + *hm = heldMetric[T]{} // release objects in the structure } return isObsolete }) if len(releasedMetrics) == 0 { delete(h.metricsByHash, hash) + } else { + h.metricsByHash[hash] = releasedMetrics } } } -func (h *heldMetricsStore[T]) truncateLabels(lvs []string) { +func (h *heldMetricsStore[T, W]) findIndex(wrappers []W, labels []string) int { + for i := range wrappers { + if slices.Equal(wrappers[i].getHeldMetric().labels, labels) { + return i + } + } + return -1 +} + +func (h *heldMetricsStore[T, W]) truncateLabels(lvs []string) { if h.metricMaxLabelValueLength == 0 { return } @@ -175,17 +203,6 @@ func (h *heldMetricsStore[T]) truncateLabels(lvs []string) { } } -func findHeldMetricIndex[T prometheus.Metric](hMetrics []*heldMetric[T], labels []string) int { - idx := -1 - for i := range hMetrics { - if slices.Equal(hMetrics[i].labels, labels) { - idx = i - break - } - } - return idx -} - func computeStringsHash(s []string) uint64 { var hash uint64 if len(s) == 1 { diff --git a/metric/metric_test.go b/metric/metric_test.go index 818fc8681..e0d1c8ccc 100644 --- a/metric/metric_test.go +++ b/metric/metric_test.go @@ -53,7 +53,11 @@ func TestUnsafeStringInMetric(t *testing.T) { bytes := []byte("hello world") unsafeString := unsafe.String(unsafe.SliceData(bytes), len(bytes)) - store := newHeldMetricsStore[prometheus.Counter](0) + store := newHeldMetricsStore(0, + func(hm *heldMetric[prometheus.Counter]) *Counter { + return &Counter{heldMetric: hm} + }, + ) labels := []string{unsafeString} m := store.GetOrCreate([]string{unsafeString}, func(s ...string) prometheus.Counter {