1package metricsutil
2
3import (
4	"strings"
5	"sync/atomic"
6	"time"
7
8	metrics "github.com/armon/go-metrics"
9	"github.com/hashicorp/vault/helper/namespace"
10)
11
12// ClusterMetricSink serves as a shim around go-metrics
13// and inserts a "cluster" label.
14//
15// It also provides a mechanism to limit the cardinality of the labels on a gauge
16// (at each reporting interval, which isn't sufficient if there is variability in which
17// labels are the top N) and a backoff mechanism for gauge computation.
18type ClusterMetricSink struct {
19	// ClusterName is either the cluster ID, or a name provided
20	// in the telemetry configuration stanza.
21	//
22	// Because it may be set after the Core is initialized, we need
23	// to protect against concurrent access.
24	ClusterName atomic.Value
25
26	MaxGaugeCardinality int
27	GaugeInterval       time.Duration
28
29	// Sink is the go-metrics instance to send to.
30	Sink metrics.MetricSink
31
32	// Constants that are helpful for metrics within the metrics sink
33	TelemetryConsts TelemetryConstConfig
34}
35
36type TelemetryConstConfig struct {
37	LeaseMetricsEpsilon         time.Duration
38	NumLeaseMetricsTimeBuckets  int
39	LeaseMetricsNameSpaceLabels bool
40}
41
42type Metrics interface {
43	SetGaugeWithLabels(key []string, val float32, labels []Label)
44	IncrCounterWithLabels(key []string, val float32, labels []Label)
45	AddSampleWithLabels(key []string, val float32, labels []Label)
46	AddDurationWithLabels(key []string, d time.Duration, labels []Label)
47	MeasureSinceWithLabels(key []string, start time.Time, labels []Label)
48}
49
50var _ Metrics = &ClusterMetricSink{}
51
52// Convenience alias
53type Label = metrics.Label
54
55func (m *ClusterMetricSink) SetGauge(key []string, val float32) {
56	m.Sink.SetGaugeWithLabels(key, val, []Label{{"cluster", m.ClusterName.Load().(string)}})
57}
58
59func (m *ClusterMetricSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {
60	m.Sink.SetGaugeWithLabels(key, val,
61		append(labels, Label{"cluster", m.ClusterName.Load().(string)}))
62}
63
64func (m *ClusterMetricSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {
65	m.Sink.IncrCounterWithLabels(key, val,
66		append(labels, Label{"cluster", m.ClusterName.Load().(string)}))
67}
68
69func (m *ClusterMetricSink) AddSample(key []string, val float32) {
70	m.Sink.AddSampleWithLabels(key, val, []Label{{"cluster", m.ClusterName.Load().(string)}})
71}
72
73func (m *ClusterMetricSink) AddSampleWithLabels(key []string, val float32, labels []Label) {
74	m.Sink.AddSampleWithLabels(key, val,
75		append(labels, Label{"cluster", m.ClusterName.Load().(string)}))
76}
77
78func (m *ClusterMetricSink) AddDurationWithLabels(key []string, d time.Duration, labels []Label) {
79	val := float32(d) / float32(time.Millisecond)
80	m.AddSampleWithLabels(key, val, labels)
81}
82
83func (m *ClusterMetricSink) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) {
84	elapsed := time.Now().Sub(start)
85	val := float32(elapsed) / float32(time.Millisecond)
86	m.AddSampleWithLabels(key, val, labels)
87}
88
89// BlackholeSink is a default suitable for use in unit tests.
90func BlackholeSink() *ClusterMetricSink {
91	conf := metrics.DefaultConfig("")
92	conf.EnableRuntimeMetrics = false
93	sink, _ := metrics.New(conf, &metrics.BlackholeSink{})
94	cms := &ClusterMetricSink{
95		ClusterName: atomic.Value{},
96		Sink:        sink,
97	}
98	cms.ClusterName.Store("")
99	return cms
100}
101
102func NewClusterMetricSink(clusterName string, sink metrics.MetricSink) *ClusterMetricSink {
103	cms := &ClusterMetricSink{
104		ClusterName:     atomic.Value{},
105		Sink:            sink,
106		TelemetryConsts: TelemetryConstConfig{},
107	}
108	cms.ClusterName.Store(clusterName)
109	return cms
110}
111
112// SetDefaultClusterName changes the cluster name from its default value,
113// if it has not previously been configured.
114func (m *ClusterMetricSink) SetDefaultClusterName(clusterName string) {
115	// This is not a true compare-and-swap, but it should be
116	// consistent enough for normal uses
117	if m.ClusterName.Load().(string) == "" {
118		m.ClusterName.Store(clusterName)
119	}
120}
121
122// NamespaceLabel creates a metrics label for the given
123// Namespace: root is "root"; others are path with the
124// final '/' removed.
125func NamespaceLabel(ns *namespace.Namespace) metrics.Label {
126	switch {
127	case ns == nil:
128		return metrics.Label{"namespace", "root"}
129	case ns.ID == namespace.RootNamespaceID:
130		return metrics.Label{"namespace", "root"}
131	default:
132		return metrics.Label{
133			"namespace",
134			strings.Trim(ns.Path, "/"),
135		}
136	}
137}
138