140 lines
4.2 KiB
Go
140 lines
4.2 KiB
Go
package metrics
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
)
|
|
|
|
// TTLMetric is an interface for metrics that support time-to-live cleanup.
|
|
// Any metric that implements Cleanup() can be registered in the TTLRegistry.
|
|
type TTLMetric interface {
|
|
// Cleanup removes stale metric label sets that have exceeded their TTL.
|
|
Cleanup()
|
|
}
|
|
|
|
// TTLGaugeVec wraps a Prometheus GaugeVec and tracks the last update time
|
|
// for each set of labels. When a set of labels is not updated within the TTL,
|
|
// it is automatically removed from the underlying GaugeVec.
|
|
type TTLGaugeVec struct {
|
|
gaugeVec *prometheus.GaugeVec // Underlying Prometheus GaugeVec.
|
|
ttl time.Duration // Duration after which an unused label set is considered stale.
|
|
lastUpdate sync.Map // Map storing last update time for each label set (key is a sorted labels string).
|
|
}
|
|
|
|
// NewTTLGaugeVec creates a new TTLGaugeVec using the provided GaugeOpts, label names, and TTL.
|
|
// The underlying GaugeVec is registered using promauto.
|
|
func NewTTLGaugeVec(opts prometheus.GaugeOpts, labelNames []string, ttl time.Duration) *TTLGaugeVec {
|
|
return &TTLGaugeVec{
|
|
gaugeVec: promauto.NewGaugeVec(opts, labelNames),
|
|
ttl: ttl,
|
|
}
|
|
}
|
|
|
|
// With returns the gauge for the given label set and records the current time
|
|
// as the last update for those labels.
|
|
func (t *TTLGaugeVec) With(labels prometheus.Labels) prometheus.Gauge {
|
|
key := labelsKey(labels)
|
|
t.lastUpdate.Store(key, time.Now())
|
|
return t.gaugeVec.With(labels)
|
|
}
|
|
|
|
// Delete removes the metric associated with the given label set from both the underlying
|
|
// GaugeVec and the lastUpdate tracking map. It returns true if the deletion was successful.
|
|
func (t *TTLGaugeVec) Delete(labels prometheus.Labels) bool {
|
|
key := labelsKey(labels)
|
|
t.lastUpdate.Delete(key)
|
|
return t.gaugeVec.Delete(labels)
|
|
}
|
|
|
|
// Cleanup iterates over all tracked label sets and deletes those that have not been updated
|
|
// within the TTL duration.
|
|
func (t *TTLGaugeVec) Cleanup() {
|
|
now := time.Now()
|
|
t.lastUpdate.Range(func(key, value interface{}) bool {
|
|
if last, ok := value.(time.Time); ok {
|
|
if now.Sub(last) > t.ttl {
|
|
labels := parseLabels(key.(string))
|
|
t.gaugeVec.Delete(labels)
|
|
t.lastUpdate.Delete(key)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// labelsKey creates a deterministic key from a Prometheus labels map.
|
|
// It sorts the keys and concatenates them in the format "key=value" separated by commas.
|
|
func labelsKey(labels prometheus.Labels) string {
|
|
var keys []string
|
|
for k := range labels {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
var parts []string
|
|
for _, k := range keys {
|
|
parts = append(parts, fmt.Sprintf("%s=%s", k, labels[k]))
|
|
}
|
|
return strings.Join(parts, ",")
|
|
}
|
|
|
|
// parseLabels converts a sorted key string back into a Prometheus labels map.
|
|
// The key is expected to be in the format produced by labelsKey.
|
|
func parseLabels(key string) prometheus.Labels {
|
|
labels := prometheus.Labels{}
|
|
parts := strings.Split(key, ",")
|
|
for _, part := range parts {
|
|
kv := strings.SplitN(part, "=", 2)
|
|
if len(kv) == 2 {
|
|
labels[kv[0]] = kv[1]
|
|
}
|
|
}
|
|
return labels
|
|
}
|
|
|
|
// TTLRegistry manages multiple TTLMetric instances and periodically cleans them up.
|
|
type TTLRegistry struct {
|
|
mu sync.RWMutex
|
|
metrics []TTLMetric
|
|
}
|
|
|
|
// NewTTLRegistry creates and returns a new TTLRegistry.
|
|
func NewTTLRegistry() *TTLRegistry {
|
|
return &TTLRegistry{
|
|
metrics: make([]TTLMetric, 0),
|
|
}
|
|
}
|
|
|
|
// Register adds a TTLMetric to the registry for periodic cleanup.
|
|
func (r *TTLRegistry) Register(metric TTLMetric) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.metrics = append(r.metrics, metric)
|
|
}
|
|
|
|
// Cleanup calls the Cleanup method on each registered TTLMetric.
|
|
func (r *TTLRegistry) Cleanup() {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
for _, metric := range r.metrics {
|
|
metric.Cleanup()
|
|
}
|
|
}
|
|
|
|
// StartCleanupLoop starts a background goroutine that periodically cleans up stale metrics.
|
|
// The cleanup is performed at the specified interval.
|
|
func (r *TTLRegistry) StartCleanupLoop(interval time.Duration) {
|
|
go func() {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
r.Cleanup()
|
|
}
|
|
}()
|
|
}
|