// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package metricsutil import ( "strings" "sync/atomic" "time" "github.com/armon/go-metrics" "github.com/hashicorp/vault/helper/namespace" ) // ClusterMetricSink serves as a shim around go-metrics // and inserts a "cluster" label. // // It also provides a mechanism to limit the cardinality of the labels on a gauge // (at each reporting interval, which isn't sufficient if there is variability in which // labels are the top N) and a backoff mechanism for gauge computation. type ClusterMetricSink struct { // ClusterName is either the cluster ID, or a name provided // in the telemetry configuration stanza. // // Because it may be set after the Core is initialized, we need // to protect against concurrent access. ClusterName atomic.Value MaxGaugeCardinality int GaugeInterval time.Duration // Sink is the go-metrics instance to send to. Sink metrics.MetricSink // Constants that are helpful for metrics within the metrics sink TelemetryConsts TelemetryConstConfig } type TelemetryConstConfig struct { LeaseMetricsEpsilon time.Duration NumLeaseMetricsTimeBuckets int LeaseMetricsNameSpaceLabels bool } type Metrics interface { SetGaugeWithLabels(key []string, val float32, labels []Label) IncrCounterWithLabels(key []string, val float32, labels []Label) AddSampleWithLabels(key []string, val float32, labels []Label) AddDurationWithLabels(key []string, d time.Duration, labels []Label) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) } var _ Metrics = &ClusterMetricSink{} // SinkWrapper implements `metricsutil.Metrics` using an instance of // armon/go-metrics `MetricSink` as the underlying implementation. type SinkWrapper struct { metrics.MetricSink } func (s SinkWrapper) AddDurationWithLabels(key []string, d time.Duration, labels []Label) { val := float32(d) / float32(time.Millisecond) s.MetricSink.AddSampleWithLabels(key, val, labels) } func (s SinkWrapper) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { elapsed := time.Now().Sub(start) val := float32(elapsed) / float32(time.Millisecond) s.MetricSink.AddSampleWithLabels(key, val, labels) } var _ Metrics = SinkWrapper{} // Convenience alias type Label = metrics.Label func (m *ClusterMetricSink) SetGauge(key []string, val float32) { m.Sink.SetGaugeWithLabels(key, val, []Label{{"cluster", m.ClusterName.Load().(string)}}) } func (m *ClusterMetricSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { m.Sink.SetGaugeWithLabels(key, val, append(labels, Label{"cluster", m.ClusterName.Load().(string)})) } func (m *ClusterMetricSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { m.Sink.IncrCounterWithLabels(key, val, append(labels, Label{"cluster", m.ClusterName.Load().(string)})) } func (m *ClusterMetricSink) AddSample(key []string, val float32) { m.Sink.AddSampleWithLabels(key, val, []Label{{"cluster", m.ClusterName.Load().(string)}}) } func (m *ClusterMetricSink) AddSampleWithLabels(key []string, val float32, labels []Label) { m.Sink.AddSampleWithLabels(key, val, append(labels, Label{"cluster", m.ClusterName.Load().(string)})) } func (m *ClusterMetricSink) AddDurationWithLabels(key []string, d time.Duration, labels []Label) { val := float32(d) / float32(time.Millisecond) m.AddSampleWithLabels(key, val, labels) } func (m *ClusterMetricSink) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { elapsed := time.Now().Sub(start) val := float32(elapsed) / float32(time.Millisecond) m.AddSampleWithLabels(key, val, labels) } // BlackholeSink is a default suitable for use in unit tests. func BlackholeSink() *ClusterMetricSink { conf := metrics.DefaultConfig("") conf.EnableRuntimeMetrics = false sink, _ := metrics.New(conf, &metrics.BlackholeSink{}) cms := &ClusterMetricSink{ ClusterName: atomic.Value{}, Sink: sink, } cms.ClusterName.Store("") return cms } func NewClusterMetricSink(clusterName string, sink metrics.MetricSink) *ClusterMetricSink { cms := &ClusterMetricSink{ ClusterName: atomic.Value{}, Sink: sink, TelemetryConsts: TelemetryConstConfig{}, } cms.ClusterName.Store(clusterName) return cms } // SetDefaultClusterName changes the cluster name from its default value, // if it has not previously been configured. func (m *ClusterMetricSink) SetDefaultClusterName(clusterName string) { // This is not a true compare-and-swap, but it should be // consistent enough for normal uses if m.ClusterName.Load().(string) == "" { m.ClusterName.Store(clusterName) } } // NamespaceLabel creates a metrics label for the given // Namespace: root is "root"; others are path with the // final '/' removed. func NamespaceLabel(ns *namespace.Namespace) metrics.Label { switch { case ns == nil: return metrics.Label{"namespace", "root"} case ns.ID == namespace.RootNamespaceID: return metrics.Label{"namespace", "root"} default: return metrics.Label{ "namespace", strings.Trim(ns.Path, "/"), } } }