diff --git a/staging/src/k8s.io/component-base/metrics/BUILD b/staging/src/k8s.io/component-base/metrics/BUILD index b13db6f3536..d991cb88c61 100644 --- a/staging/src/k8s.io/component-base/metrics/BUILD +++ b/staging/src/k8s.io/component-base/metrics/BUILD @@ -10,9 +10,12 @@ go_library( name = "go_default_library", srcs = [ "counter.go", + "gauge.go", + "histogram.go", "metric.go", "opts.go", "registry.go", + "summary.go", "version_parser.go", "wrappers.go", ], @@ -31,7 +34,10 @@ go_test( name = "go_default_test", srcs = [ "counter_test.go", + "gauge_test.go", + "histogram_test.go", "registry_test.go", + "summary_test.go", "version_parser_test.go", ], embed = [":go_default_library"], diff --git a/staging/src/k8s.io/component-base/metrics/gauge.go b/staging/src/k8s.io/component-base/metrics/gauge.go new file mode 100644 index 00000000000..a1c6f2f82b5 --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/gauge.go @@ -0,0 +1,150 @@ +/* +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 ( + "github.com/blang/semver" + "github.com/prometheus/client_golang/prometheus" +) + +// Gauge is our internal representation for our wrapping struct around prometheus +// gauges. kubeGauge implements both KubeCollector and KubeGauge. +type Gauge struct { + GaugeMetric + *GaugeOpts + lazyMetric + selfCollector +} + +// NewGauge returns an object which satisfies the KubeCollector and KubeGauge 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 { + // todo: handle defaulting better + if opts.StabilityLevel == "" { + opts.StabilityLevel = ALPHA + } + kc := &Gauge{ + GaugeOpts: opts, + lazyMetric: lazyMetric{}, + } + kc.setPrometheusGauge(noop) + kc.lazyInit(kc) + return kc +} + +// setPrometheusGauge sets the underlying KubeGauge object, i.e. the thing that does the measurement. +func (g *Gauge) setPrometheusGauge(gauge prometheus.Gauge) { + g.GaugeMetric = gauge + g.initSelfCollection(gauge) +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (g *Gauge) DeprecatedVersion() *semver.Version { + return g.GaugeOpts.DeprecatedVersion +} + +// initializeMetric invocation creates the actual underlying Gauge. Until this method is called +// the underlying gauge is a no-op. +func (g *Gauge) initializeMetric() { + g.GaugeOpts.annotateStabilityLevel() + // this actually creates the underlying prometheus gauge. + g.setPrometheusGauge(prometheus.NewGauge(g.GaugeOpts.toPromGaugeOpts())) +} + +// initializeDeprecatedMetric invocation creates the actual (but deprecated) Gauge. Until this method +// is called the underlying gauge is a no-op. +func (g *Gauge) initializeDeprecatedMetric() { + g.GaugeOpts.markDeprecated() + g.initializeMetric() +} + +// GaugeVec is the internal representation of our wrapping struct around prometheus +// gaugeVecs. kubeGaugeVec implements both KubeCollector and KubeGaugeVec. +type GaugeVec struct { + *prometheus.GaugeVec + *GaugeOpts + lazyMetric + originalLabels []string +} + +// NewGaugeVec returns an object which satisfies the KubeCollector and KubeGaugeVec interfaces. +// However, the object returned will not measure anything unless the collector is first +// registered, since the metric is lazily instantiated. +func NewGaugeVec(opts *GaugeOpts, labels []string) *GaugeVec { + // todo: handle defaulting better + if opts.StabilityLevel == "" { + opts.StabilityLevel = ALPHA + } + cv := &GaugeVec{ + GaugeVec: noopGaugeVec, + GaugeOpts: opts, + originalLabels: labels, + lazyMetric: lazyMetric{}, + } + cv.lazyInit(cv) + return cv +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (v *GaugeVec) DeprecatedVersion() *semver.Version { + return v.GaugeOpts.DeprecatedVersion +} + +// initializeMetric invocation creates the actual underlying GaugeVec. Until this method is called +// the underlying gaugeVec is a no-op. +func (v *GaugeVec) initializeMetric() { + v.GaugeOpts.annotateStabilityLevel() + v.GaugeVec = prometheus.NewGaugeVec(v.GaugeOpts.toPromGaugeOpts(), v.originalLabels) +} + +// initializeDeprecatedMetric invocation creates the actual (but deprecated) GaugeVec. Until this method is called +// the underlying gaugeVec is a no-op. +func (v *GaugeVec) initializeDeprecatedMetric() { + v.GaugeOpts.markDeprecated() + 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. +// 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 + +// 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 { + if !v.IsCreated() { + return noop // return no-op gauge + } + return v.GaugeVec.WithLabelValues(lvs...) +} + +// With returns the GaugeMetric for the given Labels map (the label names +// must match those of the VariableLabels in Desc). If that label map is +// 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 prometheus.Labels) GaugeMetric { + if !v.IsCreated() { + return noop // return no-op gauge + } + return v.GaugeVec.With(labels) +} diff --git a/staging/src/k8s.io/component-base/metrics/gauge_test.go b/staging/src/k8s.io/component-base/metrics/gauge_test.go new file mode 100644 index 00000000000..4b26a0a2cdf --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/gauge_test.go @@ -0,0 +1,210 @@ +/* +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 ( + "github.com/blang/semver" + apimachineryversion "k8s.io/apimachinery/pkg/version" + "testing" +) + +func TestGauge(t *testing.T) { + v115 := semver.MustParse("1.15.0") + v114 := semver.MustParse("1.14.0") + var tests = []struct { + desc string + GaugeOpts + registryVersion *semver.Version + expectedMetricCount int + expectedHelp string + }{ + { + desc: "Test non deprecated", + GaugeOpts: GaugeOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "gauge help", + }, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] gauge help", + }, + { + desc: "Test deprecated", + GaugeOpts: GaugeOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "gauge help", + DeprecatedVersion: &v115, + }, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] (Deprecated since 1.15.0) gauge help", + }, + { + desc: "Test hidden", + GaugeOpts: GaugeOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "gauge help", + DeprecatedVersion: &v114, + }, + registryVersion: &v115, + expectedMetricCount: 0, + expectedHelp: "gauge help", + }, + } + + 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", + }) + c := NewGauge(&test.GaugeOpts) + registry.MustRegister(c) + + ms, err := registry.Gather() + if len(ms) != test.expectedMetricCount { + t.Errorf("Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) + } + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, metric := range ms { + if metric.GetHelp() != test.expectedHelp { + t.Errorf("Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) + } + } + + // let's increment the counter and verify that the metric still works + c.Set(100) + c.Set(101) + expected := 101 + ms, err = registry.Gather() + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, mf := range ms { + for _, m := range mf.GetMetric() { + if int(m.GetGauge().GetValue()) != expected { + t.Errorf("Got %v, wanted %v as the count", m.GetGauge().GetValue(), expected) + } + t.Logf("%v\n", m.GetGauge().GetValue()) + } + } + }) + } +} + +func TestGaugeVec(t *testing.T) { + v115 := semver.MustParse("1.15.0") + v114 := semver.MustParse("1.14.0") + var tests = []struct { + desc string + GaugeOpts + labels []string + registryVersion *semver.Version + expectedMetricCount int + expectedHelp string + }{ + { + desc: "Test non deprecated", + GaugeOpts: GaugeOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "gauge help", + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] gauge help", + }, + { + desc: "Test deprecated", + GaugeOpts: GaugeOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "gauge help", + DeprecatedVersion: &v115, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] (Deprecated since 1.15.0) gauge help", + }, + { + desc: "Test hidden", + GaugeOpts: GaugeOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "gauge help", + DeprecatedVersion: &v114, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 0, + expectedHelp: "gauge help", + }, + } + + 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", + }) + c := NewGaugeVec(&test.GaugeOpts, test.labels) + registry.MustRegister(c) + c.WithLabelValues("1", "2").Set(1.0) + ms, err := registry.Gather() + + if len(ms) != test.expectedMetricCount { + t.Errorf("Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) + } + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, metric := range ms { + if metric.GetHelp() != test.expectedHelp { + t.Errorf("Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) + } + } + + // let's increment the counter and verify that the metric still works + c.WithLabelValues("1", "3").Set(1.0) + c.WithLabelValues("2", "3").Set(1.0) + ms, err = registry.Gather() + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, mf := range ms { + if len(mf.GetMetric()) != 3 { + t.Errorf("Got %v metrics, wanted 2 as the count", len(mf.GetMetric())) + } + } + }) + } +} diff --git a/staging/src/k8s.io/component-base/metrics/histogram.go b/staging/src/k8s.io/component-base/metrics/histogram.go new file mode 100644 index 00000000000..ff88ae151e7 --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/histogram.go @@ -0,0 +1,148 @@ +/* +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 ( + "github.com/blang/semver" + "github.com/prometheus/client_golang/prometheus" + "k8s.io/klog" +) + +// Histogram is our internal representation for our wrapping struct around prometheus +// histograms. Summary implements both KubeCollector and ObserverMetric +type Histogram struct { + ObserverMetric + *HistogramOpts + lazyMetric + selfCollector +} + +// NewHistogram returns an object which is Histogram-like. However, nothing +// will be measured until the histogram is registered somewhere. +func NewHistogram(opts *HistogramOpts) *Histogram { + // todo: handle defaulting better + if opts.StabilityLevel == "" { + opts.StabilityLevel = ALPHA + } + h := &Histogram{ + HistogramOpts: opts, + lazyMetric: lazyMetric{}, + } + h.setPrometheusHistogram(noopMetric{}) + h.lazyInit(h) + return h +} + +// setPrometheusHistogram sets the underlying KubeGauge object, i.e. the thing that does the measurement. +func (h *Histogram) setPrometheusHistogram(histogram prometheus.Histogram) { + h.ObserverMetric = histogram + h.initSelfCollection(histogram) +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (h *Histogram) DeprecatedVersion() *semver.Version { + return h.HistogramOpts.DeprecatedVersion +} + +// initializeMetric invokes the actual prometheus.Histogram object instantiation +// and stores a reference to it +func (h *Histogram) initializeMetric() { + h.HistogramOpts.annotateStabilityLevel() + // this actually creates the underlying prometheus gauge. + h.setPrometheusHistogram(prometheus.NewHistogram(h.HistogramOpts.toPromHistogramOpts())) +} + +// initializeDeprecatedMetric invokes the actual prometheus.Histogram object instantiation +// but modifies the Help description prior to object instantiation. +func (h *Histogram) initializeDeprecatedMetric() { + h.HistogramOpts.markDeprecated() + h.initializeMetric() +} + +// HistogramVec is the internal representation of our wrapping struct around prometheus +// histogramVecs. +type HistogramVec struct { + *prometheus.HistogramVec + *HistogramOpts + lazyMetric + originalLabels []string +} + +// 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. +func NewHistogramVec(opts *HistogramOpts, labels []string) *HistogramVec { + // todo: handle defaulting better + klog.Errorf("---%v---\n", opts) + if opts.StabilityLevel == "" { + opts.StabilityLevel = ALPHA + } + klog.Errorf("---%v---\n", opts) + v := &HistogramVec{ + HistogramVec: noopHistogramVec, + HistogramOpts: opts, + originalLabels: labels, + lazyMetric: lazyMetric{}, + } + v.lazyInit(v) + return v +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (v *HistogramVec) DeprecatedVersion() *semver.Version { + return v.HistogramOpts.DeprecatedVersion +} + +func (v *HistogramVec) initializeMetric() { + v.HistogramOpts.annotateStabilityLevel() + v.HistogramVec = prometheus.NewHistogramVec(v.HistogramOpts.toPromHistogramOpts(), v.originalLabels) +} + +func (v *HistogramVec) initializeDeprecatedMetric() { + v.HistogramOpts.markDeprecated() + 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. +// 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 + +// WithLabelValues returns the ObserverMetric 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 ObserverMetric is created IFF the HistogramVec +// has been registered to a metrics registry. +func (v *HistogramVec) WithLabelValues(lvs ...string) ObserverMetric { + if !v.IsCreated() { + return noop + } + return v.HistogramVec.WithLabelValues(lvs...) +} + +// With returns the ObserverMetric for the given Labels map (the label names +// must match those of the VariableLabels in Desc). If that label map is +// accessed for the first time, a new ObserverMetric is created IFF the HistogramVec has +// been registered to a metrics registry. +func (v *HistogramVec) With(labels prometheus.Labels) ObserverMetric { + if !v.IsCreated() { + return noop + } + return v.HistogramVec.With(labels) +} diff --git a/staging/src/k8s.io/component-base/metrics/histogram_test.go b/staging/src/k8s.io/component-base/metrics/histogram_test.go new file mode 100644 index 00000000000..ffa9c281606 --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/histogram_test.go @@ -0,0 +1,225 @@ +/* +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 ( + "github.com/blang/semver" + "github.com/prometheus/client_golang/prometheus" + apimachineryversion "k8s.io/apimachinery/pkg/version" + "testing" +) + +func TestHistogram(t *testing.T) { + v115 := semver.MustParse("1.15.0") + v114 := semver.MustParse("1.14.0") + var tests = []struct { + desc string + HistogramOpts + registryVersion *semver.Version + expectedMetricCount int + expectedHelp string + }{ + { + desc: "Test non deprecated", + HistogramOpts: HistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + Buckets: prometheus.DefBuckets, + }, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] histogram help message", + }, + { + desc: "Test deprecated", + HistogramOpts: HistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + DeprecatedVersion: &v115, + Buckets: prometheus.DefBuckets, + }, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] (Deprecated since 1.15.0) histogram help message", + }, + { + desc: "Test hidden", + HistogramOpts: HistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + DeprecatedVersion: &v114, + Buckets: prometheus.DefBuckets, + }, + registryVersion: &v115, + expectedMetricCount: 0, + expectedHelp: "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", + }) + c := NewHistogram(&test.HistogramOpts) + registry.MustRegister(c) + + ms, err := registry.Gather() + if len(ms) != test.expectedMetricCount { + t.Errorf("Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) + } + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, metric := range ms { + if metric.GetHelp() != test.expectedHelp { + t.Errorf("Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) + } + } + + // let's increment the counter and verify that the metric still works + c.Observe(1) + c.Observe(2) + c.Observe(3) + c.Observe(1.5) + expected := 4 + ms, err = registry.Gather() + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, mf := range ms { + for _, m := range mf.GetMetric() { + if int(m.GetHistogram().GetSampleCount()) != expected { + t.Errorf("Got %v, want %v as the sample count", m.GetHistogram().GetSampleCount(), expected) + } + } + } + }) + } +} + +func TestHistogramVec(t *testing.T) { + v115 := semver.MustParse("1.15.0") + v114 := semver.MustParse("1.14.0") + var tests = []struct { + desc string + HistogramOpts + labels []string + registryVersion *semver.Version + expectedMetricCount int + expectedHelp string + }{ + { + desc: "Test non deprecated", + HistogramOpts: HistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + Buckets: prometheus.DefBuckets, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] histogram help message", + }, + { + desc: "Test deprecated", + HistogramOpts: HistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + DeprecatedVersion: &v115, + Buckets: prometheus.DefBuckets, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] (Deprecated since 1.15.0) histogram help message", + }, + { + desc: "Test hidden", + HistogramOpts: HistogramOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "histogram help message", + DeprecatedVersion: &v114, + Buckets: prometheus.DefBuckets, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 0, + expectedHelp: "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", + }) + c := NewHistogramVec(&test.HistogramOpts, test.labels) + registry.MustRegister(c) + c.WithLabelValues("1", "2").Observe(1.0) + ms, err := registry.Gather() + + if len(ms) != test.expectedMetricCount { + t.Errorf("Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) + } + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, metric := range ms { + if metric.GetHelp() != test.expectedHelp { + t.Errorf("Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) + } + } + + // let's increment the counter and verify that the metric still works + c.WithLabelValues("1", "3").Observe(1.0) + c.WithLabelValues("2", "3").Observe(1.0) + ms, err = registry.Gather() + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, mf := range ms { + if len(mf.GetMetric()) != 3 { + t.Errorf("Got %v metrics, wanted 2 as the count", len(mf.GetMetric())) + } + for _, m := range mf.GetMetric() { + if m.GetHistogram().GetSampleCount() != 1 { + t.Errorf( + "Got %v metrics, expected histogram sample count to equal 1", + m.GetHistogram().GetSampleCount()) + } + } + } + }) + } +} diff --git a/staging/src/k8s.io/component-base/metrics/opts.go b/staging/src/k8s.io/component-base/metrics/opts.go index 1409f241020..7e4c491706c 100644 --- a/staging/src/k8s.io/component-base/metrics/opts.go +++ b/staging/src/k8s.io/component-base/metrics/opts.go @@ -21,6 +21,7 @@ import ( "github.com/blang/semver" "github.com/prometheus/client_golang/prometheus" "sync" + "time" ) // KubeOpts is superset struct for prometheus.Opts. The prometheus Opts structure @@ -82,3 +83,130 @@ func (o *CounterOpts) toPromCounterOpts() prometheus.CounterOpts { ConstLabels: o.ConstLabels, } } + +// GaugeOpts is an alias for Opts. See there for doc comments. +type GaugeOpts KubeOpts + +// Modify help description on the metric description. +func (o *GaugeOpts) 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 *GaugeOpts) 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 GaugeOpts) toPromGaugeOpts() prometheus.GaugeOpts { + return prometheus.GaugeOpts{ + Namespace: o.Namespace, + Subsystem: o.Subsystem, + Name: o.Name, + Help: o.Help, + ConstLabels: o.ConstLabels, + } +} + +// HistogramOpts bundles the options for creating a Histogram 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 HistogramOpts struct { + Namespace string + Subsystem string + Name string + Help string + ConstLabels prometheus.Labels + Buckets []float64 + DeprecatedVersion *semver.Version + deprecateOnce sync.Once + annotateOnce sync.Once + StabilityLevel StabilityLevel +} + +// Modify help description on the metric description. +func (o *HistogramOpts) 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 *HistogramOpts) 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 HistogramOpts) toPromHistogramOpts() prometheus.HistogramOpts { + return prometheus.HistogramOpts{ + Namespace: o.Namespace, + Subsystem: o.Subsystem, + Name: o.Name, + Help: o.Help, + ConstLabels: o.ConstLabels, + Buckets: o.Buckets, + } +} + +// 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 +// a help string and to explicitly set the Objectives field to the desired value +// as the default value will change in the upcoming v0.10 of the library. +type SummaryOpts struct { + Namespace string + Subsystem string + Name string + Help string + ConstLabels prometheus.Labels + Objectives map[float64]float64 + MaxAge time.Duration + AgeBuckets uint32 + BufCap uint32 + DeprecatedVersion *semver.Version + deprecateOnce sync.Once + annotateOnce sync.Once + StabilityLevel StabilityLevel +} + +// Modify help description on the metric description. +func (o *SummaryOpts) 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 *SummaryOpts) 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 SummaryOpts) toPromSummaryOpts() prometheus.SummaryOpts { + return prometheus.SummaryOpts{ + Namespace: o.Namespace, + Subsystem: o.Subsystem, + Name: o.Name, + Help: o.Help, + ConstLabels: o.ConstLabels, + Objectives: o.Objectives, + MaxAge: o.MaxAge, + AgeBuckets: o.AgeBuckets, + BufCap: o.BufCap, + } +} diff --git a/staging/src/k8s.io/component-base/metrics/summary.go b/staging/src/k8s.io/component-base/metrics/summary.go new file mode 100644 index 00000000000..366021e3865 --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/summary.go @@ -0,0 +1,152 @@ +/* +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 ( + "github.com/blang/semver" + "github.com/prometheus/client_golang/prometheus" +) + +// Summary is our internal representation for our wrapping struct around prometheus +// summaries. Summary implements both KubeCollector and ObserverMetric +// +// DEPRECATED: as per the metrics overhaul KEP +type Summary struct { + ObserverMetric + *SummaryOpts + lazyMetric + selfCollector +} + +// NewSummary returns an object which is Summary-like. However, nothing +// will be measured until the summary is registered somewhere. +// +// DEPRECATED: as per the metrics overhaul KEP +func NewSummary(opts *SummaryOpts) *Summary { + // todo: handle defaulting better + if opts.StabilityLevel == "" { + opts.StabilityLevel = ALPHA + } + s := &Summary{ + SummaryOpts: opts, + lazyMetric: lazyMetric{}, + } + s.setPrometheusSummary(noopMetric{}) + s.lazyInit(s) + return s +} + +// setPrometheusSummary sets the underlying KubeGauge object, i.e. the thing that does the measurement. +func (s *Summary) setPrometheusSummary(summary prometheus.Summary) { + s.ObserverMetric = summary + s.initSelfCollection(summary) +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (s *Summary) DeprecatedVersion() *semver.Version { + return s.SummaryOpts.DeprecatedVersion +} + +// initializeMetric invokes the actual prometheus.Summary object instantiation +// and stores a reference to it +func (s *Summary) initializeMetric() { + s.SummaryOpts.annotateStabilityLevel() + // this actually creates the underlying prometheus gauge. + s.setPrometheusSummary(prometheus.NewSummary(s.SummaryOpts.toPromSummaryOpts())) +} + +// initializeDeprecatedMetric invokes the actual prometheus.Summary object instantiation +// but modifies the Help description prior to object instantiation. +func (s *Summary) initializeDeprecatedMetric() { + s.SummaryOpts.markDeprecated() + s.initializeMetric() +} + +// SummaryVec is the internal representation of our wrapping struct around prometheus +// summaryVecs. +// +// DEPRECATED: as per the metrics overhaul KEP +type SummaryVec struct { + *prometheus.SummaryVec + *SummaryOpts + lazyMetric + originalLabels []string +} + +// 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. +// +// DEPRECATED: as per the metrics overhaul KEP +func NewSummaryVec(opts *SummaryOpts, labels []string) *SummaryVec { + // todo: handle defaulting better + if opts.StabilityLevel == "" { + opts.StabilityLevel = ALPHA + } + v := &SummaryVec{ + SummaryOpts: opts, + originalLabels: labels, + lazyMetric: lazyMetric{}, + } + v.lazyInit(v) + return v +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (v *SummaryVec) DeprecatedVersion() *semver.Version { + return v.SummaryOpts.DeprecatedVersion +} + +func (v *SummaryVec) initializeMetric() { + v.SummaryOpts.annotateStabilityLevel() + v.SummaryVec = prometheus.NewSummaryVec(v.SummaryOpts.toPromSummaryOpts(), v.originalLabels) +} + +func (v *SummaryVec) initializeDeprecatedMetric() { + v.SummaryOpts.markDeprecated() + 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. +// 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 + +// WithLabelValues returns the ObserverMetric 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 ObserverMetric is created IFF the summaryVec +// has been registered to a metrics registry. +func (v *SummaryVec) WithLabelValues(lvs ...string) ObserverMetric { + if !v.IsCreated() { + return noop + } + return v.SummaryVec.WithLabelValues(lvs...) +} + +// With returns the ObserverMetric for the given Labels map (the label names +// must match those of the VariableLabels in Desc). If that label map is +// accessed for the first time, a new ObserverMetric is created IFF the summaryVec has +// been registered to a metrics registry. +func (v *SummaryVec) With(labels prometheus.Labels) ObserverMetric { + if !v.IsCreated() { + return noop + } + return v.SummaryVec.With(labels) +} diff --git a/staging/src/k8s.io/component-base/metrics/summary_test.go b/staging/src/k8s.io/component-base/metrics/summary_test.go new file mode 100644 index 00000000000..c7113b1e5f2 --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/summary_test.go @@ -0,0 +1,220 @@ +/* +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 ( + "github.com/blang/semver" + apimachineryversion "k8s.io/apimachinery/pkg/version" + "testing" +) + +func TestSummary(t *testing.T) { + v115 := semver.MustParse("1.15.0") + v114 := semver.MustParse("1.14.0") + var tests = []struct { + desc string + SummaryOpts + registryVersion *semver.Version + expectedMetricCount int + expectedHelp string + }{ + { + desc: "Test non deprecated", + SummaryOpts: SummaryOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "summary help message", + StabilityLevel: ALPHA, + }, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] summary help message", + }, + { + desc: "Test deprecated", + SummaryOpts: SummaryOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "summary help message", + DeprecatedVersion: &v115, + StabilityLevel: ALPHA, + }, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] (Deprecated since 1.15.0) summary help message", + }, + { + desc: "Test hidden", + SummaryOpts: SummaryOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "summary help message", + DeprecatedVersion: &v114, + }, + registryVersion: &v115, + expectedMetricCount: 0, + expectedHelp: "summary 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", + }) + c := NewSummary(&test.SummaryOpts) + registry.MustRegister(c) + + ms, err := registry.Gather() + if len(ms) != test.expectedMetricCount { + t.Errorf("Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) + } + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, metric := range ms { + if metric.GetHelp() != test.expectedHelp { + t.Errorf("Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) + } + } + + // let's increment the counter and verify that the metric still works + c.Observe(1) + c.Observe(2) + c.Observe(3) + c.Observe(1.5) + expected := 4 + ms, err = registry.Gather() + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, mf := range ms { + for _, m := range mf.GetMetric() { + if int(m.GetSummary().GetSampleCount()) != expected { + t.Errorf("Got %v, want %v as the sample count", m.GetHistogram().GetSampleCount(), expected) + } + } + } + }) + } +} + +func TestSummaryVec(t *testing.T) { + v115 := semver.MustParse("1.15.0") + v114 := semver.MustParse("1.14.0") + var tests = []struct { + desc string + SummaryOpts + labels []string + registryVersion *semver.Version + expectedMetricCount int + expectedHelp string + }{ + { + desc: "Test non deprecated", + SummaryOpts: SummaryOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "summary help message", + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] summary help message", + }, + { + desc: "Test deprecated", + SummaryOpts: SummaryOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "summary help message", + DeprecatedVersion: &v115, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 1, + expectedHelp: "[ALPHA] (Deprecated since 1.15.0) summary help message", + }, + { + desc: "Test hidden", + SummaryOpts: SummaryOpts{ + Namespace: "namespace", + Name: "metric_test_name", + Subsystem: "subsystem", + Help: "summary help message", + DeprecatedVersion: &v114, + }, + labels: []string{"label_a", "label_b"}, + registryVersion: &v115, + expectedMetricCount: 0, + expectedHelp: "summary 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", + }) + c := NewSummaryVec(&test.SummaryOpts, test.labels) + registry.MustRegister(c) + c.WithLabelValues("1", "2").Observe(1.0) + ms, err := registry.Gather() + + if len(ms) != test.expectedMetricCount { + t.Errorf("Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) + } + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, metric := range ms { + if metric.GetHelp() != test.expectedHelp { + t.Errorf("Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) + } + } + + // let's increment the counter and verify that the metric still works + c.WithLabelValues("1", "3").Observe(1.0) + c.WithLabelValues("2", "3").Observe(1.0) + ms, err = registry.Gather() + if err != nil { + t.Fatalf("Gather failed %v", err) + } + for _, mf := range ms { + if len(mf.GetMetric()) != 3 { + t.Errorf("Got %v metrics, wanted 2 as the count", len(mf.GetMetric())) + } + for _, m := range mf.GetMetric() { + if m.GetSummary().GetSampleCount() != 1 { + t.Errorf( + "Got %v metrics, wanted 2 as the summary sample count", + m.GetSummary().GetSampleCount()) + } + } + } + }) + } +} diff --git a/staging/src/k8s.io/component-base/metrics/wrappers.go b/staging/src/k8s.io/component-base/metrics/wrappers.go index 5bb72cacfbc..f2d4c9dedd9 100644 --- a/staging/src/k8s.io/component-base/metrics/wrappers.go +++ b/staging/src/k8s.io/component-base/metrics/wrappers.go @@ -56,6 +56,16 @@ type CounterVecMetric interface { With(prometheus.Labels) CounterMetric } +// GaugeMetric is an interface which defines a subset of the interface provided by prometheus.Gauge +type GaugeMetric interface { + Set(float64) +} + +// ObserverMetric captures individual observations. +type ObserverMetric interface { + Observe(float64) +} + // PromRegistry is an interface which implements a subset of prometheus.Registerer and // prometheus.Gatherer interfaces type PromRegistry interface {