From b37ca982a9e1b999f0bfef90ccc82e6fa6b84ce4 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 12 Feb 2015 20:55:56 +0100 Subject: [PATCH 1/7] Add support for optionally missing +Inf bucket in protobuf. Also, clean up style issues in metricfamilyprocessor.go. --- extraction/metricfamilyprocessor.go | 135 ++++++++++++++++------------ text/create.go | 18 +++- text/create_test.go | 44 +++++++++ 3 files changed, 140 insertions(+), 57 deletions(-) diff --git a/extraction/metricfamilyprocessor.go b/extraction/metricfamilyprocessor.go index 5ed5343..608e786 100644 --- a/extraction/metricfamilyprocessor.go +++ b/extraction/metricfamilyprocessor.go @@ -16,6 +16,7 @@ package extraction import ( "fmt" "io" + "math" dto "github.com/prometheus/client_model/go" @@ -85,7 +86,10 @@ func extractCounter(out Ingester, o *ProcessOptions, f *dto.MetricFamily) error continue } - sample := new(model.Sample) + sample := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(m.Counter.GetValue()), + } samples = append(samples, sample) if m.TimestampMs != nil { @@ -93,16 +97,12 @@ func extractCounter(out Ingester, o *ProcessOptions, f *dto.MetricFamily) error } else { sample.Timestamp = o.Timestamp } - sample.Metric = model.Metric{} - metric := sample.Metric + metric := sample.Metric for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } - metric[model.MetricNameLabel] = model.LabelValue(f.GetName()) - - sample.Value = model.SampleValue(m.Counter.GetValue()) } return out.Ingest(samples) @@ -116,7 +116,10 @@ func extractGauge(out Ingester, o *ProcessOptions, f *dto.MetricFamily) error { continue } - sample := new(model.Sample) + sample := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(m.Gauge.GetValue()), + } samples = append(samples, sample) if m.TimestampMs != nil { @@ -124,16 +127,12 @@ func extractGauge(out Ingester, o *ProcessOptions, f *dto.MetricFamily) error { } else { sample.Timestamp = o.Timestamp } - sample.Metric = model.Metric{} - metric := sample.Metric + metric := sample.Metric for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } - metric[model.MetricNameLabel] = model.LabelValue(f.GetName()) - - sample.Value = model.SampleValue(m.Gauge.GetValue()) } return out.Ingest(samples) @@ -153,48 +152,50 @@ func extractSummary(out Ingester, o *ProcessOptions, f *dto.MetricFamily) error } for _, q := range m.Summary.Quantile { - sample := new(model.Sample) + sample := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(q.GetValue()), + Timestamp: timestamp, + } samples = append(samples, sample) - sample.Timestamp = timestamp - sample.Metric = model.Metric{} metric := sample.Metric - for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } // BUG(matt): Update other names to "quantile". metric[model.LabelName("quantile")] = model.LabelValue(fmt.Sprint(q.GetQuantile())) - metric[model.MetricNameLabel] = model.LabelValue(f.GetName()) - - sample.Value = model.SampleValue(q.GetValue()) } if m.Summary.SampleSum != nil { - sum := new(model.Sample) - sum.Timestamp = timestamp - metric := model.Metric{} + sum := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(m.Summary.GetSampleSum()), + Timestamp: timestamp, + } + samples = append(samples, sum) + + metric := sum.Metric for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } metric[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_sum") - sum.Metric = metric - sum.Value = model.SampleValue(m.Summary.GetSampleSum()) - samples = append(samples, sum) } if m.Summary.SampleCount != nil { - count := new(model.Sample) - count.Timestamp = timestamp - metric := model.Metric{} + count := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(m.Summary.GetSampleCount()), + Timestamp: timestamp, + } + samples = append(samples, count) + + metric := count.Metric for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } metric[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_count") - count.Metric = metric - count.Value = model.SampleValue(m.Summary.GetSampleCount()) - samples = append(samples, count) } } @@ -209,7 +210,10 @@ func extractUntyped(out Ingester, o *ProcessOptions, f *dto.MetricFamily) error continue } - sample := new(model.Sample) + sample := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(m.Untyped.GetValue()), + } samples = append(samples, sample) if m.TimestampMs != nil { @@ -217,16 +221,12 @@ func extractUntyped(out Ingester, o *ProcessOptions, f *dto.MetricFamily) error } else { sample.Timestamp = o.Timestamp } - sample.Metric = model.Metric{} - metric := sample.Metric + metric := sample.Metric for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } - metric[model.MetricNameLabel] = model.LabelValue(f.GetName()) - - sample.Value = model.SampleValue(m.Untyped.GetValue()) } return out.Ingest(samples) @@ -245,49 +245,72 @@ func extractHistogram(out Ingester, o *ProcessOptions, f *dto.MetricFamily) erro timestamp = model.TimestampFromUnixNano(*m.TimestampMs * 1000000) } + infSeen := false + for _, q := range m.Histogram.Bucket { - sample := new(model.Sample) + sample := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(q.GetCumulativeCount()), + Timestamp: timestamp, + } samples = append(samples, sample) - sample.Timestamp = timestamp - sample.Metric = model.Metric{} metric := sample.Metric - for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } metric[model.LabelName("le")] = model.LabelValue(fmt.Sprint(q.GetUpperBound())) - metric[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_bucket") - sample.Value = model.SampleValue(q.GetCumulativeCount()) + if math.IsInf(q.GetUpperBound(), +1) { + infSeen = true + } } - // TODO: If +Inf bucket is missing, add it. if m.Histogram.SampleSum != nil { - sum := new(model.Sample) - sum.Timestamp = timestamp - metric := model.Metric{} + sum := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(m.Histogram.GetSampleSum()), + Timestamp: timestamp, + } + samples = append(samples, sum) + + metric := sum.Metric for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } metric[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_sum") - sum.Metric = metric - sum.Value = model.SampleValue(m.Histogram.GetSampleSum()) - samples = append(samples, sum) } if m.Histogram.SampleCount != nil { - count := new(model.Sample) - count.Timestamp = timestamp - metric := model.Metric{} + count := &model.Sample{ + Metric: model.Metric{}, + Value: model.SampleValue(m.Histogram.GetSampleCount()), + Timestamp: timestamp, + } + samples = append(samples, count) + + metric := count.Metric for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } metric[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_count") - count.Metric = metric - count.Value = model.SampleValue(m.Histogram.GetSampleCount()) - samples = append(samples, count) + + if !infSeen { + infBucket := &model.Sample{ + Metric: model.Metric{}, + Value: count.Value, + Timestamp: timestamp, + } + samples = append(samples, infBucket) + + metric := infBucket.Metric + for _, p := range m.Label { + metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) + } + metric[model.LabelName("le")] = model.LabelValue("+Inf") + metric[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_bucket") + } } } diff --git a/text/create.go b/text/create.go index 64e372c..1b1cbd3 100644 --- a/text/create.go +++ b/text/create.go @@ -24,6 +24,7 @@ import ( "bytes" "fmt" "io" + "math" "strings" dto "github.com/prometheus/client_model/go" @@ -145,6 +146,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) { "expected summary in metric %s", metric, ) } + infSeen := false for _, q := range metric.Histogram.Bucket { n, err = writeSample( name+"_bucket", metric, @@ -156,7 +158,21 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) { if err != nil { return written, err } - // TODO: Add +inf bucket if it's missing. + if math.IsInf(q.GetUpperBound(), +1) { + infSeen = true + } + } + if !infSeen { + n, err = writeSample( + name+"_bucket", metric, + "le", "+Inf", + float64(metric.Histogram.GetSampleCount()), + out, + ) + if err != nil { + return written, err + } + written += n } n, err = writeSample( name+"_sum", metric, "", "", diff --git a/text/create_test.go b/text/create_test.go index 9326e6e..fe938de 100644 --- a/text/create_test.go +++ b/text/create_test.go @@ -267,6 +267,50 @@ request_duration_microseconds_bucket{le="172.8"} 1524 request_duration_microseconds_bucket{le="+Inf"} 2693 request_duration_microseconds_sum 1.7560473e+06 request_duration_microseconds_count 2693 +`, + }, + // 5: Histogram with missing +Inf bucket. + { + in: &dto.MetricFamily{ + Name: proto.String("request_duration_microseconds"), + Help: proto.String("The response latency."), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + &dto.Metric{ + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2693), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + &dto.Bucket{ + UpperBound: proto.Float64(100), + CumulativeCount: proto.Uint64(123), + }, + &dto.Bucket{ + UpperBound: proto.Float64(120), + CumulativeCount: proto.Uint64(412), + }, + &dto.Bucket{ + UpperBound: proto.Float64(144), + CumulativeCount: proto.Uint64(592), + }, + &dto.Bucket{ + UpperBound: proto.Float64(172.8), + CumulativeCount: proto.Uint64(1524), + }, + }, + }, + }, + }, + }, + out: `# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} 412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693 `, }, } From 6958242277a8843a98fbd8ccc9afd819cb90e65a Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 18 Feb 2015 19:23:34 +0100 Subject: [PATCH 2/7] Add support for histograms to the Go client library. --- prometheus/benchmark_test.go | 28 +++ prometheus/examples_test.go | 46 +++++ prometheus/histogram.go | 329 +++++++++++++++++++++++++++++++++++ prometheus/histogram_test.go | 318 +++++++++++++++++++++++++++++++++ prometheus/summary.go | 19 +- 5 files changed, 733 insertions(+), 7 deletions(-) create mode 100644 prometheus/histogram.go create mode 100644 prometheus/histogram_test.go diff --git a/prometheus/benchmark_test.go b/prometheus/benchmark_test.go index d43a857..6ae7333 100644 --- a/prometheus/benchmark_test.go +++ b/prometheus/benchmark_test.go @@ -129,3 +129,31 @@ func BenchmarkSummaryNoLabels(b *testing.B) { 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) + } +} diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index 78b1b81..64139a8 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -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 + // > + // > +} diff --git a/prometheus/histogram.go b/prometheus/histogram.go new file mode 100644 index 0000000..1d3cfb1 --- /dev/null +++ b/prometheus/histogram.go @@ -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) +} diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go new file mode 100644 index 0000000..e4ed07d --- /dev/null +++ b/prometheus/histogram_test.go @@ -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) + } +} diff --git a/prometheus/summary.go b/prometheus/summary.go index 5d8b82f..0626d08 100644 --- a/prometheus/summary.go +++ b/prometheus/summary.go @@ -35,6 +35,12 @@ import ( // Summary provides the median, the 90th and the 99th percentile of the latency // 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. type Summary interface { Metric @@ -110,7 +116,10 @@ type SummaryOpts struct { // AgeBuckets is the number of buckets used to exclude observations that // are older than MaxAge from the summary. A higher number has a // 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 // 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 // "github.com/bmizerany/perks/quantile"). 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 @@ -411,14 +416,14 @@ func (m *SummaryVec) GetMetricWith(labels Labels) (Summary, error) { // 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").Add(42) +// myVec.WithLabelValues("404", "GET").Observe(42.21) func (m *SummaryVec) WithLabelValues(lvs ...string) Summary { return m.MetricVec.WithLabelValues(lvs...).(Summary) } // 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"}).Add(42) +// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21) func (m *SummaryVec) With(labels Labels) Summary { return m.MetricVec.With(labels).(Summary) } From 4c4f51d546c117c007e31b7c486c634b54ef16db Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 19 Feb 2015 02:01:06 +0100 Subject: [PATCH 3/7] Catch illegal label names for summaries in histograms. --- prometheus/summary.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/prometheus/summary.go b/prometheus/summary.go index 0626d08..2d4bc55 100644 --- a/prometheus/summary.go +++ b/prometheus/summary.go @@ -163,6 +163,17 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { panic(errInconsistentCardinality) } + for _, n := range desc.variableLabels { + if n == "quantile" { + panic("'quantile' is not allowed as label name in summaries") + } + } + for _, lp := range desc.constLabelPairs { + if lp.GetName() == "quantile" { + panic("'quantile' is not allowed as label name in summaries") + } + } + if len(opts.Objectives) == 0 { opts.Objectives = DefObjectives } From 3e50eddd64a70c22d6c5c50fc6062b5ad5192280 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 19 Feb 2015 12:40:29 +0100 Subject: [PATCH 4/7] Do not count the +Inf bucket for bucket creation. Add explanation for DefBuckets. --- prometheus/examples_test.go | 2 +- prometheus/histogram.go | 34 ++++++++++++++++++---------------- prometheus/histogram_test.go | 4 ++-- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index 64139a8..a82d32c 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -457,7 +457,7 @@ 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. + Buckets: prometheus.LinearBuckets(20, 5, 5), // 5 buckets, each 5 centigrade wide. }) // Simulate some observations. diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 1d3cfb1..a02fd53 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -47,23 +47,25 @@ type Histogram interface { Observe(float64) } -// DefBuckets are the default Histogram buckets. Most likely, you want to define -// buckets customized to your use case. +// DefBuckets are the default Histogram buckets. The default buckets are +// tailored to broadly measure response time in seconds for a typical online +// serving system. Most likely, however, you will be required 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. +// bucket has an upper bound of 'start'. The final +Inf bucket is not counted +// and 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. +// The function panics if 'count' is zero or negative. func LinearBuckets(start, width float64, count int) []float64 { - if count < 2 { - panic("LinearBuckets needs a count > 1") + if count < 1 { + panic("LinearBuckets needs a positive count") } - buckets := make([]float64, count-1) + buckets := make([]float64, count) for i := range buckets { buckets[i] = start start += width @@ -73,15 +75,15 @@ func LinearBuckets(start, width float64, count int) []float64 { // 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. +// times the previous bucket's upper bound. The final +Inf bucket is not counted +// and 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, +// The function panics if 'count' is 0 or negative, 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 count < 1 { + panic("ExponentialBuckets needs a positive count") } if start <= 0 { panic("ExponentialBuckets needs a positive start value") @@ -89,7 +91,7 @@ func ExponentialBuckets(start, factor float64, count int) []float64 { if factor <= 1 { panic("ExponentialBuckets needs a factor greater than 1") } - buckets := make([]float64, count-1) + buckets := make([]float64, count) for i := range buckets { buckets[i] = start start *= factor diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go index e4ed07d..855af46 100644 --- a/prometheus/histogram_test.go +++ b/prometheus/histogram_test.go @@ -304,13 +304,13 @@ func getCumulativeCounts(vars []float64) []uint64 { } func TestBuckets(t *testing.T) { - got := LinearBuckets(-15, 5, 7) + got := LinearBuckets(-15, 5, 6) 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) + got = ExponentialBuckets(100, 1.2, 3) want = []float64{100, 120, 144} if !reflect.DeepEqual(got, want) { t.Errorf("linear buckets: got %v, want %v", got, want) From b1e7299877c31cec61356b8e8774420548715d9f Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 19 Feb 2015 15:31:43 +0100 Subject: [PATCH 5/7] Turned "le" and "quantile" label names into constants. --- extraction/metricfamilyprocessor.go | 6 +++--- model/labelname.go | 8 ++++++++ prometheus/histogram.go | 23 ++++++++++++++--------- prometheus/summary.go | 15 ++++++++++----- text/create.go | 7 ++++--- text/parse.go | 8 ++++---- 6 files changed, 43 insertions(+), 24 deletions(-) diff --git a/extraction/metricfamilyprocessor.go b/extraction/metricfamilyprocessor.go index 608e786..5edb49c 100644 --- a/extraction/metricfamilyprocessor.go +++ b/extraction/metricfamilyprocessor.go @@ -164,7 +164,7 @@ func extractSummary(out Ingester, o *ProcessOptions, f *dto.MetricFamily) error metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } // BUG(matt): Update other names to "quantile". - metric[model.LabelName("quantile")] = model.LabelValue(fmt.Sprint(q.GetQuantile())) + metric[model.LabelName(model.QuantileLabel)] = model.LabelValue(fmt.Sprint(q.GetQuantile())) metric[model.MetricNameLabel] = model.LabelValue(f.GetName()) } @@ -259,7 +259,7 @@ func extractHistogram(out Ingester, o *ProcessOptions, f *dto.MetricFamily) erro for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } - metric[model.LabelName("le")] = model.LabelValue(fmt.Sprint(q.GetUpperBound())) + metric[model.LabelName(model.BucketLabel)] = model.LabelValue(fmt.Sprint(q.GetUpperBound())) metric[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_bucket") if math.IsInf(q.GetUpperBound(), +1) { @@ -308,7 +308,7 @@ func extractHistogram(out Ingester, o *ProcessOptions, f *dto.MetricFamily) erro for _, p := range m.Label { metric[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue()) } - metric[model.LabelName("le")] = model.LabelValue("+Inf") + metric[model.LabelName(model.BucketLabel)] = model.LabelValue("+Inf") metric[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_bucket") } } diff --git a/model/labelname.go b/model/labelname.go index 047e756..75b2e79 100644 --- a/model/labelname.go +++ b/model/labelname.go @@ -33,6 +33,14 @@ const ( // JobLabel is the label name indicating the job from which a timeseries // was scraped. JobLabel LabelName = "job" + + // BucketLabel is used for the label that defines the upper bound of a + // bucket of a histogram ("le" -> "less or equal"). + BucketLabel = "le" + + // QuantileLabel is used for the label that defines the quantile in a + // summary. + QuantileLabel = "quantile" ) // A LabelName is a key for a LabelSet or Metric. It has a value associated diff --git a/prometheus/histogram.go b/prometheus/histogram.go index a02fd53..7f3da39 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -21,6 +21,7 @@ import ( "github.com/golang/protobuf/proto" + "github.com/prometheus/client_golang/model" dto "github.com/prometheus/client_model/go" ) @@ -33,7 +34,7 @@ import ( // // 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 +// procedures). However, Histograms require 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. @@ -47,12 +48,16 @@ type Histogram interface { Observe(float64) } -// DefBuckets are the default Histogram buckets. The default buckets are -// tailored to broadly measure response time in seconds for a typical online -// serving system. Most likely, however, you will be required to define buckets -// customized to your use case. var ( + // DefBuckets are the default Histogram buckets. The default buckets are + // tailored to broadly measure response time in seconds for a typical online + // serving system. Most likely, however, you will be required to define buckets + // customized to your use case. DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} + + errBucketLabelNotAllowed = fmt.Errorf( + "%q is not allowed as label name in histograms", model.BucketLabel, + ) ) // LinearBuckets creates 'count' buckets, each 'width' wide, where the lowest @@ -165,13 +170,13 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr } for _, n := range desc.variableLabels { - if n == "le" { - panic("'le' is not allowed as label name in histograms") + if n == model.BucketLabel { + panic(errBucketLabelNotAllowed) } } for _, lp := range desc.constLabelPairs { - if lp.GetName() == "le" { - panic("'le' is not allowed as label name in histograms") + if lp.GetName() == model.BucketLabel { + panic(errBucketLabelNotAllowed) } } diff --git a/prometheus/summary.go b/prometheus/summary.go index 2d4bc55..f93cb35 100644 --- a/prometheus/summary.go +++ b/prometheus/summary.go @@ -25,6 +25,7 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/client_golang/_vendor/perks/quantile" + "github.com/prometheus/client_golang/model" ) // A Summary captures individual observations from an event or sample stream and @@ -50,9 +51,13 @@ type Summary interface { Observe(float64) } -// DefObjectives are the default Summary quantile values. var ( + // DefObjectives are the default Summary quantile values. DefObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001} + + errQuantileLabelNotAllowed = fmt.Errorf( + "%q is not allowed as label name in summaries", model.QuantileLabel, + ) ) // Default values for SummaryOpts. @@ -164,13 +169,13 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { } for _, n := range desc.variableLabels { - if n == "quantile" { - panic("'quantile' is not allowed as label name in summaries") + if n == model.QuantileLabel { + panic(errQuantileLabelNotAllowed) } } for _, lp := range desc.constLabelPairs { - if lp.GetName() == "quantile" { - panic("'quantile' is not allowed as label name in summaries") + if lp.GetName() == model.QuantileLabel { + panic(errQuantileLabelNotAllowed) } } diff --git a/text/create.go b/text/create.go index 1b1cbd3..4430459 100644 --- a/text/create.go +++ b/text/create.go @@ -27,6 +27,7 @@ import ( "math" "strings" + "github.com/prometheus/client_golang/model" dto "github.com/prometheus/client_model/go" ) @@ -117,7 +118,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) { for _, q := range metric.Summary.Quantile { n, err = writeSample( name, metric, - "quantile", fmt.Sprint(q.GetQuantile()), + model.QuantileLabel, fmt.Sprint(q.GetQuantile()), q.GetValue(), out, ) @@ -150,7 +151,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) { for _, q := range metric.Histogram.Bucket { n, err = writeSample( name+"_bucket", metric, - "le", fmt.Sprint(q.GetUpperBound()), + model.BucketLabel, fmt.Sprint(q.GetUpperBound()), float64(q.GetCumulativeCount()), out, ) @@ -165,7 +166,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) { if !infSeen { n, err = writeSample( name+"_bucket", metric, - "le", "+Inf", + model.BucketLabel, "+Inf", float64(metric.Histogram.GetSampleCount()), out, ) diff --git a/text/parse.go b/text/parse.go index eaff592..e317d68 100644 --- a/text/parse.go +++ b/text/parse.go @@ -274,8 +274,8 @@ func (p *Parser) startLabelName() stateFn { } // Special summary/histogram treatment. Don't add 'quantile' and 'le' // labels to 'real' labels. - if !(p.currentMF.GetType() == dto.MetricType_SUMMARY && p.currentLabelPair.GetName() == "quantile") && - !(p.currentMF.GetType() == dto.MetricType_HISTOGRAM && p.currentLabelPair.GetName() == "le") { + if !(p.currentMF.GetType() == dto.MetricType_SUMMARY && p.currentLabelPair.GetName() == model.QuantileLabel) && + !(p.currentMF.GetType() == dto.MetricType_HISTOGRAM && p.currentLabelPair.GetName() == model.BucketLabel) { p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPair) } if p.skipBlankTabIfCurrentBlankTab(); p.err != nil { @@ -306,7 +306,7 @@ func (p *Parser) startLabelValue() stateFn { // - Quantile labels are special, will result in dto.Quantile later. // - Other labels have to be added to currentLabels for signature calculation. if p.currentMF.GetType() == dto.MetricType_SUMMARY { - if p.currentLabelPair.GetName() == "quantile" { + if p.currentLabelPair.GetName() == model.QuantileLabel { if p.currentQuantile, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil { // Create a more helpful error message. p.parseError(fmt.Sprintf("expected float as value for 'quantile' label, got %q", p.currentLabelPair.GetValue())) @@ -318,7 +318,7 @@ func (p *Parser) startLabelValue() stateFn { } // Similar special treatment of histograms. if p.currentMF.GetType() == dto.MetricType_HISTOGRAM { - if p.currentLabelPair.GetName() == "le" { + if p.currentLabelPair.GetName() == model.BucketLabel { if p.currentBucket, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil { // Create a more helpful error message. p.parseError(fmt.Sprintf("expected float as value for 'le' label, got %q", p.currentLabelPair.GetValue())) From 000ef4515784d72897cad9042f47acaea7d92255 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 19 Feb 2015 15:34:04 +0100 Subject: [PATCH 6/7] Replaced http by HTTP if used as the name of the protocol in English. --- prometheus/counter.go | 2 +- prometheus/examples_test.go | 6 +++--- prometheus/histogram.go | 2 +- prometheus/http.go | 2 +- prometheus/registry.go | 2 +- prometheus/summary.go | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/prometheus/counter.go b/prometheus/counter.go index d715ee0..f8d633f 100644 --- a/prometheus/counter.go +++ b/prometheus/counter.go @@ -74,7 +74,7 @@ func (c *counter) Add(v float64) { // CounterVec is a Collector that bundles a set of Counters 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. number of http requests, partitioned by response code and +// (e.g. number of HTTP requests, partitioned by response code and // method). Create instances with NewCounterVec. // // CounterVec embeds MetricVec. See there for a full list of methods with diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index a82d32c..5e62967 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -129,7 +129,7 @@ func ExampleCounterVec() { httpReqs := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", - Help: "How many HTTP requests processed, partitioned by status code and http method.", + Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", ConstLabels: prometheus.Labels{"env": *binaryVersion}, }, []string{"code", "method"}, @@ -200,7 +200,7 @@ func ExampleRegister() { fmt.Println("taskCounter registered.") } // Don't forget to tell the HTTP server about the Prometheus handler. - // (In a real program, you still need to start the http server...) + // (In a real program, you still need to start the HTTP server...) http.Handle("/metrics", prometheus.Handler()) // Now you can start workers and give every one of them a pointer to @@ -240,7 +240,7 @@ func ExampleRegister() { // Prometheus will not allow you to ever export metrics with // inconsistent help strings or label names. After unregistering, the - // unregistered metrics will cease to show up in the /metrics http + // unregistered metrics will cease to show up in the /metrics HTTP // response, but the registry still remembers that those metrics had // been exported before. For this example, we will now choose a // different name. (In a real program, you would obviously not export diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 7f3da39..f2fe510 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -270,7 +270,7 @@ func (h *histogram) Write(out *dto.Metric) error { // 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 +// (e.g. HTTP request latencies, partitioned by status code and method). Create // instances with NewHistogramVec. type HistogramVec struct { MetricVec diff --git a/prometheus/http.go b/prometheus/http.go index 818c90f..dac92fd 100644 --- a/prometheus/http.go +++ b/prometheus/http.go @@ -47,7 +47,7 @@ func nowSeries(t ...time.Time) nower { } // InstrumentHandler wraps the given HTTP handler for instrumentation. It -// registers four metric collectors (if not already done) and reports http +// registers four metric collectors (if not already done) and reports HTTP // metrics to the (newly or already) registered collectors: http_requests_total // (CounterVec), http_request_duration_microseconds (Summary), // http_request_size_bytes (Summary), http_response_size_bytes (Summary). Each diff --git a/prometheus/registry.go b/prometheus/registry.go index 505a4aa..f0aa392 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -171,7 +171,7 @@ func SetMetricFamilyInjectionHook(hook func() []*dto.MetricFamily) { } // PanicOnCollectError sets the behavior whether a panic is caused upon an error -// while metrics are collected and served to the http endpoint. By default, an +// while metrics are collected and served to the HTTP endpoint. By default, an // internal server error (status code 500) is served with an error message. func PanicOnCollectError(b bool) { defRegistry.panicOnCollectError = b diff --git a/prometheus/summary.go b/prometheus/summary.go index f93cb35..93e7b6b 100644 --- a/prometheus/summary.go +++ b/prometheus/summary.go @@ -39,7 +39,7 @@ import ( // 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 +// queries served across all instances of a service), consider the Histogram // metric type. See the Prometheus documentation for more details. // // To create Summary instances, use NewSummary. @@ -379,7 +379,7 @@ func (s quantSort) Less(i, j int) bool { // SummaryVec is a Collector that bundles a set of Summaries 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 +// (e.g. HTTP request latencies, partitioned by status code and method). Create // instances with NewSummaryVec. type SummaryVec struct { MetricVec From 9eaf3705d2a0092f6919433a4812f78e4837c4ea Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 19 Feb 2015 16:03:24 +0100 Subject: [PATCH 7/7] Avoid "online serving system". --- prometheus/histogram.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/prometheus/histogram.go b/prometheus/histogram.go index f2fe510..bb14a14 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -50,9 +50,9 @@ type Histogram interface { var ( // DefBuckets are the default Histogram buckets. The default buckets are - // tailored to broadly measure response time in seconds for a typical online - // serving system. Most likely, however, you will be required to define buckets - // customized to your use case. + // tailored to broadly measure the response time (in seconds) of a + // network service. Most likely, however, you will be required to define + // buckets customized to your use case. DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} errBucketLabelNotAllowed = fmt.Errorf(