Add wrapper for TimingHistogram

Do not bother wrapping WeightedHistogram because it is not used
in k/k.
This commit is contained in:
Mike Spreitzer 2022-04-29 17:24:27 -04:00
parent edac6fce2a
commit 68d9249490
9 changed files with 812 additions and 16 deletions

View File

@ -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

View File

@ -18,6 +18,7 @@ package metrics
import (
"context"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)

View File

@ -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)
}
}

View File

@ -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{}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}
})
}
}

View File

@ -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")

1
vendor/modules.txt vendored
View File

@ -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