Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add sampling histogram #94672

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions build/tools.go
Expand Up @@ -29,4 +29,5 @@ import (
_ "k8s.io/gengo/examples/import-boss/generators"
_ "k8s.io/gengo/examples/set-gen/generators"
_ "k8s.io/kube-openapi/cmd/openapi-gen"
_ "k8s.io/utils/clock/testing"
)
3 changes: 3 additions & 0 deletions staging/src/k8s.io/component-base/metrics/BUILD
Expand Up @@ -15,6 +15,7 @@ go_library(
"opts.go",
"processstarttime.go",
"registry.go",
"sampling-histogram.go",
"summary.go",
"value.go",
"version.go",
Expand All @@ -26,6 +27,7 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/version:go_default_library",
"//staging/src/k8s.io/component-base/metrics/prometheusextension:go_default_library",
"//staging/src/k8s.io/component-base/version:go_default_library",
"//vendor/github.com/blang/semver:go_default_library",
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
Expand Down Expand Up @@ -79,6 +81,7 @@ filegroup(
"//staging/src/k8s.io/component-base/metrics/prometheus/restclient:all-srcs",
"//staging/src/k8s.io/component-base/metrics/prometheus/version:all-srcs",
"//staging/src/k8s.io/component-base/metrics/prometheus/workqueue:all-srcs",
"//staging/src/k8s.io/component-base/metrics/prometheusextension:all-srcs",
"//staging/src/k8s.io/component-base/metrics/testutil:all-srcs",
],
tags = ["automanaged"],
Expand Down
52 changes: 52 additions & 0 deletions staging/src/k8s.io/component-base/metrics/opts.go
Expand Up @@ -22,6 +22,7 @@ import (
"time"

"github.com/prometheus/client_golang/prometheus"
promext "k8s.io/component-base/metrics/prometheusextension"
)

// KubeOpts is superset struct for prometheus.Opts. The prometheus Opts structure
Expand Down Expand Up @@ -180,6 +181,57 @@ func (o *HistogramOpts) toPromHistogramOpts() prometheus.HistogramOpts {
}
}

// SamplingHistogramOpts bundles the options for creating a SamplingHistogram 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 SamplingHistogramOpts struct {
Namespace string
Subsystem string
Name string
Help string
ConstLabels map[string]string
Buckets []float64
InitialValue float64
SamplingPeriod time.Duration
DeprecatedVersion string
deprecateOnce sync.Once
annotateOnce sync.Once
StabilityLevel StabilityLevel
}

// Modify help description on the metric description.
func (o *SamplingHistogramOpts) 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 *SamplingHistogramOpts) 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 *SamplingHistogramOpts) toPromSamplingHistogramOpts() promext.SamplingHistogramOpts {
return promext.SamplingHistogramOpts{
HistogramOpts: prometheus.HistogramOpts{
Namespace: o.Namespace,
Subsystem: o.Subsystem,
Name: o.Name,
Help: o.Help,
ConstLabels: o.ConstLabels,
Buckets: o.Buckets,
},
InitialValue: o.InitialValue,
SamplingPeriod: o.SamplingPeriod,
}
}

// 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
Expand Down
@@ -0,0 +1,39 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["sampling-histogram.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/component-base/metrics/prometheusextension",
importpath = "k8s.io/component-base/metrics/prometheusextension",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
"//vendor/github.com/prometheus/client_model/go:go_default_library",
"//vendor/k8s.io/utils/clock:go_default_library",
],
)

go_test(
name = "go_default_test",
srcs = ["sampling-histogram_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
"//vendor/github.com/prometheus/client_model/go:go_default_library",
"//vendor/k8s.io/utils/clock/testing:go_default_library",
],
)

filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)

filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
@@ -0,0 +1,137 @@
/*
Copyright 2020 Mike Spreitzer.

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 prometheusextension

import (
"fmt"
"sync"
"time"

"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"k8s.io/utils/clock"
)

// A SamplingHistogram samples the values of a float64 variable at a
// configured rate. The samples contribute to a Histogram.
type SamplingHistogram interface {
prometheus.Metric
prometheus.Collector

// Set the variable to the given value.
Set(float64)

// Add the given change to the variable
Add(float64)
}

// SamplingHistogramOpts bundles the options for creating a
// SamplingHistogram metric. This builds on the options for creating
// a Histogram metric.
type SamplingHistogramOpts struct {
prometheus.HistogramOpts

// The initial value of the variable.
InitialValue float64

// The variable is sampled once every this often.
// Must be set to a positive value.
SamplingPeriod time.Duration
}

// NewSamplingHistogram creates a new SamplingHistogram
func NewSamplingHistogram(opts SamplingHistogramOpts) (SamplingHistogram, error) {
return NewTestableSamplingHistogram(clock.RealClock{}, opts)
}

// NewTestableSamplingHistogram creates a SamplingHistogram that uses a mockable clock
func NewTestableSamplingHistogram(clock clock.Clock, opts SamplingHistogramOpts) (SamplingHistogram, error) {
desc := prometheus.NewDesc(
prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
)
return newSamplingHistogram(clock, desc, opts)
}

func newSamplingHistogram(clock clock.Clock, desc *prometheus.Desc, opts SamplingHistogramOpts, labelValues ...string) (SamplingHistogram, error) {
if opts.SamplingPeriod <= 0 {
return nil, fmt.Errorf("the given sampling period was %v but must be positive", opts.SamplingPeriod)
}
return &samplingHistogram{
samplingPeriod: opts.SamplingPeriod,
histogram: prometheus.NewHistogram(opts.HistogramOpts),
clock: clock,
lastSampleIndex: clock.Now().UnixNano() / int64(opts.SamplingPeriod),
value: opts.InitialValue,
}, nil
}

type samplingHistogram struct {
samplingPeriod time.Duration
histogram prometheus.Histogram
clock clock.Clock
lock sync.Mutex

// identifies the last sampling period completed
lastSampleIndex int64
value float64
}

var _ SamplingHistogram = &samplingHistogram{}

func (sh *samplingHistogram) Set(newValue float64) {
sh.Update(func(float64) float64 { return newValue })
}

func (sh *samplingHistogram) Add(delta float64) {
sh.Update(func(oldValue float64) float64 { return oldValue + delta })
}

func (sh *samplingHistogram) Update(updateFn func(float64) float64) {
oldValue, numSamples := func() (float64, int64) {
sh.lock.Lock()
defer sh.lock.Unlock()
newSampleIndex := sh.clock.Now().UnixNano() / int64(sh.samplingPeriod)
deltaIndex := newSampleIndex - sh.lastSampleIndex
sh.lastSampleIndex = newSampleIndex
oldValue := sh.value
sh.value = updateFn(sh.value)
return oldValue, deltaIndex
}()
for i := int64(0); i < numSamples; i++ {
sh.histogram.Observe(oldValue)
}
}

func (sh *samplingHistogram) Desc() *prometheus.Desc {
return sh.histogram.Desc()
}

func (sh *samplingHistogram) Write(dest *dto.Metric) error {
return sh.histogram.Write(dest)
}

func (sh *samplingHistogram) Describe(ch chan<- *prometheus.Desc) {
sh.histogram.Describe(ch)
}

func (sh *samplingHistogram) Collect(ch chan<- prometheus.Metric) {
sh.Add(0)
sh.histogram.Collect(ch)
}
@@ -0,0 +1,125 @@
/*
Copyright 2020 Mike Spreitzer.

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 prometheusextension

import (
"testing"
"time"

"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
testclock "k8s.io/utils/clock/testing"
)

func TestSamplingHistogram(t *testing.T) {
clk := testclock.NewFakeClock(time.Unix(time.Now().Unix(), 999999990))
sh, err := NewTestableSamplingHistogram(clk, SamplingHistogramOpts{
HistogramOpts: prometheus.HistogramOpts{
Namespace: "test",
Subsystem: "func",
Name: "one",
Help: "a helpful string",
ConstLabels: map[string]string{"l1": "v1", "l2": "v2"},
Buckets: []float64{0, 1, 2},
},
InitialValue: 1,
SamplingPeriod: time.Second,
})
if sh == nil || err != nil {
t.Errorf("Creation failed; err=%s", err.Error())
}
expectHistogram(t, "After creation", sh, 0, 0, 0, 0)
sh.Set(2)
expectHistogram(t, "After initial Set", sh, 0, 0, 0, 0)
clk.Step(9 * time.Nanosecond)
expectHistogram(t, "Just before the end of the first sampling period", sh, 0, 0, 0, 0)
clk.Step(1 * time.Nanosecond)
expectHistogram(t, "At the end of the first sampling period", sh, 0, 0, 1, 1)
clk.Step(1 * time.Nanosecond)
expectHistogram(t, "Barely into second sampling period", sh, 0, 0, 1, 1)
sh.Set(-0.5)
sh.Add(1)
clk.Step(999999998 * time.Nanosecond)
expectHistogram(t, "Just before the end of second sampling period", sh, 0, 0, 1, 1)
clk.Step(1 * time.Nanosecond)
expectHistogram(t, "At the end of second sampling period", sh, 0, 1, 2, 2)
}

func expectHistogram(t *testing.T, when string, sh SamplingHistogram, buckets ...uint64) {
metrics := make(chan prometheus.Metric, 2)
sh.Collect(metrics)
var dtom dto.Metric
select {
case metric := <-metrics:
metric.Write(&dtom)
default:
t.Errorf("%s, zero Metrics collected", when)
}
select {
case metric := <-metrics:
t.Errorf("%s, collected more than one Metric; second Metric = %#+v", when, metric)
default:
}
missingLabels := map[string]string{"l1": "v1", "l2": "v2"}
for _, lp := range dtom.Label {
if lp == nil || lp.Name == nil || lp.Value == nil {
continue
}
if val, ok := missingLabels[*(lp.Name)]; ok {
if val != *(lp.Value) {
t.Errorf("%s, found label named %q with value %q instead of %q", when, *(lp.Name), *(lp.Value), val)
}
} else {
t.Errorf("%s, got unexpected label name %q", when, *(lp.Name))
}
delete(missingLabels, *(lp.Name))
}
if len(missingLabels) > 0 {
t.Errorf("%s, missed labels %#+v", when, missingLabels)
}
if dtom.Histogram == nil {
t.Errorf("%s, Collect returned a Metric without a Histogram: %#+v", when, dtom)
return
}
mh := dtom.Histogram
if len(mh.Bucket) != len(buckets)-1 {
t.Errorf("%s, expected %d buckets but got %d: %#+v", when, len(buckets)-1, len(mh.Bucket), mh.Bucket)
}
if mh.SampleCount == nil {
t.Errorf("%s, got Histogram with nil SampleCount", when)
} else if *(mh.SampleCount) != buckets[len(mh.Bucket)] {
t.Errorf("%s, SampleCount=%d but expected %d", when, *(mh.SampleCount), buckets[len(mh.Bucket)])
}
for i, mhBucket := range mh.Bucket {
if mhBucket == nil {
t.Errorf("%s, bucket %d was nil", when, i)
continue
}
if mhBucket.UpperBound == nil || mhBucket.CumulativeCount == nil {
t.Errorf("%s, bucket %d had nil bound or count", when, i)
continue
}
ub := float64(i)
if ub != *(mhBucket.UpperBound) {
t.Errorf("%s, bucket %d's upper bound was %v", when, i, *(mhBucket.UpperBound))
}
expectedCount := buckets[i]
if expectedCount != *(mhBucket.CumulativeCount) {
t.Errorf("%s, bucket %d's count was %d rather than %d", when, i, mhBucket.CumulativeCount, expectedCount)
}
}
}