Merge pull request #83062 from RainbowMango/pr_provide_custom_collector_support

Provide a mechanism for custom collectors to use the metrics stability framework
This commit is contained in:
Kubernetes Prow Robot 2019-11-06 02:23:51 -08:00 committed by GitHub
commit 76d04cf602
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 554 additions and 0 deletions

View File

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

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 (
"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{}

View File

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

View File

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

View File

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

View File

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

View File

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