Add support for histograms to the Go client library.
This commit is contained in:
parent
b37ca982a9
commit
6958242277
|
@ -129,3 +129,31 @@ func BenchmarkSummaryNoLabels(b *testing.B) {
|
||||||
m.Observe(3.1415)
|
m.Observe(3.1415)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramWithLabelValues(b *testing.B) {
|
||||||
|
m := NewHistogramVec(
|
||||||
|
HistogramOpts{
|
||||||
|
Name: "benchmark_histogram",
|
||||||
|
Help: "A histogram to benchmark it.",
|
||||||
|
},
|
||||||
|
[]string{"one", "two", "three"},
|
||||||
|
)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
m.WithLabelValues("eins", "zwei", "drei").Observe(3.1415)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramNoLabels(b *testing.B) {
|
||||||
|
m := NewHistogram(HistogramOpts{
|
||||||
|
Name: "benchmark_histogram",
|
||||||
|
Help: "A histogram to benchmark it.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
m.Observe(3.1415)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -452,3 +452,49 @@ func ExampleSummaryVec() {
|
||||||
// >
|
// >
|
||||||
// ]
|
// ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleHistogram() {
|
||||||
|
temps := prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Name: "pond_temperature_celsius",
|
||||||
|
Help: "The temperature of the frog pond.", // Sorry, we can't measure how badly it smells.
|
||||||
|
Buckets: prometheus.LinearBuckets(20, 5, 6), // 6 buckets, each 5 centigrade wide.
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate some observations.
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
temps.Observe(30 + math.Floor(120*math.Sin(float64(i)*0.1))/10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just for demonstration, let's check the state of the histogram by
|
||||||
|
// (ab)using its Write method (which is usually only used by Prometheus
|
||||||
|
// internally).
|
||||||
|
metric := &dto.Metric{}
|
||||||
|
temps.Write(metric)
|
||||||
|
fmt.Println(proto.MarshalTextString(metric))
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// histogram: <
|
||||||
|
// sample_count: 1000
|
||||||
|
// sample_sum: 29969.50000000001
|
||||||
|
// bucket: <
|
||||||
|
// cumulative_count: 192
|
||||||
|
// upper_bound: 20
|
||||||
|
// >
|
||||||
|
// bucket: <
|
||||||
|
// cumulative_count: 366
|
||||||
|
// upper_bound: 25
|
||||||
|
// >
|
||||||
|
// bucket: <
|
||||||
|
// cumulative_count: 501
|
||||||
|
// upper_bound: 30
|
||||||
|
// >
|
||||||
|
// bucket: <
|
||||||
|
// cumulative_count: 638
|
||||||
|
// upper_bound: 35
|
||||||
|
// >
|
||||||
|
// bucket: <
|
||||||
|
// cumulative_count: 816
|
||||||
|
// upper_bound: 40
|
||||||
|
// >
|
||||||
|
// >
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,329 @@
|
||||||
|
// Copyright 2015 The Prometheus 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 prometheus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"math"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Histogram counts individual observations from an event or sample stream in
|
||||||
|
// configurable buckets. Similar to a summary, it also provides a sum of
|
||||||
|
// observations and an observation count.
|
||||||
|
//
|
||||||
|
// On the Prometheus server, quantiles can be calculated from a Histogram using
|
||||||
|
// the histogram_quantile function in the query language.
|
||||||
|
//
|
||||||
|
// Note that Histograms, in contrast to Summaries, can be aggregated with the
|
||||||
|
// Prometheus query language (see the documentation for detailed
|
||||||
|
// procedures). However, Histograms requires the user to pre-define suitable
|
||||||
|
// buckets, and they are in general less accurate. The Observe method of a
|
||||||
|
// Histogram has a very low performance overhead in comparison with the Observe
|
||||||
|
// method of a Summary.
|
||||||
|
//
|
||||||
|
// To create Histogram instances, use NewHistogram.
|
||||||
|
type Histogram interface {
|
||||||
|
Metric
|
||||||
|
Collector
|
||||||
|
|
||||||
|
// Observe adds a single observation to the histogram.
|
||||||
|
Observe(float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefBuckets are the default Histogram buckets. Most likely, you want to define
|
||||||
|
// buckets customized to your use case.
|
||||||
|
var (
|
||||||
|
DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}
|
||||||
|
)
|
||||||
|
|
||||||
|
// LinearBuckets creates 'count' buckets, each 'width' wide, where the lowest
|
||||||
|
// bucket has an upper bound of 'start'. The final +Inf bucket is counted
|
||||||
|
// towards the total 'count', but not included in the returned slice. The
|
||||||
|
// returned slice is meant to be used for the Buckets field of HistogramOpts.
|
||||||
|
//
|
||||||
|
// The function panics if 'count' is less than 2.
|
||||||
|
func LinearBuckets(start, width float64, count int) []float64 {
|
||||||
|
if count < 2 {
|
||||||
|
panic("LinearBuckets needs a count > 1")
|
||||||
|
}
|
||||||
|
buckets := make([]float64, count-1)
|
||||||
|
for i := range buckets {
|
||||||
|
buckets[i] = start
|
||||||
|
start += width
|
||||||
|
}
|
||||||
|
return buckets
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExponentialBuckets creates 'count' buckets, where the lowest bucket has an
|
||||||
|
// upper bound of 'start' and each following bucket's upper bound is 'factor'
|
||||||
|
// times the previous bucket's upper bound. The final +Inf bucket is counted
|
||||||
|
// towards the total 'count', but not included in the returned slice. The
|
||||||
|
// returned slice is meant to be used for the Buckets field of HistogramOpts.
|
||||||
|
//
|
||||||
|
// The function panics if 'count' is less than 2, if 'start' is 0 or negative,
|
||||||
|
// or if 'factor' is less than or equal 1.
|
||||||
|
func ExponentialBuckets(start, factor float64, count int) []float64 {
|
||||||
|
if count < 2 {
|
||||||
|
panic("ExponentialBuckets needs a count > 1")
|
||||||
|
}
|
||||||
|
if start <= 0 {
|
||||||
|
panic("ExponentialBuckets needs a positive start value")
|
||||||
|
}
|
||||||
|
if factor <= 1 {
|
||||||
|
panic("ExponentialBuckets needs a factor greater than 1")
|
||||||
|
}
|
||||||
|
buckets := make([]float64, count-1)
|
||||||
|
for i := range buckets {
|
||||||
|
buckets[i] = start
|
||||||
|
start *= factor
|
||||||
|
}
|
||||||
|
return buckets
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistogramOpts bundles the options for creating a Histogram metric. It is
|
||||||
|
// mandatory to set Name and Help to a non-empty string. All other fields are
|
||||||
|
// optional and can safely be left at their zero value.
|
||||||
|
type HistogramOpts struct {
|
||||||
|
// Namespace, Subsystem, and Name are components of the fully-qualified
|
||||||
|
// name of the Histogram (created by joining these components with
|
||||||
|
// "_"). Only Name is mandatory, the others merely help structuring the
|
||||||
|
// name. Note that the fully-qualified name of the Histogram must be a
|
||||||
|
// valid Prometheus metric name.
|
||||||
|
Namespace string
|
||||||
|
Subsystem string
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Help provides information about this Histogram. Mandatory!
|
||||||
|
//
|
||||||
|
// Metrics with the same fully-qualified name must have the same Help
|
||||||
|
// string.
|
||||||
|
Help string
|
||||||
|
|
||||||
|
// ConstLabels are used to attach fixed labels to this
|
||||||
|
// Histogram. Histograms with the same fully-qualified name must have the
|
||||||
|
// same label names in their ConstLabels.
|
||||||
|
//
|
||||||
|
// Note that in most cases, labels have a value that varies during the
|
||||||
|
// lifetime of a process. Those labels are usually managed with a
|
||||||
|
// HistogramVec. ConstLabels serve only special purposes. One is for the
|
||||||
|
// special case where the value of a label does not change during the
|
||||||
|
// lifetime of a process, e.g. if the revision of the running binary is
|
||||||
|
// put into a label. Another, more advanced purpose is if more than one
|
||||||
|
// Collector needs to collect Histograms with the same fully-qualified
|
||||||
|
// name. In that case, those Summaries must differ in the values of
|
||||||
|
// their ConstLabels. See the Collector examples.
|
||||||
|
//
|
||||||
|
// If the value of a label never changes (not even between binaries),
|
||||||
|
// that label most likely should not be a label at all (but part of the
|
||||||
|
// metric name).
|
||||||
|
ConstLabels Labels
|
||||||
|
|
||||||
|
// Buckets defines the buckets into which observations are counted. Each
|
||||||
|
// element in the slice is the upper inclusive bound of a bucket. The
|
||||||
|
// values must be sorted in strictly increasing order. There is no need
|
||||||
|
// to add a highest bucket with +Inf bound, it will be added
|
||||||
|
// implicitly. The default value is DefObjectives.
|
||||||
|
Buckets []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHistogram creates a new Histogram based on the provided HistogramOpts. It
|
||||||
|
// panics if the buckets in HistogramOpts are not in strictly increasing order.
|
||||||
|
func NewHistogram(opts HistogramOpts) Histogram {
|
||||||
|
return newHistogram(
|
||||||
|
NewDesc(
|
||||||
|
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
opts.Help,
|
||||||
|
nil,
|
||||||
|
opts.ConstLabels,
|
||||||
|
),
|
||||||
|
opts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogram {
|
||||||
|
if len(desc.variableLabels) != len(labelValues) {
|
||||||
|
panic(errInconsistentCardinality)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range desc.variableLabels {
|
||||||
|
if n == "le" {
|
||||||
|
panic("'le' is not allowed as label name in histograms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, lp := range desc.constLabelPairs {
|
||||||
|
if lp.GetName() == "le" {
|
||||||
|
panic("'le' is not allowed as label name in histograms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.Buckets) == 0 {
|
||||||
|
opts.Buckets = DefBuckets
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &histogram{
|
||||||
|
desc: desc,
|
||||||
|
upperBounds: opts.Buckets,
|
||||||
|
labelPairs: makeLabelPairs(desc, labelValues),
|
||||||
|
}
|
||||||
|
for i, upperBound := range h.upperBounds {
|
||||||
|
if i < len(h.upperBounds)-1 {
|
||||||
|
if upperBound >= h.upperBounds[i+1] {
|
||||||
|
panic(fmt.Errorf(
|
||||||
|
"histogram buckets must be in increasing order: %f >= %f",
|
||||||
|
upperBound, h.upperBounds[i+1],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if math.IsInf(upperBound, +1) {
|
||||||
|
// The +Inf bucket is implicit. Remove it here.
|
||||||
|
h.upperBounds = h.upperBounds[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Finally we know the final length of h.upperBounds and can make counts.
|
||||||
|
h.counts = make([]uint64, len(h.upperBounds))
|
||||||
|
|
||||||
|
h.Init(h) // Init self-collection.
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
type histogram struct {
|
||||||
|
SelfCollector
|
||||||
|
// Note that there is no mutex required.
|
||||||
|
|
||||||
|
desc *Desc
|
||||||
|
|
||||||
|
upperBounds []float64
|
||||||
|
counts []uint64
|
||||||
|
|
||||||
|
labelPairs []*dto.LabelPair
|
||||||
|
|
||||||
|
sumBits uint64 // The bits of the float64 representing the sum of all observations.
|
||||||
|
count uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *histogram) Desc() *Desc {
|
||||||
|
return h.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *histogram) Observe(v float64) {
|
||||||
|
for i, upperBound := range h.upperBounds {
|
||||||
|
if v <= upperBound {
|
||||||
|
atomic.AddUint64(&h.counts[i], 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atomic.AddUint64(&h.count, 1)
|
||||||
|
for {
|
||||||
|
oldBits := atomic.LoadUint64(&h.sumBits)
|
||||||
|
newBits := math.Float64bits(math.Float64frombits(oldBits) + v)
|
||||||
|
if atomic.CompareAndSwapUint64(&h.sumBits, oldBits, newBits) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *histogram) Write(out *dto.Metric) error {
|
||||||
|
his := &dto.Histogram{}
|
||||||
|
buckets := make([]*dto.Bucket, len(h.upperBounds))
|
||||||
|
|
||||||
|
his.SampleSum = proto.Float64(math.Float64frombits(atomic.LoadUint64(&h.sumBits)))
|
||||||
|
his.SampleCount = proto.Uint64(atomic.LoadUint64(&h.count))
|
||||||
|
var count uint64
|
||||||
|
for i, upperBound := range h.upperBounds {
|
||||||
|
count += atomic.LoadUint64(&h.counts[i])
|
||||||
|
buckets[i] = &dto.Bucket{
|
||||||
|
CumulativeCount: proto.Uint64(count),
|
||||||
|
UpperBound: proto.Float64(upperBound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
his.Bucket = buckets
|
||||||
|
out.Histogram = his
|
||||||
|
out.Label = h.labelPairs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistogramVec is a Collector that bundles a set of Histograms that all share the
|
||||||
|
// same Desc, but have different values for their variable labels. This is used
|
||||||
|
// if you want to count the same thing partitioned by various dimensions
|
||||||
|
// (e.g. http request latencies, partitioned by status code and method). Create
|
||||||
|
// instances with NewHistogramVec.
|
||||||
|
type HistogramVec struct {
|
||||||
|
MetricVec
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHistogramVec creates a new HistogramVec based on the provided HistogramOpts and
|
||||||
|
// partitioned by the given label names. At least one label name must be
|
||||||
|
// provided.
|
||||||
|
func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec {
|
||||||
|
desc := NewDesc(
|
||||||
|
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
opts.Help,
|
||||||
|
labelNames,
|
||||||
|
opts.ConstLabels,
|
||||||
|
)
|
||||||
|
return &HistogramVec{
|
||||||
|
MetricVec: MetricVec{
|
||||||
|
children: map[uint64]Metric{},
|
||||||
|
desc: desc,
|
||||||
|
hash: fnv.New64a(),
|
||||||
|
newMetric: func(lvs ...string) Metric {
|
||||||
|
return newHistogram(desc, opts, lvs...)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetricWithLabelValues replaces the method of the same name in
|
||||||
|
// MetricVec. The difference is that this method returns a Histogram and not a
|
||||||
|
// Metric so that no type conversion is required.
|
||||||
|
func (m *HistogramVec) GetMetricWithLabelValues(lvs ...string) (Histogram, error) {
|
||||||
|
metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...)
|
||||||
|
if metric != nil {
|
||||||
|
return metric.(Histogram), err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetricWith replaces the method of the same name in MetricVec. The
|
||||||
|
// difference is that this method returns a Histogram and not a Metric so that no
|
||||||
|
// type conversion is required.
|
||||||
|
func (m *HistogramVec) GetMetricWith(labels Labels) (Histogram, error) {
|
||||||
|
metric, err := m.MetricVec.GetMetricWith(labels)
|
||||||
|
if metric != nil {
|
||||||
|
return metric.(Histogram), err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLabelValues works as GetMetricWithLabelValues, but panics where
|
||||||
|
// GetMetricWithLabelValues would have returned an error. By not returning an
|
||||||
|
// error, WithLabelValues allows shortcuts like
|
||||||
|
// myVec.WithLabelValues("404", "GET").Observe(42.21)
|
||||||
|
func (m *HistogramVec) WithLabelValues(lvs ...string) Histogram {
|
||||||
|
return m.MetricVec.WithLabelValues(lvs...).(Histogram)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
|
||||||
|
// returned an error. By not returning an error, With allows shortcuts like
|
||||||
|
// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21)
|
||||||
|
func (m *HistogramVec) With(labels Labels) Histogram {
|
||||||
|
return m.MetricVec.With(labels).(Histogram)
|
||||||
|
}
|
|
@ -0,0 +1,318 @@
|
||||||
|
// Copyright 2015 The Prometheus 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 prometheus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"testing/quick"
|
||||||
|
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func benchmarkHistogramObserve(w int, b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
wg.Add(w)
|
||||||
|
|
||||||
|
g := new(sync.WaitGroup)
|
||||||
|
g.Add(1)
|
||||||
|
|
||||||
|
s := NewHistogram(HistogramOpts{})
|
||||||
|
|
||||||
|
for i := 0; i < w; i++ {
|
||||||
|
go func() {
|
||||||
|
g.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s.Observe(float64(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
g.Done()
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramObserve1(b *testing.B) {
|
||||||
|
benchmarkHistogramObserve(1, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramObserve2(b *testing.B) {
|
||||||
|
benchmarkHistogramObserve(2, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramObserve4(b *testing.B) {
|
||||||
|
benchmarkHistogramObserve(4, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramObserve8(b *testing.B) {
|
||||||
|
benchmarkHistogramObserve(8, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkHistogramWrite(w int, b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
wg.Add(w)
|
||||||
|
|
||||||
|
g := new(sync.WaitGroup)
|
||||||
|
g.Add(1)
|
||||||
|
|
||||||
|
s := NewHistogram(HistogramOpts{})
|
||||||
|
|
||||||
|
for i := 0; i < 1000000; i++ {
|
||||||
|
s.Observe(float64(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := 0; j < w; j++ {
|
||||||
|
outs := make([]dto.Metric, b.N)
|
||||||
|
|
||||||
|
go func(o []dto.Metric) {
|
||||||
|
g.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s.Write(&o[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
}(outs)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
g.Done()
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramWrite1(b *testing.B) {
|
||||||
|
benchmarkHistogramWrite(1, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramWrite2(b *testing.B) {
|
||||||
|
benchmarkHistogramWrite(2, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramWrite4(b *testing.B) {
|
||||||
|
benchmarkHistogramWrite(4, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHistogramWrite8(b *testing.B) {
|
||||||
|
benchmarkHistogramWrite(8, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intentionally adding +Inf here to test if that case is handled correctly.
|
||||||
|
// Also, getCumulativeCounts depends on it.
|
||||||
|
var testBuckets = []float64{-2, -1, -0.5, 0, 0.5, 1, 2, math.Inf(+1)}
|
||||||
|
|
||||||
|
func TestHistogramConcurrency(t *testing.T) {
|
||||||
|
rand.Seed(42)
|
||||||
|
|
||||||
|
it := func(n uint32) bool {
|
||||||
|
mutations := int(n%1e4 + 1e4)
|
||||||
|
concLevel := int(n%5 + 1)
|
||||||
|
total := mutations * concLevel
|
||||||
|
|
||||||
|
var start, end sync.WaitGroup
|
||||||
|
start.Add(1)
|
||||||
|
end.Add(concLevel)
|
||||||
|
|
||||||
|
sum := NewHistogram(HistogramOpts{
|
||||||
|
Name: "test_histogram",
|
||||||
|
Help: "helpless",
|
||||||
|
Buckets: testBuckets,
|
||||||
|
})
|
||||||
|
|
||||||
|
allVars := make([]float64, total)
|
||||||
|
var sampleSum float64
|
||||||
|
for i := 0; i < concLevel; i++ {
|
||||||
|
vals := make([]float64, mutations)
|
||||||
|
for j := 0; j < mutations; j++ {
|
||||||
|
v := rand.NormFloat64()
|
||||||
|
vals[j] = v
|
||||||
|
allVars[i*mutations+j] = v
|
||||||
|
sampleSum += v
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(vals []float64) {
|
||||||
|
start.Wait()
|
||||||
|
for _, v := range vals {
|
||||||
|
sum.Observe(v)
|
||||||
|
}
|
||||||
|
end.Done()
|
||||||
|
}(vals)
|
||||||
|
}
|
||||||
|
sort.Float64s(allVars)
|
||||||
|
start.Done()
|
||||||
|
end.Wait()
|
||||||
|
|
||||||
|
m := &dto.Metric{}
|
||||||
|
sum.Write(m)
|
||||||
|
if got, want := int(*m.Histogram.SampleCount), total; got != want {
|
||||||
|
t.Errorf("got sample count %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := *m.Histogram.SampleSum, sampleSum; math.Abs((got-want)/want) > 0.001 {
|
||||||
|
t.Errorf("got sample sum %f, want %f", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantCounts := getCumulativeCounts(allVars)
|
||||||
|
|
||||||
|
if got, want := len(m.Histogram.Bucket), len(testBuckets)-1; got != want {
|
||||||
|
t.Errorf("got %d buckets in protobuf, want %d", got, want)
|
||||||
|
}
|
||||||
|
for i, wantBound := range testBuckets {
|
||||||
|
if i == len(testBuckets)-1 {
|
||||||
|
break // No +Inf bucket in protobuf.
|
||||||
|
}
|
||||||
|
if gotBound := *m.Histogram.Bucket[i].UpperBound; gotBound != wantBound {
|
||||||
|
t.Errorf("got bound %f, want %f", gotBound, wantBound)
|
||||||
|
}
|
||||||
|
if gotCount, wantCount := *m.Histogram.Bucket[i].CumulativeCount, wantCounts[i]; gotCount != wantCount {
|
||||||
|
t.Errorf("got count %d, want %d", gotCount, wantCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := quick.Check(it, nil); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHistogramVecConcurrency(t *testing.T) {
|
||||||
|
rand.Seed(42)
|
||||||
|
|
||||||
|
objectives := make([]float64, 0, len(DefObjectives))
|
||||||
|
for qu := range DefObjectives {
|
||||||
|
|
||||||
|
objectives = append(objectives, qu)
|
||||||
|
}
|
||||||
|
sort.Float64s(objectives)
|
||||||
|
|
||||||
|
it := func(n uint32) bool {
|
||||||
|
mutations := int(n%1e4 + 1e4)
|
||||||
|
concLevel := int(n%7 + 1)
|
||||||
|
vecLength := int(n%3 + 1)
|
||||||
|
|
||||||
|
var start, end sync.WaitGroup
|
||||||
|
start.Add(1)
|
||||||
|
end.Add(concLevel)
|
||||||
|
|
||||||
|
his := NewHistogramVec(
|
||||||
|
HistogramOpts{
|
||||||
|
Name: "test_histogram",
|
||||||
|
Help: "helpless",
|
||||||
|
Buckets: []float64{-2, -1, -0.5, 0, 0.5, 1, 2, math.Inf(+1)},
|
||||||
|
},
|
||||||
|
[]string{"label"},
|
||||||
|
)
|
||||||
|
|
||||||
|
allVars := make([][]float64, vecLength)
|
||||||
|
sampleSums := make([]float64, vecLength)
|
||||||
|
for i := 0; i < concLevel; i++ {
|
||||||
|
vals := make([]float64, mutations)
|
||||||
|
picks := make([]int, mutations)
|
||||||
|
for j := 0; j < mutations; j++ {
|
||||||
|
v := rand.NormFloat64()
|
||||||
|
vals[j] = v
|
||||||
|
pick := rand.Intn(vecLength)
|
||||||
|
picks[j] = pick
|
||||||
|
allVars[pick] = append(allVars[pick], v)
|
||||||
|
sampleSums[pick] += v
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(vals []float64) {
|
||||||
|
start.Wait()
|
||||||
|
for i, v := range vals {
|
||||||
|
his.WithLabelValues(string('A' + picks[i])).Observe(v)
|
||||||
|
}
|
||||||
|
end.Done()
|
||||||
|
}(vals)
|
||||||
|
}
|
||||||
|
for _, vars := range allVars {
|
||||||
|
sort.Float64s(vars)
|
||||||
|
}
|
||||||
|
start.Done()
|
||||||
|
end.Wait()
|
||||||
|
|
||||||
|
for i := 0; i < vecLength; i++ {
|
||||||
|
m := &dto.Metric{}
|
||||||
|
s := his.WithLabelValues(string('A' + i))
|
||||||
|
s.Write(m)
|
||||||
|
|
||||||
|
if got, want := len(m.Histogram.Bucket), len(testBuckets)-1; got != want {
|
||||||
|
t.Errorf("got %d buckets in protobuf, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := int(*m.Histogram.SampleCount), len(allVars[i]); got != want {
|
||||||
|
t.Errorf("got sample count %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := *m.Histogram.SampleSum, sampleSums[i]; math.Abs((got-want)/want) > 0.001 {
|
||||||
|
t.Errorf("got sample sum %f, want %f", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantCounts := getCumulativeCounts(allVars[i])
|
||||||
|
|
||||||
|
for j, wantBound := range testBuckets {
|
||||||
|
if j == len(testBuckets)-1 {
|
||||||
|
break // No +Inf bucket in protobuf.
|
||||||
|
}
|
||||||
|
if gotBound := *m.Histogram.Bucket[j].UpperBound; gotBound != wantBound {
|
||||||
|
t.Errorf("got bound %f, want %f", gotBound, wantBound)
|
||||||
|
}
|
||||||
|
if gotCount, wantCount := *m.Histogram.Bucket[j].CumulativeCount, wantCounts[j]; gotCount != wantCount {
|
||||||
|
t.Errorf("got count %d, want %d", gotCount, wantCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := quick.Check(it, nil); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCumulativeCounts(vars []float64) []uint64 {
|
||||||
|
counts := make([]uint64, len(testBuckets))
|
||||||
|
for _, v := range vars {
|
||||||
|
for i := len(testBuckets) - 1; i >= 0; i-- {
|
||||||
|
if v > testBuckets[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
counts[i]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuckets(t *testing.T) {
|
||||||
|
got := LinearBuckets(-15, 5, 7)
|
||||||
|
want := []float64{-15, -10, -5, 0, 5, 10}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("linear buckets: got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = ExponentialBuckets(100, 1.2, 4)
|
||||||
|
want = []float64{100, 120, 144}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("linear buckets: got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,12 @@ import (
|
||||||
// Summary provides the median, the 90th and the 99th percentile of the latency
|
// Summary provides the median, the 90th and the 99th percentile of the latency
|
||||||
// as rank estimations.
|
// as rank estimations.
|
||||||
//
|
//
|
||||||
|
// Note that the rank estimations cannot be aggregated in a meaningful way with
|
||||||
|
// the Prometheus query language (i.e. you cannot average or add them). If you
|
||||||
|
// need aggregatable quantiles (e.g. you want the 99th percentile latency of all
|
||||||
|
// queries served across all instances of a service), check out the Histogram
|
||||||
|
// metric type. See the Prometheus documentation for more details.
|
||||||
|
//
|
||||||
// To create Summary instances, use NewSummary.
|
// To create Summary instances, use NewSummary.
|
||||||
type Summary interface {
|
type Summary interface {
|
||||||
Metric
|
Metric
|
||||||
|
@ -110,7 +116,10 @@ type SummaryOpts struct {
|
||||||
// AgeBuckets is the number of buckets used to exclude observations that
|
// AgeBuckets is the number of buckets used to exclude observations that
|
||||||
// are older than MaxAge from the summary. A higher number has a
|
// are older than MaxAge from the summary. A higher number has a
|
||||||
// resource penalty, so only increase it if the higher resolution is
|
// resource penalty, so only increase it if the higher resolution is
|
||||||
// really required. The default value is DefAgeBuckets.
|
// really required. For very high observation rates, you might want to
|
||||||
|
// reduce the number of age buckets. With only one age bucket, you will
|
||||||
|
// effectively see a complete reset of the summary each time MaxAge has
|
||||||
|
// passed. The default value is DefAgeBuckets.
|
||||||
AgeBuckets uint32
|
AgeBuckets uint32
|
||||||
|
|
||||||
// BufCap defines the default sample stream buffer size. The default
|
// BufCap defines the default sample stream buffer size. The default
|
||||||
|
@ -119,10 +128,6 @@ type SummaryOpts struct {
|
||||||
// is the internal buffer size of the underlying package
|
// is the internal buffer size of the underlying package
|
||||||
// "github.com/bmizerany/perks/quantile").
|
// "github.com/bmizerany/perks/quantile").
|
||||||
BufCap uint32
|
BufCap uint32
|
||||||
|
|
||||||
// Epsilon is the error epsilon for the quantile rank estimate. Must be
|
|
||||||
// positive. The default is DefEpsilon.
|
|
||||||
Epsilon float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Great fuck-up with the sliding-window decay algorithm... The Merge
|
// TODO: Great fuck-up with the sliding-window decay algorithm... The Merge
|
||||||
|
@ -411,14 +416,14 @@ func (m *SummaryVec) GetMetricWith(labels Labels) (Summary, error) {
|
||||||
// WithLabelValues works as GetMetricWithLabelValues, but panics where
|
// WithLabelValues works as GetMetricWithLabelValues, but panics where
|
||||||
// GetMetricWithLabelValues would have returned an error. By not returning an
|
// GetMetricWithLabelValues would have returned an error. By not returning an
|
||||||
// error, WithLabelValues allows shortcuts like
|
// error, WithLabelValues allows shortcuts like
|
||||||
// myVec.WithLabelValues("404", "GET").Add(42)
|
// myVec.WithLabelValues("404", "GET").Observe(42.21)
|
||||||
func (m *SummaryVec) WithLabelValues(lvs ...string) Summary {
|
func (m *SummaryVec) WithLabelValues(lvs ...string) Summary {
|
||||||
return m.MetricVec.WithLabelValues(lvs...).(Summary)
|
return m.MetricVec.WithLabelValues(lvs...).(Summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
|
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
|
||||||
// returned an error. By not returning an error, With allows shortcuts like
|
// returned an error. By not returning an error, With allows shortcuts like
|
||||||
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
|
// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21)
|
||||||
func (m *SummaryVec) With(labels Labels) Summary {
|
func (m *SummaryVec) With(labels Labels) Summary {
|
||||||
return m.MetricVec.With(labels).(Summary)
|
return m.MetricVec.With(labels).(Summary)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue