From 68d92494907ae1d8a87b2e29cc9cc2a2709ca3a9 Mon Sep 17 00:00:00 2001 From: Mike Spreitzer Date: Fri, 29 Apr 2022 17:24:27 -0400 Subject: [PATCH] Add wrapper for TimingHistogram Do not bother wrapping WeightedHistogram because it is not used in k/k. --- hack/verify-prometheus-imports.sh | 1 + .../component-base/metrics/histogram.go | 1 + .../component-base/metrics/histogram_test.go | 40 +- .../k8s.io/component-base/metrics/metric.go | 25 +- .../src/k8s.io/component-base/metrics/opts.go | 49 +++ .../metrics/timing_histogram.go | 268 +++++++++++++ .../metrics/timing_histogram_test.go | 370 ++++++++++++++++++ .../k8s.io/component-base/metrics/wrappers.go | 73 ++++ vendor/modules.txt | 1 + 9 files changed, 812 insertions(+), 16 deletions(-) create mode 100644 staging/src/k8s.io/component-base/metrics/timing_histogram.go create mode 100644 staging/src/k8s.io/component-base/metrics/timing_histogram_test.go diff --git a/hack/verify-prometheus-imports.sh b/hack/verify-prometheus-imports.sh index d7c689fe72f..3572a3d905f 100755 --- a/hack/verify-prometheus-imports.sh +++ b/hack/verify-prometheus-imports.sh @@ -66,6 +66,7 @@ allowed_prometheus_importers=( ./staging/src/k8s.io/component-base/metrics/testutil/metrics_test.go ./staging/src/k8s.io/component-base/metrics/testutil/promlint.go ./staging/src/k8s.io/component-base/metrics/testutil/testutil.go + ./staging/src/k8s.io/component-base/metrics/timing_histogram_test.go ./staging/src/k8s.io/component-base/metrics/value.go ./staging/src/k8s.io/component-base/metrics/wrappers.go ./test/e2e/apimachinery/flowcontrol.go diff --git a/staging/src/k8s.io/component-base/metrics/histogram.go b/staging/src/k8s.io/component-base/metrics/histogram.go index e93c7a4b3ff..45c5da97217 100644 --- a/staging/src/k8s.io/component-base/metrics/histogram.go +++ b/staging/src/k8s.io/component-base/metrics/histogram.go @@ -18,6 +18,7 @@ package metrics import ( "context" + "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" ) diff --git a/staging/src/k8s.io/component-base/metrics/histogram_test.go b/staging/src/k8s.io/component-base/metrics/histogram_test.go index e563a395ac8..e13cc18281b 100644 --- a/staging/src/k8s.io/component-base/metrics/histogram_test.go +++ b/staging/src/k8s.io/component-base/metrics/histogram_test.go @@ -87,6 +87,19 @@ func TestHistogram(t *testing.T) { }) c := NewHistogram(test.HistogramOpts) registry.MustRegister(c) + cm := c.ObserverMetric.(prometheus.Metric) + + metricChan := make(chan prometheus.Metric, 2) + c.Collect(metricChan) + close(metricChan) + m1 := <-metricChan + if m1 != cm { + t.Error("Unexpected metric", m1, cm) + } + m2, ok := <-metricChan + if ok { + t.Error("Unexpected second metric", m2) + } ms, err := registry.Gather() assert.Equalf(t, test.expectedMetricCount, len(ms), "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) @@ -179,7 +192,24 @@ func TestHistogramVec(t *testing.T) { }) c := NewHistogramVec(test.HistogramOpts, test.labels) registry.MustRegister(c) - c.WithLabelValues("1", "2").Observe(1.0) + ov12 := c.WithLabelValues("1", "2") + cm1 := ov12.(prometheus.Metric) + ov12.Observe(1.0) + + if test.expectedMetricCount > 0 { + metricChan := make(chan prometheus.Metric, 2) + c.Collect(metricChan) + close(metricChan) + m1 := <-metricChan + if m1 != cm1 { + t.Error("Unexpected metric", m1, cm1) + } + m2, ok := <-metricChan + if ok { + t.Error("Unexpected second metric", m2) + } + } + ms, err := registry.Gather() assert.Equalf(t, test.expectedMetricCount, len(ms), "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) assert.Nil(t, err, "Gather failed %v", err) @@ -218,12 +248,12 @@ func TestHistogramWithLabelValueAllowList(t *testing.T) { var tests = []struct { desc string labelValues [][]string - expectMetricValues map[string]int + expectMetricValues map[string]uint64 }{ { desc: "Test no unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"allowed", "b2"}}, - expectMetricValues: map[string]int{ + expectMetricValues: map[string]uint64{ "allowed b1": 1.0, "allowed b2": 1.0, }, @@ -231,7 +261,7 @@ func TestHistogramWithLabelValueAllowList(t *testing.T) { { desc: "Test unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"not_allowed", "b1"}}, - expectMetricValues: map[string]int{ + expectMetricValues: map[string]uint64{ "allowed b1": 1.0, "unexpected b1": 1.0, }, @@ -274,7 +304,7 @@ func TestHistogramWithLabelValueAllowList(t *testing.T) { labelValuePair := aValue + " " + bValue expectedValue, ok := test.expectMetricValues[labelValuePair] assert.True(t, ok, "Got unexpected label values, lable_a is %v, label_b is %v", aValue, bValue) - actualValue := int(m.GetHistogram().GetSampleCount()) + actualValue := m.GetHistogram().GetSampleCount() assert.Equalf(t, expectedValue, actualValue, "Got %v, wanted %v as the count while setting label_a to %v and label b to %v", actualValue, expectedValue, aValue, bValue) } } diff --git a/staging/src/k8s.io/component-base/metrics/metric.go b/staging/src/k8s.io/component-base/metrics/metric.go index c72aecfc6b4..e57e0b383d1 100644 --- a/staging/src/k8s.io/component-base/metrics/metric.go +++ b/staging/src/k8s.io/component-base/metrics/metric.go @@ -22,6 +22,7 @@ import ( "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + promext "k8s.io/component-base/metrics/prometheusextension" "k8s.io/klog/v2" ) @@ -203,6 +204,7 @@ func (c *selfCollector) Collect(ch chan<- prometheus.Metric) { // no-op vecs for convenience var noopCounterVec = &prometheus.CounterVec{} var noopHistogramVec = &prometheus.HistogramVec{} +var noopTimingHistogramVec = &promext.TimingHistogramVec{} var noopGaugeVec = &prometheus.GaugeVec{} var noopObserverVec = &noopObserverVector{} @@ -211,17 +213,18 @@ var noop = &noopMetric{} type noopMetric struct{} -func (noopMetric) Inc() {} -func (noopMetric) Add(float64) {} -func (noopMetric) Dec() {} -func (noopMetric) Set(float64) {} -func (noopMetric) Sub(float64) {} -func (noopMetric) Observe(float64) {} -func (noopMetric) SetToCurrentTime() {} -func (noopMetric) Desc() *prometheus.Desc { return nil } -func (noopMetric) Write(*dto.Metric) error { return nil } -func (noopMetric) Describe(chan<- *prometheus.Desc) {} -func (noopMetric) Collect(chan<- prometheus.Metric) {} +func (noopMetric) Inc() {} +func (noopMetric) Add(float64) {} +func (noopMetric) Dec() {} +func (noopMetric) Set(float64) {} +func (noopMetric) Sub(float64) {} +func (noopMetric) Observe(float64) {} +func (noopMetric) ObserveWithWeight(float64, uint64) {} +func (noopMetric) SetToCurrentTime() {} +func (noopMetric) Desc() *prometheus.Desc { return nil } +func (noopMetric) Write(*dto.Metric) error { return nil } +func (noopMetric) Describe(chan<- *prometheus.Desc) {} +func (noopMetric) Collect(chan<- prometheus.Metric) {} type noopObserverVector struct{} diff --git a/staging/src/k8s.io/component-base/metrics/opts.go b/staging/src/k8s.io/component-base/metrics/opts.go index 04203b74e0a..9d359d6acb6 100644 --- a/staging/src/k8s.io/component-base/metrics/opts.go +++ b/staging/src/k8s.io/component-base/metrics/opts.go @@ -24,6 +24,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/util/sets" + promext "k8s.io/component-base/metrics/prometheusextension" ) var ( @@ -189,6 +190,54 @@ func (o *HistogramOpts) toPromHistogramOpts() prometheus.HistogramOpts { } } +// TimingHistogramOpts bundles the options for creating a TimingHistogram metric. It is +// mandatory to set Name to a non-empty string. All other fields are optional +// and can safely be left at their zero value, although it is strongly +// encouraged to set a Help string. +type TimingHistogramOpts struct { + Namespace string + Subsystem string + Name string + Help string + ConstLabels map[string]string + Buckets []float64 + InitialValue float64 + DeprecatedVersion string + deprecateOnce sync.Once + annotateOnce sync.Once + StabilityLevel StabilityLevel + LabelValueAllowLists *MetricLabelAllowList +} + +// Modify help description on the metric description. +func (o *TimingHistogramOpts) markDeprecated() { + o.deprecateOnce.Do(func() { + o.Help = fmt.Sprintf("(Deprecated since %v) %v", o.DeprecatedVersion, o.Help) + }) +} + +// annotateStabilityLevel annotates help description on the metric description with the stability level +// of the metric +func (o *TimingHistogramOpts) annotateStabilityLevel() { + o.annotateOnce.Do(func() { + o.Help = fmt.Sprintf("[%v] %v", o.StabilityLevel, o.Help) + }) +} + +// convenience function to allow easy transformation to the prometheus +// counterpart. This will do more once we have a proper label abstraction +func (o *TimingHistogramOpts) toPromHistogramOpts() promext.TimingHistogramOpts { + return promext.TimingHistogramOpts{ + Namespace: o.Namespace, + Subsystem: o.Subsystem, + Name: o.Name, + Help: o.Help, + ConstLabels: o.ConstLabels, + Buckets: o.Buckets, + InitialValue: o.InitialValue, + } +} + // SummaryOpts bundles the options for creating a Summary metric. It is // mandatory to set Name to a non-empty string. While all other fields are // optional and can safely be left at their zero value, it is recommended to set diff --git a/staging/src/k8s.io/component-base/metrics/timing_histogram.go b/staging/src/k8s.io/component-base/metrics/timing_histogram.go new file mode 100644 index 00000000000..1472b541dab --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/timing_histogram.go @@ -0,0 +1,268 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "context" + "time" + + "github.com/blang/semver/v4" + promext "k8s.io/component-base/metrics/prometheusextension" +) + +// TimingHistogram is our internal representation for our wrapping struct around timing +// histograms. It implements both kubeCollector and GaugeMetric +type TimingHistogram struct { + GaugeMetric + *TimingHistogramOpts + nowFunc func() time.Time + lazyMetric + selfCollector +} + +// NewTimingHistogram returns an object which is TimingHistogram-like. However, nothing +// will be measured until the histogram is registered somewhere. +func NewTimingHistogram(opts *TimingHistogramOpts) *TimingHistogram { + return NewTestableTimingHistogram(time.Now, opts) +} + +// NewTestableTimingHistogram adds injection of the clock +func NewTestableTimingHistogram(nowFunc func() time.Time, opts *TimingHistogramOpts) *TimingHistogram { + opts.StabilityLevel.setDefaults() + + h := &TimingHistogram{ + TimingHistogramOpts: opts, + nowFunc: nowFunc, + lazyMetric: lazyMetric{}, + } + h.setPrometheusHistogram(noopMetric{}) + h.lazyInit(h, BuildFQName(opts.Namespace, opts.Subsystem, opts.Name)) + return h +} + +// setPrometheusHistogram sets the underlying KubeGauge object, i.e. the thing that does the measurement. +func (h *TimingHistogram) setPrometheusHistogram(histogram promext.TimingHistogram) { + h.GaugeMetric = histogram + h.initSelfCollection(histogram) +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (h *TimingHistogram) DeprecatedVersion() *semver.Version { + return parseSemver(h.TimingHistogramOpts.DeprecatedVersion) +} + +// initializeMetric invokes the actual prometheus.Histogram object instantiation +// and stores a reference to it +func (h *TimingHistogram) initializeMetric() { + h.TimingHistogramOpts.annotateStabilityLevel() + // this actually creates the underlying prometheus gauge. + histogram, err := promext.NewTestableTimingHistogram(h.nowFunc, h.TimingHistogramOpts.toPromHistogramOpts()) + if err != nil { + panic(err) // handle as for regular histograms + } + h.setPrometheusHistogram(histogram) +} + +// initializeDeprecatedMetric invokes the actual prometheus.Histogram object instantiation +// but modifies the Help description prior to object instantiation. +func (h *TimingHistogram) initializeDeprecatedMetric() { + h.TimingHistogramOpts.markDeprecated() + h.initializeMetric() +} + +// WithContext allows the normal TimingHistogram metric to pass in context. The context is no-op now. +func (h *TimingHistogram) WithContext(ctx context.Context) GaugeMetric { + return h.GaugeMetric +} + +// timingHistogramVec is the internal representation of our wrapping struct around prometheus +// TimingHistogramVecs. +type timingHistogramVec struct { + *promext.TimingHistogramVec + *TimingHistogramOpts + nowFunc func() time.Time + lazyMetric + originalLabels []string +} + +// NewTimingHistogramVec returns an object which satisfies kubeCollector and +// wraps the promext.timingHistogramVec object. Note well the way that +// behavior depends on registration and whether this is hidden. +func NewTimingHistogramVec(opts *TimingHistogramOpts, labels []string) PreContextAndRegisterableGaugeMetricVec { + return NewTestableTimingHistogramVec(time.Now, opts, labels) +} + +// NewTestableTimingHistogramVec adds injection of the clock. +func NewTestableTimingHistogramVec(nowFunc func() time.Time, opts *TimingHistogramOpts, labels []string) PreContextAndRegisterableGaugeMetricVec { + opts.StabilityLevel.setDefaults() + + fqName := BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) + allowListLock.RLock() + if allowList, ok := labelValueAllowLists[fqName]; ok { + opts.LabelValueAllowLists = allowList + } + allowListLock.RUnlock() + + v := &timingHistogramVec{ + TimingHistogramVec: noopTimingHistogramVec, + TimingHistogramOpts: opts, + nowFunc: nowFunc, + originalLabels: labels, + lazyMetric: lazyMetric{}, + } + v.lazyInit(v, fqName) + return v +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (v *timingHistogramVec) DeprecatedVersion() *semver.Version { + return parseSemver(v.TimingHistogramOpts.DeprecatedVersion) +} + +func (v *timingHistogramVec) initializeMetric() { + v.TimingHistogramOpts.annotateStabilityLevel() + v.TimingHistogramVec = promext.NewTestableTimingHistogramVec(v.nowFunc, v.TimingHistogramOpts.toPromHistogramOpts(), v.originalLabels...) +} + +func (v *timingHistogramVec) initializeDeprecatedMetric() { + v.TimingHistogramOpts.markDeprecated() + v.initializeMetric() +} + +func (v *timingHistogramVec) Set(value float64, labelValues ...string) { + gm, _ := v.WithLabelValues(labelValues...) + gm.Set(value) +} + +func (v *timingHistogramVec) Inc(labelValues ...string) { + gm, _ := v.WithLabelValues(labelValues...) + gm.Inc() +} + +func (v *timingHistogramVec) Dec(labelValues ...string) { + gm, _ := v.WithLabelValues(labelValues...) + gm.Dec() +} + +func (v *timingHistogramVec) Add(delta float64, labelValues ...string) { + gm, _ := v.WithLabelValues(labelValues...) + gm.Add(delta) +} +func (v *timingHistogramVec) SetToCurrentTime(labelValues ...string) { + gm, _ := v.WithLabelValues(labelValues...) + gm.SetToCurrentTime() +} + +func (v *timingHistogramVec) SetForLabels(value float64, labels map[string]string) { + gm, _ := v.With(labels) + gm.Set(value) +} + +func (v *timingHistogramVec) IncForLabels(labels map[string]string) { + gm, _ := v.With(labels) + gm.Inc() +} + +func (v *timingHistogramVec) DecForLabels(labels map[string]string) { + gm, _ := v.With(labels) + gm.Dec() +} + +func (v *timingHistogramVec) AddForLabels(delta float64, labels map[string]string) { + gm, _ := v.With(labels) + gm.Add(delta) +} +func (v *timingHistogramVec) SetToCurrentTimeForLabels(labels map[string]string) { + gm, _ := v.With(labels) + gm.SetToCurrentTime() +} + +// WithLabelValues, if called after this vector has been +// registered in at least one registry and this vector is not +// hidden, will return a GaugeMetric that is NOT a noop along +// with nil error. If called on a hidden vector then it will +// return a noop and a nil error. Otherwise it returns a noop +// and an error that passes ErrIsNotReady. +func (v *timingHistogramVec) WithLabelValues(lvs ...string) (GaugeMetric, error) { + if v.IsHidden() { + return noop, nil + } + if !v.IsCreated() { + return noop, errNotReady + } + if v.LabelValueAllowLists != nil { + v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs) + } + return v.TimingHistogramVec.WithLabelValues(lvs...).(GaugeMetric), nil +} + +// With, if called after this vector has been +// registered in at least one registry and this vector is not +// hidden, will return a GaugeMetric that is NOT a noop along +// with nil error. If called on a hidden vector then it will +// return a noop and a nil error. Otherwise it returns a noop +// and an error that passes ErrIsNotReady. +func (v *timingHistogramVec) With(labels map[string]string) (GaugeMetric, error) { + if v.IsHidden() { + return noop, nil + } + if !v.IsCreated() { + return noop, errNotReady + } + if v.LabelValueAllowLists != nil { + v.LabelValueAllowLists.ConstrainLabelMap(labels) + } + return v.TimingHistogramVec.With(labels).(GaugeMetric), nil +} + +// Delete deletes the metric where the variable labels are the same as those +// passed in as labels. It returns true if a metric was deleted. +// +// It is not an error if the number and names of the Labels are inconsistent +// with those of the VariableLabels in Desc. However, such inconsistent Labels +// can never match an actual metric, so the method will always return false in +// that case. +func (v *timingHistogramVec) Delete(labels map[string]string) bool { + if !v.IsCreated() { + return false // since we haven't created the metric, we haven't deleted a metric with the passed in values + } + return v.TimingHistogramVec.Delete(labels) +} + +// Reset deletes all metrics in this vector. +func (v *timingHistogramVec) Reset() { + if !v.IsCreated() { + return + } + + v.TimingHistogramVec.Reset() +} + +// WithContext returns wrapped timingHistogramVec with context +func (v *timingHistogramVec) WithContext(ctx context.Context) GaugeMetricVec { + return &TimingHistogramVecWithContext{ + ctx: ctx, + timingHistogramVec: v, + } +} + +// TimingHistogramVecWithContext is the wrapper of timingHistogramVec with context. +// Currently the context is ignored. +type TimingHistogramVecWithContext struct { + *timingHistogramVec + ctx context.Context +} diff --git a/staging/src/k8s.io/component-base/metrics/timing_histogram_test.go b/staging/src/k8s.io/component-base/metrics/timing_histogram_test.go new file mode 100644 index 00000000000..43b5a1489ac --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/timing_histogram_test.go @@ -0,0 +1,370 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "testing" + "time" + + "github.com/blang/semver/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + + apimachineryversion "k8s.io/apimachinery/pkg/version" + testclock "k8s.io/utils/clock/testing" +) + +func TestTimingHistogram(t *testing.T) { + v115 := semver.MustParse("1.15.0") + var tests = []struct { + desc string + *TimingHistogramOpts + registryVersion *semver.Version + expectedMetricCount int + expectedHelp string + }{ + { + desc: "Test non deprecated", + TimingHistogramOpts: &TimingHistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + Buckets: DefBuckets, + InitialValue: 13, + }, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "EXPERIMENTAL: [ALPHA] histogram help message", + }, + { + desc: "Test deprecated", + TimingHistogramOpts: &TimingHistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + DeprecatedVersion: "1.15.0", + Buckets: DefBuckets, + InitialValue: 3, + }, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "EXPERIMENTAL: [ALPHA] (Deprecated since 1.15.0) histogram help message", + }, + { + desc: "Test hidden", + TimingHistogramOpts: &TimingHistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + DeprecatedVersion: "1.14.0", + Buckets: DefBuckets, + InitialValue: 5, + }, + registryVersion: &v115, + expectedMetricCount: 0, + expectedHelp: "EXPERIMENTAL: histogram help message", + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + registry := newKubeRegistry(apimachineryversion.Info{ + Major: "1", + Minor: "15", + GitVersion: "v1.15.0-alpha-1.12345", + }) + t0 := time.Now() + clk := testclock.NewFakePassiveClock(t0) + c := NewTestableTimingHistogram(clk.Now, test.TimingHistogramOpts) + registry.MustRegister(c) + + metricChan := make(chan prometheus.Metric) + go func() { + c.Collect(metricChan) + close(metricChan) + }() + m1 := <-metricChan + gm1, ok := m1.(GaugeMetric) + if !ok || gm1 != c.GaugeMetric { + t.Error("Unexpected metric", m1, c.GaugeMetric) + } + m2, ok := <-metricChan + if ok { + t.Error("Unexpected second metric", m2) + } + + ms, err := registry.Gather() + assert.Equalf(t, test.expectedMetricCount, len(ms), "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) + assert.Nil(t, err, "Gather failed %v", err) + + for _, metric := range ms { + assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) + } + + // let's exercise the metric and check that it still works + v0 := test.TimingHistogramOpts.InitialValue + dt1 := time.Nanosecond + t1 := t0.Add(dt1) + clk.SetTime(t1) + var v1 float64 = 10 + c.Set(v1) + dt2 := time.Hour + t2 := t1.Add(dt2) + clk.SetTime(t2) + var v2 float64 = 1e6 + c.Add(v2 - v1) + dt3 := time.Microsecond + t3 := t2.Add(dt3) + clk.SetTime(t3) + c.Set(0) + expectedCount := uint64(dt1 + dt2 + dt3) + expectedSum := float64(dt1)*v0 + float64(dt2)*v1 + float64(dt3)*v2 + ms, err = registry.Gather() + assert.Nil(t, err, "Gather failed %v", err) + + for _, mf := range ms { + t.Logf("Considering metric family %s", mf.GetName()) + for _, m := range mf.GetMetric() { + assert.Equalf(t, expectedCount, m.GetHistogram().GetSampleCount(), "Got %v, want %v as the sample count of metric %s", m.GetHistogram().GetSampleCount(), expectedCount, m.String()) + assert.Equalf(t, expectedSum, m.GetHistogram().GetSampleSum(), "Got %v, want %v as the sample sum of metric %s", m.GetHistogram().GetSampleSum(), expectedSum, m.String()) + } + } + }) + } +} + +func TestTimingHistogramVec(t *testing.T) { + v115 := semver.MustParse("1.15.0") + var tests = []struct { + desc string + *TimingHistogramOpts + labels []string + registryVersion *semver.Version + expectedMetricCount int + expectedHelp string + }{ + { + desc: "Test non deprecated", + TimingHistogramOpts: &TimingHistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + Buckets: DefBuckets, + InitialValue: 5, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "EXPERIMENTAL: [ALPHA] histogram help message", + }, + { + desc: "Test deprecated", + TimingHistogramOpts: &TimingHistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + DeprecatedVersion: "1.15.0", + Buckets: DefBuckets, + InitialValue: 13, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "EXPERIMENTAL: [ALPHA] (Deprecated since 1.15.0) histogram help message", + }, + { + desc: "Test hidden", + TimingHistogramOpts: &TimingHistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + DeprecatedVersion: "1.14.0", + Buckets: DefBuckets, + InitialValue: 42, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 0, + expectedHelp: "EXPERIMENTAL: histogram help message", + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + registry := newKubeRegistry(apimachineryversion.Info{ + Major: "1", + Minor: "15", + GitVersion: "v1.15.0-alpha-1.12345", + }) + t0 := time.Now() + clk := testclock.NewFakePassiveClock(t0) + c := NewTestableTimingHistogramVec(clk.Now, test.TimingHistogramOpts, test.labels) + registry.MustRegister(c) + var v0 float64 = 3 + cm1, err := c.WithLabelValues("1", "2") + if err != nil { + t.Error(err) + } + cm1.Set(v0) + + if test.expectedMetricCount > 0 { + metricChan := make(chan prometheus.Metric, 2) + c.Collect(metricChan) + close(metricChan) + m1 := <-metricChan + if m1 != cm1.(prometheus.Metric) { + t.Error("Unexpected metric", m1, cm1) + } + m2, ok := <-metricChan + if ok { + t.Error("Unexpected second metric", m2) + } + } + + ms, err := registry.Gather() + assert.Equalf(t, test.expectedMetricCount, len(ms), "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) + assert.Nil(t, err, "Gather failed %v", err) + for _, metric := range ms { + if metric.GetHelp() != test.expectedHelp { + assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) + } + } + + // let's exercise the metric and verify it still works + c.Set(v0, "1", "3") + c.Set(v0, "2", "3") + dt1 := time.Nanosecond + t1 := t0.Add(dt1) + clk.SetTime(t1) + c.Add(5.0, "1", "2") + c.Add(5.0, "1", "3") + c.Add(5.0, "2", "3") + ms, err = registry.Gather() + assert.Nil(t, err, "Gather failed %v", err) + + for _, mf := range ms { + t.Logf("Considering metric family %s", mf.String()) + assert.Equalf(t, 3, len(mf.GetMetric()), "Got %v metrics, wanted 3 as the count for family %#+v", len(mf.GetMetric()), mf) + for _, m := range mf.GetMetric() { + expectedCount := uint64(dt1) + expectedSum := float64(dt1) * v0 + assert.Equalf(t, expectedCount, m.GetHistogram().GetSampleCount(), "Got %v, expected histogram sample count to equal %d for metric %s", m.GetHistogram().GetSampleCount(), expectedCount, m.String()) + assert.Equalf(t, expectedSum, m.GetHistogram().GetSampleSum(), "Got %v, expected histogram sample sum to equal %v for metric %s", m.GetHistogram().GetSampleSum(), expectedSum, m.String()) + } + } + }) + } +} + +func TestTimingHistogramWithLabelValueAllowList(t *testing.T) { + labelAllowValues := map[string]string{ + "namespace_subsystem_metric_allowlist_test,label_a": "allowed", + } + labels := []string{"label_a", "label_b"} + opts := &TimingHistogramOpts{ + Namespace: "namespace", + Name: "metric_allowlist_test", + Subsystem: "subsystem", + InitialValue: 7, + } + var tests = []struct { + desc string + labelValues [][]string + expectMetricValues map[string]uint64 + }{ + { + desc: "Test no unexpected input", + labelValues: [][]string{{"allowed", "b1"}, {"allowed", "b2"}}, + expectMetricValues: map[string]uint64{ + "allowed b1": 1.0, + "allowed b2": 1.0, + }, + }, + { + desc: "Test unexpected input", + labelValues: [][]string{{"allowed", "b1"}, {"not_allowed", "b1"}}, + expectMetricValues: map[string]uint64{ + "allowed b1": 1.0, + "unexpected b1": 1.0, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + SetLabelAllowListFromCLI(labelAllowValues) + registry := newKubeRegistry(apimachineryversion.Info{ + Major: "1", + Minor: "15", + GitVersion: "v1.15.0-alpha-1.12345", + }) + t0 := time.Now() + clk := testclock.NewFakePassiveClock(t0) + c := NewTestableTimingHistogramVec(clk.Now, opts, labels) + registry.MustRegister(c) + var v0 float64 = 13 + for _, lv := range test.labelValues { + c.Set(v0, lv...) + } + + dt1 := 3 * time.Hour + t1 := t0.Add(dt1) + clk.SetTime(t1) + + for _, lv := range test.labelValues { + c.Add(1.0, lv...) + } + mfs, err := registry.Gather() + assert.Nil(t, err, "Gather failed %v", err) + + for _, mf := range mfs { + if *mf.Name != BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) { + continue + } + mfMetric := mf.GetMetric() + t.Logf("Consider metric family %s", mf.GetName()) + + for _, m := range mfMetric { + var aValue, bValue string + for _, l := range m.Label { + if *l.Name == "label_a" { + aValue = *l.Value + } + if *l.Name == "label_b" { + bValue = *l.Value + } + } + labelValuePair := aValue + " " + bValue + expectedCount, ok := test.expectMetricValues[labelValuePair] + assert.True(t, ok, "Got unexpected label values, lable_a is %v, label_b is %v", aValue, bValue) + expectedSum := float64(dt1) * v0 * float64(expectedCount) + expectedCount *= uint64(dt1) + actualCount := m.GetHistogram().GetSampleCount() + actualSum := m.GetHistogram().GetSampleSum() + assert.Equalf(t, expectedCount, actualCount, "Got %v, wanted %v as the count while setting label_a to %v and label b to %v", actualCount, expectedCount, aValue, bValue) + assert.Equalf(t, expectedSum, actualSum, "Got %v, wanted %v as the sum while setting label_a to %v and label b to %v", actualSum, expectedSum, aValue, bValue) + } + } + }) + } +} diff --git a/staging/src/k8s.io/component-base/metrics/wrappers.go b/staging/src/k8s.io/component-base/metrics/wrappers.go index 6ae8a458acb..4fb5e4d87af 100644 --- a/staging/src/k8s.io/component-base/metrics/wrappers.go +++ b/staging/src/k8s.io/component-base/metrics/wrappers.go @@ -17,6 +17,9 @@ limitations under the License. package metrics import ( + "context" + "errors" + "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) @@ -65,6 +68,70 @@ type GaugeMetric interface { SetToCurrentTime() } +// GaugeMetricVec is a collection of Gauges that differ only in label values. +// This is really just one Metric. +// It might be better called GaugeVecMetric, but that pattern of name is already +// taken by the other pattern --- which is treacherous. The treachery is that +// WithLabelValues can return an object that is permanently broken (i.e., a noop). +type GaugeMetricVec interface { + Set(value float64, labelValues ...string) + Inc(labelValues ...string) + Dec(labelValues ...string) + Add(delta float64, labelValues ...string) + SetToCurrentTime(labelValues ...string) + + SetForLabels(value float64, labels map[string]string) + IncForLabels(labels map[string]string) + DecForLabels(labels map[string]string) + AddForLabels(delta float64, labels map[string]string) + SetToCurrentTimeForLabels(labels map[string]string) + + // WithLabelValues, if called after this vector has been + // registered in at least one registry and this vector is not + // hidden, will return a GaugeMetric that is NOT a noop along + // with nil error. If called on a hidden vector then it will + // return a noop and a nil error. Otherwise it returns a noop + // and an error that passes ErrIsNotReady. + WithLabelValues(labelValues ...string) (GaugeMetric, error) + + // With, if called after this vector has been + // registered in at least one registry and this vector is not + // hidden, will return a GaugeMetric that is NOT a noop along + // with nil error. If called on a hidden vector then it will + // return a noop and a nil error. Otherwise it returns a noop + // and an error that passes ErrIsNotReady. + With(labels map[string]string) (GaugeMetric, error) + + // Delete asserts that the vec should have no member for the given label set. + // The returned bool indicates whether there was a change. + // The return will certainly be `false` if the given label set has the wrong + // set of label names. + Delete(map[string]string) bool + + // Reset removes all the members + Reset() +} + +// PreContextGaugeMetricVec is something that can construct a GaugeMetricVec +// that uses a given Context. +type PreContextGaugeMetricVec interface { + // WithContext creates a GaugeMetricVec that uses the given Context + WithContext(ctx context.Context) GaugeMetricVec +} + +// RegisterableGaugeMetricVec is the intersection of Registerable and GaugeMetricVec +type RegisterableGaugeMetricVec interface { + Registerable + GaugeMetricVec +} + +// PreContextAndRegisterableGaugeMetricVec is the intersection of +// PreContextGaugeMetricVec and RegisterableGaugeMetricVec +type PreContextAndRegisterableGaugeMetricVec interface { + PreContextGaugeMetricVec + RegisterableGaugeMetricVec +} + // ObserverMetric captures individual observations. type ObserverMetric interface { Observe(float64) @@ -93,3 +160,9 @@ type GaugeFunc interface { Metric Collector } + +func ErrIsNotReady(err error) bool { + return err == errNotReady +} + +var errNotReady = errors.New("metric vec is not registered yet") diff --git a/vendor/modules.txt b/vendor/modules.txt index f3860c0c9ff..b8d202bf04b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1954,6 +1954,7 @@ k8s.io/component-base/metrics/prometheus/ratelimiter k8s.io/component-base/metrics/prometheus/restclient k8s.io/component-base/metrics/prometheus/version k8s.io/component-base/metrics/prometheus/workqueue +k8s.io/component-base/metrics/prometheusextension k8s.io/component-base/metrics/testutil k8s.io/component-base/term k8s.io/component-base/traces