Merge pull request #109729 from MikeSpreitzer/wrap-weighted-histograms

Wrap weighted histograms
This commit is contained in:
Kubernetes Prow Robot
2022-05-12 12:32:32 -07:00
committed by GitHub
12 changed files with 963 additions and 41 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"
dto "github.com/prometheus/client_model/go"
@@ -106,9 +107,14 @@ type CounterVec struct {
originalLabels []string
}
// NewCounterVec returns an object which satisfies the kubeCollector and CounterVecMetric interfaces.
var _ kubeCollector = &CounterVec{}
// TODO: make this true: var _ CounterVecMetric = &CounterVec{}
// NewCounterVec returns an object which satisfies the kubeCollector and (almost) CounterVecMetric interfaces.
// However, the object returned will not measure anything unless the collector is first
// registered, since the metric is lazily instantiated.
// registered, since the metric is lazily instantiated, and only members extracted after
// registration will actually measure anything.
func NewCounterVec(opts *CounterOpts, labels []string) *CounterVec {
opts.StabilityLevel.setDefaults()
@@ -149,13 +155,16 @@ func (v *CounterVec) initializeDeprecatedMetric() {
v.initializeMetric()
}
// Default Prometheus behavior actually results in the creation of a new metric
// if a metric with the unique label values is not found in the underlying stored metricMap.
// Default Prometheus Vec behavior is that member extraction results in creation of a new element
// if one with the unique label values is not found in the underlying stored metricMap.
// This means that if this function is called but the underlying metric is not registered
// (which means it will never be exposed externally nor consumed), the metric will exist in memory
// for perpetuity (i.e. throughout application lifecycle).
//
// For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/counter.go#L179-L197
//
// In contrast, the Vec behavior in this package is that member extraction before registration
// returns a permanent noop object.
// WithLabelValues returns the Counter for the given slice of label
// values (same order as the VariableLabels in Desc). If that combination of

View File

@@ -18,6 +18,7 @@ package metrics
import (
"context"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
@@ -33,7 +34,11 @@ type Gauge struct {
selfCollector
}
// NewGauge returns an object which satisfies the kubeCollector and KubeGauge interfaces.
var _ GaugeMetric = &Gauge{}
var _ Registerable = &Gauge{}
var _ kubeCollector = &Gauge{}
// NewGauge returns an object which satisfies the kubeCollector, Registerable, and Gauge interfaces.
// However, the object returned will not measure anything unless the collector is first
// registered, since the metric is lazily instantiated.
func NewGauge(opts *GaugeOpts) *Gauge {
@@ -88,9 +93,14 @@ type GaugeVec struct {
originalLabels []string
}
// NewGaugeVec returns an object which satisfies the kubeCollector and KubeGaugeVec interfaces.
var _ GaugeVecMetric = &GaugeVec{}
var _ Registerable = &GaugeVec{}
var _ kubeCollector = &GaugeVec{}
// NewGaugeVec returns an object which satisfies the kubeCollector, Registerable, and GaugeVecMetric interfaces.
// However, the object returned will not measure anything unless the collector is first
// registered, since the metric is lazily instantiated.
// registered, since the metric is lazily instantiated, and only members extracted after
// registration will actually measure anything.
func NewGaugeVec(opts *GaugeOpts, labels []string) *GaugeVec {
opts.StabilityLevel.setDefaults()
@@ -130,26 +140,55 @@ func (v *GaugeVec) initializeDeprecatedMetric() {
v.initializeMetric()
}
// Default Prometheus behavior actually results in the creation of a new metric
// if a metric with the unique label values is not found in the underlying stored metricMap.
func (v *GaugeVec) WithLabelValuesChecked(lvs ...string) (GaugeMetric, error) {
if !v.IsCreated() {
if v.IsHidden() {
return noop, nil
}
return noop, errNotRegistered // return no-op gauge
}
if v.LabelValueAllowLists != nil {
v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs)
}
elt, err := v.GaugeVec.GetMetricWithLabelValues(lvs...)
return elt, err
}
// Default Prometheus Vec behavior is that member extraction results in creation of a new element
// if one with the unique label values is not found in the underlying stored metricMap.
// This means that if this function is called but the underlying metric is not registered
// (which means it will never be exposed externally nor consumed), the metric will exist in memory
// for perpetuity (i.e. throughout application lifecycle).
//
// For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/gauge.go#L190-L208
//
// In contrast, the Vec behavior in this package is that member extraction before registration
// returns a permanent noop object.
// WithLabelValues returns the GaugeMetric for the given slice of label
// values (same order as the VariableLabels in Desc). If that combination of
// label values is accessed for the first time, a new GaugeMetric is created IFF the gaugeVec
// has been registered to a metrics registry.
func (v *GaugeVec) WithLabelValues(lvs ...string) GaugeMetric {
ans, err := v.WithLabelValuesChecked(lvs...)
if err == nil || ErrIsNotRegistered(err) {
return ans
}
panic(err)
}
func (v *GaugeVec) WithChecked(labels map[string]string) (GaugeMetric, error) {
if !v.IsCreated() {
return noop // return no-op gauge
if v.IsHidden() {
return noop, nil
}
return noop, errNotRegistered // return no-op gauge
}
if v.LabelValueAllowLists != nil {
v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs)
v.LabelValueAllowLists.ConstrainLabelMap(labels)
}
return v.GaugeVec.WithLabelValues(lvs...)
elt, err := v.GaugeVec.GetMetricWith(labels)
return elt, err
}
// With returns the GaugeMetric for the given Labels map (the label names
@@ -157,13 +196,11 @@ func (v *GaugeVec) WithLabelValues(lvs ...string) GaugeMetric {
// accessed for the first time, a new GaugeMetric is created IFF the gaugeVec has
// been registered to a metrics registry.
func (v *GaugeVec) With(labels map[string]string) GaugeMetric {
if !v.IsCreated() {
return noop // return no-op gauge
ans, err := v.WithChecked(labels)
if err == nil || ErrIsNotRegistered(err) {
return ans
}
if v.LabelValueAllowLists != nil {
v.LabelValueAllowLists.ConstrainLabelMap(labels)
}
return v.GaugeVec.With(labels)
panic(err)
}
// Delete deletes the metric where the variable labels are the same as those
@@ -219,6 +256,10 @@ func (v *GaugeVec) WithContext(ctx context.Context) *GaugeVecWithContext {
}
}
func (v *GaugeVec) InterfaceWithContext(ctx context.Context) GaugeVecMetric {
return v.WithContext(ctx)
}
// GaugeVecWithContext is the wrapper of GaugeVec with context.
type GaugeVecWithContext struct {
*GaugeVec

View File

@@ -18,6 +18,7 @@ package metrics
import (
"context"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
@@ -100,7 +101,10 @@ type HistogramVec struct {
// NewHistogramVec returns an object which satisfies kubeCollector and wraps the
// prometheus.HistogramVec object. However, the object returned will not measure
// anything unless the collector is first registered, since the metric is lazily instantiated.
// anything unless the collector is first registered, since the metric is lazily instantiated,
// and only members extracted after
// registration will actually measure anything.
func NewHistogramVec(opts *HistogramOpts, labels []string) *HistogramVec {
opts.StabilityLevel.setDefaults()
@@ -136,13 +140,16 @@ func (v *HistogramVec) initializeDeprecatedMetric() {
v.initializeMetric()
}
// Default Prometheus behavior actually results in the creation of a new metric
// if a metric with the unique label values is not found in the underlying stored metricMap.
// Default Prometheus Vec behavior is that member extraction results in creation of a new element
// if one with the unique label values is not found in the underlying stored metricMap.
// This means that if this function is called but the underlying metric is not registered
// (which means it will never be exposed externally nor consumed), the metric will exist in memory
// for perpetuity (i.e. throughout application lifecycle).
//
// For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/histogram.go#L460-L470
//
// In contrast, the Vec behavior in this package is that member extraction before registration
// returns a permanent noop object.
// WithLabelValues returns the ObserverMetric for the given slice of label
// values (same order as the VariableLabels in Desc). If that combination of

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

@@ -18,6 +18,7 @@ package metrics
import (
"context"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
@@ -93,7 +94,9 @@ type SummaryVec struct {
// NewSummaryVec returns an object which satisfies kubeCollector and wraps the
// prometheus.SummaryVec object. However, the object returned will not measure
// anything unless the collector is first registered, since the metric is lazily instantiated.
// anything unless the collector is first registered, since the metric is lazily instantiated,
// and only members extracted after
// registration will actually measure anything.
//
// DEPRECATED: as per the metrics overhaul KEP
func NewSummaryVec(opts *SummaryOpts, labels []string) *SummaryVec {
@@ -130,13 +133,16 @@ func (v *SummaryVec) initializeDeprecatedMetric() {
v.initializeMetric()
}
// Default Prometheus behavior actually results in the creation of a new metric
// if a metric with the unique label values is not found in the underlying stored metricMap.
// Default Prometheus Vec behavior is that member extraction results in creation of a new element
// if one with the unique label values is not found in the underlying stored metricMap.
// This means that if this function is called but the underlying metric is not registered
// (which means it will never be exposed externally nor consumed), the metric will exist in memory
// for perpetuity (i.e. throughout application lifecycle).
//
// For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/summary.go#L485-L495
// For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/histogram.go#L460-L470
//
// In contrast, the Vec behavior in this package is that member extraction before registration
// returns a permanent noop object.
// WithLabelValues returns the ObserverMetric for the given slice of label
// values (same order as the VariableLabels in Desc). If that combination of

View File

@@ -0,0 +1,267 @@
/*
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"
)
// PrometheusTimingHistogram is the abstraction of the underlying histogram
// that we want to promote from the wrapper.
type PrometheusTimingHistogram interface {
GaugeMetric
}
// TimingHistogram is our internal representation for our wrapping struct around
// timing histograms. It implements both kubeCollector and GaugeMetric
type TimingHistogram struct {
PrometheusTimingHistogram
*TimingHistogramOpts
nowFunc func() time.Time
lazyMetric
selfCollector
}
var _ GaugeMetric = &TimingHistogram{}
var _ Registerable = &TimingHistogram{}
var _ kubeCollector = &TimingHistogram{}
// 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.PrometheusTimingHistogram = 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.PrometheusTimingHistogram
}
// 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
}
var _ GaugeVecMetric = &TimingHistogramVec{}
var _ Registerable = &TimingHistogramVec{}
var _ kubeCollector = &TimingHistogramVec{}
// NewTimingHistogramVec returns an object which satisfies the kubeCollector, Registerable, and GaugeVecMetric interfaces
// and wraps an underlying promext.TimingHistogramVec object. Note well the way that
// behavior depends on registration and whether this is hidden.
func NewTimingHistogramVec(opts *TimingHistogramOpts, labels []string) *TimingHistogramVec {
return NewTestableTimingHistogramVec(time.Now, opts, labels)
}
// NewTestableTimingHistogramVec adds injection of the clock.
func NewTestableTimingHistogramVec(nowFunc func() time.Time, opts *TimingHistogramOpts, labels []string) *TimingHistogramVec {
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()
}
// WithLabelValuesChecked, if called before this vector has been registered in
// at least one registry, will return a noop gauge and
// an error that passes ErrIsNotRegistered.
// If called on a hidden vector,
// will return a noop gauge and a nil error.
// If called with a syntactic problem in the labels, will
// return a noop gauge and an error about the labels.
// If none of the above apply, this method will return
// the appropriate vector member and a nil error.
func (v *TimingHistogramVec) WithLabelValuesChecked(lvs ...string) (GaugeMetric, error) {
if !v.IsCreated() {
if v.IsHidden() {
return noop, nil
}
return noop, errNotRegistered
}
if v.LabelValueAllowLists != nil {
v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs)
}
ops, err := v.TimingHistogramVec.GetMetricWithLabelValues(lvs...)
return ops.(GaugeMetric), err
}
// WithLabelValues calls WithLabelValuesChecked
// and handles errors as follows.
// An error that passes ErrIsNotRegistered is ignored
// and the noop gauge is returned;
// all other errors cause a panic.
func (v *TimingHistogramVec) WithLabelValues(lvs ...string) GaugeMetric {
ans, err := v.WithLabelValuesChecked(lvs...)
if err == nil || ErrIsNotRegistered(err) {
return ans
}
panic(err)
}
// WithChecked, if called before this vector has been registered in
// at least one registry, will return a noop gauge and
// an error that passes ErrIsNotRegistered.
// If called on a hidden vector,
// will return a noop gauge and a nil error.
// If called with a syntactic problem in the labels, will
// return a noop gauge and an error about the labels.
// If none of the above apply, this method will return
// the appropriate vector member and a nil error.
func (v *TimingHistogramVec) WithChecked(labels map[string]string) (GaugeMetric, error) {
if !v.IsCreated() {
if v.IsHidden() {
return noop, nil
}
return noop, errNotRegistered
}
if v.LabelValueAllowLists != nil {
v.LabelValueAllowLists.ConstrainLabelMap(labels)
}
ops, err := v.TimingHistogramVec.GetMetricWith(labels)
return ops.(GaugeMetric), err
}
// With calls WithChecked and handles errors as follows.
// An error that passes ErrIsNotRegistered is ignored
// and the noop gauge is returned;
// all other errors cause a panic.
func (v *TimingHistogramVec) With(labels map[string]string) GaugeMetric {
ans, err := v.WithChecked(labels)
if err == nil || ErrIsNotRegistered(err) {
return ans
}
panic(err)
}
// 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) InterfaceWithContext(ctx context.Context) GaugeVecMetric {
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,442 @@
/*
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.PrometheusTimingHistogram {
t.Error("Unexpected metric", m1, c.PrometheusTimingHistogram)
}
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.WithLabelValuesChecked("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.WithLabelValues("1", "3").Set(v0)
c.WithLabelValues("2", "3").Set(v0)
dt1 := time.Nanosecond
t1 := t0.Add(dt1)
clk.SetTime(t1)
c.WithLabelValues("1", "2").Add(5.0)
c.WithLabelValues("1", "3").Add(5.0)
c.WithLabelValues("2", "3").Add(5.0)
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.WithLabelValues(lv...).Set(v0)
}
dt1 := 3 * time.Hour
t1 := t0.Add(dt1)
clk.SetTime(t1)
for _, lv := range test.labelValues {
c.WithLabelValues(lv...).Add(1.0)
}
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)
}
}
})
}
}
func BenchmarkTimingHistogram(b *testing.B) {
b.StopTimer()
now := time.Now()
th := NewTestableTimingHistogram(func() time.Time { return now }, &TimingHistogramOpts{
Namespace: "testns",
Subsystem: "testsubsys",
Name: "testhist",
Help: "Me",
Buckets: []float64{1, 2, 4, 8, 16},
InitialValue: 3,
})
registry := NewKubeRegistry()
registry.MustRegister(th)
var x int
b.StartTimer()
for i := 0; i < b.N; i++ {
now = now.Add(time.Duration(31-x) * time.Microsecond)
th.Set(float64(x))
x = (x + i) % 23
}
}
func BenchmarkTimingHistogramVecEltCached(b *testing.B) {
b.StopTimer()
now := time.Now()
hv := NewTestableTimingHistogramVec(func() time.Time { return now }, &TimingHistogramOpts{
Namespace: "testns",
Subsystem: "testsubsys",
Name: "testhist",
Help: "Me",
Buckets: []float64{1, 2, 4, 8, 16},
InitialValue: 3,
},
[]string{"label1", "label2"})
registry := NewKubeRegistry()
registry.MustRegister(hv)
th, err := hv.WithLabelValuesChecked("v1", "v2")
if err != nil {
b.Error(err)
}
var x int
b.StartTimer()
for i := 0; i < b.N; i++ {
now = now.Add(time.Duration(31-x) * time.Microsecond)
th.Set(float64(x))
x = (x + i) % 23
}
}
func BenchmarkTimingHistogramVecEltFetched(b *testing.B) {
b.StopTimer()
now := time.Now()
hv := NewTestableTimingHistogramVec(func() time.Time { return now }, &TimingHistogramOpts{
Namespace: "testns",
Subsystem: "testsubsys",
Name: "testhist",
Help: "Me",
Buckets: []float64{1, 2, 4, 8, 16},
InitialValue: 3,
},
[]string{"label1", "label2"})
registry := NewKubeRegistry()
registry.MustRegister(hv)
var x int
b.StartTimer()
for i := 0; i < b.N; i++ {
now = now.Add(time.Duration(31-x) * time.Microsecond)
hv.WithLabelValues("v1", "v2").Set(float64(x))
x = (x + i) % 60
}
}

View File

@@ -17,6 +17,8 @@ limitations under the License.
package metrics
import (
"errors"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
@@ -65,6 +67,64 @@ type GaugeMetric interface {
SetToCurrentTime()
}
// GaugeVecMetric is a collection of Gauges that differ only in label values.
type GaugeVecMetric interface {
// Default Prometheus Vec behavior is that member extraction results in creation of a new element
// if one with the unique label values is not found in the underlying stored metricMap.
// This means that if this function is called but the underlying metric is not registered
// (which means it will never be exposed externally nor consumed), the metric would exist in memory
// for perpetuity (i.e. throughout application lifecycle).
//
// For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/gauge.go#L190-L208
//
// In contrast, the Vec behavior in this package is that member extraction before registration
// returns a permanent noop object.
// WithLabelValuesChecked, if called before this vector has been registered in
// at least one registry, will return a noop gauge and
// an error that passes ErrIsNotRegistered.
// If called on a hidden vector,
// will return a noop gauge and a nil error.
// If called with a syntactic problem in the labels, will
// return a noop gauge and an error about the labels.
// If none of the above apply, this method will return
// the appropriate vector member and a nil error.
WithLabelValuesChecked(labelValues ...string) (GaugeMetric, error)
// WithLabelValues calls WithLabelValuesChecked
// and handles errors as follows.
// An error that passes ErrIsNotRegistered is ignored
// and the noop gauge is returned;
// all other errors cause a panic.
WithLabelValues(labelValues ...string) GaugeMetric
// WithChecked, if called before this vector has been registered in
// at least one registry, will return a noop gauge and
// an error that passes ErrIsNotRegistered.
// If called on a hidden vector,
// will return a noop gauge and a nil error.
// If called with a syntactic problem in the labels, will
// return a noop gauge and an error about the labels.
// If none of the above apply, this method will return
// the appropriate vector member and a nil error.
WithChecked(labels map[string]string) (GaugeMetric, error)
// With calls WithChecked and handles errors as follows.
// An error that passes ErrIsNotRegistered is ignored
// and the noop gauge is returned;
// all other errors cause a panic.
With(labels map[string]string) GaugeMetric
// 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()
}
// ObserverMetric captures individual observations.
type ObserverMetric interface {
Observe(float64)
@@ -93,3 +153,9 @@ type GaugeFunc interface {
Metric
Collector
}
func ErrIsNotRegistered(err error) bool {
return err == errNotRegistered
}
var errNotRegistered = errors.New("metric vec is not registered yet")

1
vendor/modules.txt vendored
View File

@@ -2070,6 +2070,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