Merge pull request #77618 from logicalhan/other-metric-wrappers

add wrappers around gauge, histogram & summary
This commit is contained in:
Kubernetes Prow Robot 2019-05-10 16:32:11 -07:00 committed by GitHub
commit 33d763ff3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1249 additions and 0 deletions

View File

@ -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"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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