diff --git a/staging/src/k8s.io/component-base/metrics/BUILD b/staging/src/k8s.io/component-base/metrics/BUILD index 1602d395668..03c7470f2eb 100644 --- a/staging/src/k8s.io/component-base/metrics/BUILD +++ b/staging/src/k8s.io/component-base/metrics/BUILD @@ -3,7 +3,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "collector.go", "counter.go", + "desc.go", "gauge.go", "histogram.go", "http.go", @@ -13,6 +15,7 @@ go_library( "processstarttime.go", "registry.go", "summary.go", + "value.go", "version.go", "version_parser.go", "wrappers.go", @@ -35,6 +38,7 @@ go_library( go_test( name = "go_default_test", srcs = [ + "collector_test.go", "counter_test.go", "gauge_test.go", "histogram_test.go", @@ -49,6 +53,7 @@ go_test( "//vendor/github.com/blang/semver:go_default_library", "//vendor/github.com/prometheus/client_golang/prometheus:go_default_library", "//vendor/github.com/prometheus/client_golang/prometheus/testutil:go_default_library", + "//vendor/github.com/prometheus/client_model/go:go_default_library", "//vendor/github.com/prometheus/common/expfmt:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", ], diff --git a/staging/src/k8s.io/component-base/metrics/collector.go b/staging/src/k8s.io/component-base/metrics/collector.go new file mode 100644 index 00000000000..f720249068c --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/collector.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 ( + "fmt" + + "github.com/blang/semver" + "github.com/prometheus/client_golang/prometheus" +) + +// StableCollector extends the prometheus.Collector interface to allow customization of the +// metric registration process, it's especially intend to be used in scenario of custom collector. +type StableCollector interface { + prometheus.Collector + + // DescribeWithStability sends the super-set of all possible metrics.Desc collected + // by this StableCollector to the provided channel. + DescribeWithStability(chan<- *Desc) + + // CollectWithStability sends each collected metrics.Metric via the provide channel. + CollectWithStability(chan<- Metric) + + // Create will initialize all Desc and it intends to be called by registry. + Create(version *semver.Version, self StableCollector) bool +} + +// BaseStableCollector which implements almost all of the methods defined by StableCollector +// is a convenient assistant for custom collectors. +// It is recommend that inherit BaseStableCollector when implementing custom collectors. +type BaseStableCollector struct { + descriptors []*Desc // stores all Desc collected from DescribeWithStability(). + registrable []*Desc // stores registrable Desc(not be hidden), is a subset of descriptors. + self StableCollector +} + +// DescribeWithStability sends all descriptors to the provided channel. +// Every custom collector should over-write this method. +func (bsc *BaseStableCollector) DescribeWithStability(ch chan<- *Desc) { + panic(fmt.Errorf("custom collector should over-write DescribeWithStability method")) +} + +// Describe sends all descriptors to the provided channel. +// It intend to be called by prometheus registry. +func (bsc *BaseStableCollector) Describe(ch chan<- *prometheus.Desc) { + for _, d := range bsc.registrable { + ch <- d.toPrometheusDesc() + } +} + +// CollectWithStability sends all metrics to the provided channel. +// Every custom collector should over-write this method. +func (bsc *BaseStableCollector) CollectWithStability(ch chan<- Metric) { + panic(fmt.Errorf("custom collector should over-write CollectWithStability method")) +} + +// Collect is called by the Prometheus registry when collecting metrics. +func (bsc *BaseStableCollector) Collect(ch chan<- prometheus.Metric) { + mch := make(chan Metric) + + go func() { + bsc.self.CollectWithStability(mch) + close(mch) + }() + + for m := range mch { + // nil Metric usually means hidden metrics + if m == nil { + continue + } + + ch <- prometheus.Metric(m) + } +} + +func (bsc *BaseStableCollector) add(d *Desc) { + bsc.descriptors = append(bsc.descriptors, d) +} + +// Init intends to be called by registry. +func (bsc *BaseStableCollector) init(self StableCollector) { + bsc.self = self + + dch := make(chan *Desc) + + // collect all possible descriptions from custom side + go func() { + bsc.self.DescribeWithStability(dch) + close(dch) + }() + + for d := range dch { + bsc.add(d) + } +} + +// Create intends to be called by registry. +// Create will return true as long as there is one or more metrics not be hidden. +// Otherwise return false, that means the whole collector will be ignored by registry. +func (bsc *BaseStableCollector) Create(version *semver.Version, self StableCollector) bool { + bsc.init(self) + + for _, d := range bsc.descriptors { + if version != nil { + d.determineDeprecationStatus(*version) + } + + d.createOnce.Do(func() { + d.createLock.Lock() + defer d.createLock.Unlock() + + if d.IsHidden() { + // do nothing for hidden metrics + } else if d.IsDeprecated() { + d.initializeDeprecatedDesc() + bsc.registrable = append(bsc.registrable, d) + d.isCreated = true + } else { + d.initialize() + bsc.registrable = append(bsc.registrable, d) + d.isCreated = true + } + }) + } + + if len(bsc.registrable) > 0 { + return true + } + + return false +} + +// Check if our BaseStableCollector implements necessary interface +var _ StableCollector = &BaseStableCollector{} diff --git a/staging/src/k8s.io/component-base/metrics/collector_test.go b/staging/src/k8s.io/component-base/metrics/collector_test.go new file mode 100644 index 00000000000..08a86980169 --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/collector_test.go @@ -0,0 +1,153 @@ +/* +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" + + dto "github.com/prometheus/client_model/go" + + apimachineryversion "k8s.io/apimachinery/pkg/version" +) + +type testCustomCollector struct { + BaseStableCollector +} + +var ( + currentVersion = apimachineryversion.Info{ + Major: "1", + Minor: "17", + GitVersion: "v1.17.0-alpha-1.12345", + } + alphaDesc = NewDesc("metric_alpha", "alpha metric", []string{"name"}, nil, + ALPHA, "") + stableDesc = NewDesc("metric_stable", "stable metrics", []string{"name"}, nil, + STABLE, "") + deprecatedDesc = NewDesc("metric_deprecated", "stable deprecated metrics", []string{"name"}, nil, + STABLE, "1.17.0") + hiddenDesc = NewDesc("metric_hidden", "stable hidden metrics", []string{"name"}, nil, + STABLE, "1.16.0") +) + +func (tc *testCustomCollector) DescribeWithStability(ch chan<- *Desc) { + ch <- alphaDesc + ch <- stableDesc + ch <- deprecatedDesc + ch <- hiddenDesc +} + +func (tc *testCustomCollector) CollectWithStability(ch chan<- Metric) { + ch <- NewLazyConstMetric( + alphaDesc, + GaugeValue, + 1, + "value", + ) + ch <- NewLazyConstMetric( + stableDesc, + GaugeValue, + 1, + "value", + ) + ch <- NewLazyConstMetric( + deprecatedDesc, + GaugeValue, + 1, + "value", + ) + ch <- NewLazyConstMetric( + hiddenDesc, + GaugeValue, + 1, + "value", + ) + +} + +func getMetric(metrics []*dto.MetricFamily, fqName string) *dto.MetricFamily { + for _, m := range metrics { + if *m.Name == fqName { + return m + } + } + + return nil +} + +func TestBaseCustomCollector(t *testing.T) { + var tests = []struct { + name string + d *Desc + shouldHidden bool + expectedHelp string + }{ + { + name: "alpha metric should contains stability metadata", + d: alphaDesc, + shouldHidden: false, + expectedHelp: "[ALPHA] alpha metric", + }, + { + name: "stable metric should contains stability metadata", + d: stableDesc, + shouldHidden: false, + expectedHelp: "[STABLE] stable metrics", + }, + { + name: "deprecated metric should contains stability metadata", + d: deprecatedDesc, + shouldHidden: false, + expectedHelp: "[STABLE] (Deprecated since 1.17.0) stable deprecated metrics", + }, + { + name: "hidden metric should be ignored", + d: hiddenDesc, + shouldHidden: true, + expectedHelp: "[STABLE] stable hidden metrics", + }, + } + + registry := newKubeRegistry(currentVersion) + customCollector := &testCustomCollector{} + + if err := registry.CustomRegister(customCollector); err != nil { + t.Fatalf("register collector failed with err: %v", err) + } + + metrics, err := registry.Gather() + if err != nil { + t.Fatalf("failed to get metrics from collector, %v", err) + } + + for _, test := range tests { + tc := test + t.Run(tc.name, func(t *testing.T) { + m := getMetric(metrics, tc.d.fqName) + if m == nil { + if !tc.shouldHidden { + t.Fatalf("Want metric: %s", tc.d.fqName) + } + } else { + if m.GetHelp() != tc.expectedHelp { + t.Fatalf("Metric(%s) HELP(%s) not contains: %s", tc.d.fqName, *m.Help, tc.expectedHelp) + } + } + + }) + } +} diff --git a/staging/src/k8s.io/component-base/metrics/desc.go b/staging/src/k8s.io/component-base/metrics/desc.go new file mode 100644 index 00000000000..03d7a20e661 --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/desc.go @@ -0,0 +1,158 @@ +/* +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 ( + "fmt" + "sync" + + "github.com/blang/semver" + "github.com/prometheus/client_golang/prometheus" + + "k8s.io/klog" +) + +// Desc is a prometheus.Desc extension. +// +// Use NewDesc to create new Desc instances. +type Desc struct { + // fqName has been built from Namespace, Subsystem, and Name. + fqName string + // help provides some helpful information about this metric. + help string + // constLabels is the label names. Their label values are variable. + constLabels Labels + // variableLabels contains names of labels for which the metric + // maintains variable values. + variableLabels []string + + // promDesc is the descriptor used by every Prometheus Metric. + promDesc *prometheus.Desc + + // stabilityLevel represents the API guarantees for a given defined metric. + stabilityLevel StabilityLevel + // deprecatedVersion represents in which version this metric be deprecated. + deprecatedVersion string + + isDeprecated bool + isHidden bool + isCreated bool + createLock sync.RWMutex + markDeprecationOnce sync.Once + createOnce sync.Once + deprecateOnce sync.Once + hideOnce sync.Once + annotateOnce sync.Once +} + +// NewDesc extends prometheus.NewDesc with stability support. +// +// The stabilityLevel should be valid stability label, such as "metrics.ALPHA" +// and "metrics.STABLE"(Maybe "metrics.BETA" in future). Default value "metrics.ALPHA" +// will be used in case of empty or invalid stability label. +// +// The deprecatedVersion represents in which version this Metric be deprecated. +// The deprecation policy outlined by the control plane metrics stability KEP. +func NewDesc(fqName string, help string, variableLabels []string, constLabels Labels, + stabilityLevel StabilityLevel, deprecatedVersion string) *Desc { + d := &Desc{ + fqName: fqName, + help: help, + variableLabels: variableLabels, + constLabels: constLabels, + stabilityLevel: stabilityLevel, + deprecatedVersion: deprecatedVersion, + } + d.stabilityLevel.setDefaults() + + return d +} + +// String formats the Desc as a string. +// The stability metadata maybe annotated in 'HELP' section if called after registry, +// otherwise not. +func (d *Desc) String() string { + if d.isCreated { + return d.promDesc.String() + } + + return prometheus.NewDesc(d.fqName, d.help, d.variableLabels, prometheus.Labels(d.constLabels)).String() +} + +// toPrometheusDesc transform self to prometheus.Desc +func (d *Desc) toPrometheusDesc() *prometheus.Desc { + return d.promDesc +} + +// DeprecatedVersion returns a pointer to the Version or nil +func (d *Desc) DeprecatedVersion() *semver.Version { + return parseSemver(d.deprecatedVersion) + +} + +func (d *Desc) determineDeprecationStatus(version semver.Version) { + selfVersion := d.DeprecatedVersion() + if selfVersion == nil { + return + } + d.markDeprecationOnce.Do(func() { + if selfVersion.LTE(version) { + d.isDeprecated = true + } + if ShouldShowHidden() { + klog.Warningf("Hidden metrics(%s) have been manually overridden, showing this very deprecated metric.", d.fqName) + return + } + if shouldHide(&version, selfVersion) { + klog.Warningf("This metric(%s) has been deprecated for more than one release, hiding.", d.fqName) + d.isHidden = true + } + }) +} + +// IsHidden returns if metric will be hidden +func (d *Desc) IsHidden() bool { + return d.isHidden +} + +// IsDeprecated returns if metric has been deprecated +func (d *Desc) IsDeprecated() bool { + return d.isDeprecated +} + +func (d *Desc) markDeprecated() { + d.deprecateOnce.Do(func() { + d.help = fmt.Sprintf("(Deprecated since %s) %s", d.deprecatedVersion, d.help) + }) +} + +func (d *Desc) annotateStabilityLevel() { + d.annotateOnce.Do(func() { + d.help = fmt.Sprintf("[%v] %v", d.stabilityLevel, d.help) + }) +} + +func (d *Desc) initialize() { + d.annotateStabilityLevel() + // this actually creates the underlying prometheus desc. + d.promDesc = prometheus.NewDesc(d.fqName, d.help, d.variableLabels, prometheus.Labels(d.constLabels)) +} + +func (d *Desc) initializeDeprecatedDesc() { + d.markDeprecated() + d.initialize() +} diff --git a/staging/src/k8s.io/component-base/metrics/legacyregistry/registry.go b/staging/src/k8s.io/component-base/metrics/legacyregistry/registry.go index 267706c4b25..7f9609f71bd 100644 --- a/staging/src/k8s.io/component-base/metrics/legacyregistry/registry.go +++ b/staging/src/k8s.io/component-base/metrics/legacyregistry/registry.go @@ -85,3 +85,22 @@ func RawRegister(c prometheus.Collector) error { prometheus.Register(c) return err } + +// CustomRegister registers a custom collector but uses the global registry. +func CustomRegister(c metrics.StableCollector) error { + err := defaultRegistry.CustomRegister(c) + + //TODO(RainbowMango): Maybe we can wrap this error by error wrapping.(Golang 1.13) + _ = prometheus.Register(c) + + return err +} + +// CustomMustRegister registers custom collectors but uses the global registry. +func CustomMustRegister(cs ...metrics.StableCollector) { + defaultRegistry.CustomMustRegister(cs...) + + for _, c := range cs { + prometheus.MustRegister(c) + } +} diff --git a/staging/src/k8s.io/component-base/metrics/registry.go b/staging/src/k8s.io/component-base/metrics/registry.go index ff8d5ac02ba..35e878997db 100644 --- a/staging/src/k8s.io/component-base/metrics/registry.go +++ b/staging/src/k8s.io/component-base/metrics/registry.go @@ -78,6 +78,8 @@ type KubeRegistry interface { RawRegister(prometheus.Collector) error // Deprecated RawMustRegister(...prometheus.Collector) + CustomRegister(c StableCollector) error + CustomMustRegister(cs ...StableCollector) Register(Registerable) error MustRegister(...Registerable) Unregister(Registerable) bool @@ -117,6 +119,29 @@ func (kr *kubeRegistry) MustRegister(cs ...Registerable) { kr.PromRegistry.MustRegister(metrics...) } +// CustomRegister registers a new custom collector. +func (kr *kubeRegistry) CustomRegister(c StableCollector) error { + if c.Create(&kr.version, c) { + return kr.PromRegistry.Register(c) + } + + return nil +} + +// CustomMustRegister works like CustomRegister but registers any number of +// StableCollectors and panics upon the first registration that causes an +// error. +func (kr *kubeRegistry) CustomMustRegister(cs ...StableCollector) { + collectors := make([]prometheus.Collector, 0, len(cs)) + for _, c := range cs { + if c.Create(&kr.version, c) { + collectors = append(collectors, c) + } + } + + kr.PromRegistry.MustRegister(collectors...) +} + // RawRegister takes a native prometheus.Collector and registers the collector // to the registry. This bypasses metrics safety checks, so should only be used // to register custom prometheus collectors. diff --git a/staging/src/k8s.io/component-base/metrics/value.go b/staging/src/k8s.io/component-base/metrics/value.go new file mode 100644 index 00000000000..5717338510e --- /dev/null +++ b/staging/src/k8s.io/component-base/metrics/value.go @@ -0,0 +1,46 @@ +/* +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/prometheus/client_golang/prometheus" +) + +// ValueType is an enumeration of metric types that represent a simple value. +type ValueType int + +// Possible values for the ValueType enum. +const ( + _ ValueType = iota + CounterValue + GaugeValue + UntypedValue +) + +func (vt *ValueType) toPromValueType() prometheus.ValueType { + return prometheus.ValueType(*vt) +} + +// NewLazyConstMetric is a helper of MustNewConstMetric. +// +// Note: If the metrics described by the desc is hidden, the metrics will not be created. +func NewLazyConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) Metric { + if desc.IsHidden() { + return nil + } + return prometheus.MustNewConstMetric(desc.toPrometheusDesc(), valueType.toPromValueType(), value, labelValues...) +}