| // 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" |
| "math" |
| "sort" |
| "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 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. |
| // |
| // To create Histogram instances, use NewHistogram. |
| type Histogram interface { |
| Metric |
| Collector |
| |
| // Observe adds a single observation to the histogram. |
| Observe(float64) |
| } |
| |
| // bucketLabel is used for the label that defines the upper bound of a |
| // bucket of a histogram ("le" -> "less or equal"). |
| const bucketLabel = "le" |
| |
| // DefBuckets are the default Histogram buckets. The default buckets are |
| // 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. |
| var ( |
| 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", bucketLabel, |
| ) |
| ) |
| |
| // LinearBuckets creates 'count' buckets, each 'width' wide, where the lowest |
| // 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 zero or negative. |
| func LinearBuckets(start, width float64, count int) []float64 { |
| if count < 1 { |
| panic("LinearBuckets needs a positive count") |
| } |
| buckets := make([]float64, count) |
| 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 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 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 < 1 { |
| panic("ExponentialBuckets needs a positive count") |
| } |
| 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) |
| 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 DefBuckets. |
| 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 == bucketLabel { |
| panic(errBucketLabelNotAllowed) |
| } |
| } |
| for _, lp := range desc.constLabelPairs { |
| if lp.GetName() == bucketLabel { |
| panic(errBucketLabelNotAllowed) |
| } |
| } |
| |
| 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 { |
| // sumBits contains the bits of the float64 representing the sum of all |
| // observations. sumBits and count have to go first in the struct to |
| // guarantee alignment for atomic operations. |
| // http://golang.org/pkg/sync/atomic/#pkg-note-BUG |
| sumBits uint64 |
| count uint64 |
| |
| selfCollector |
| // Note that there is no mutex required. |
| |
| desc *Desc |
| |
| upperBounds []float64 |
| counts []uint64 |
| |
| labelPairs []*dto.LabelPair |
| } |
| |
| func (h *histogram) Desc() *Desc { |
| return h.desc |
| } |
| |
| func (h *histogram) Observe(v float64) { |
| // TODO(beorn7): For small numbers of buckets (<30), a linear search is |
| // slightly faster than the binary search. If we really care, we could |
| // switch from one search strategy to the other depending on the number |
| // of buckets. |
| // |
| // Microbenchmarks (BenchmarkHistogramNoLabels): |
| // 11 buckets: 38.3 ns/op linear - binary 48.7 ns/op |
| // 100 buckets: 78.1 ns/op linear - binary 54.9 ns/op |
| // 300 buckets: 154 ns/op linear - binary 61.6 ns/op |
| i := sort.SearchFloat64s(h.upperBounds, v) |
| if i < len(h.counts) { |
| atomic.AddUint64(&h.counts[i], 1) |
| } |
| 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: newMetricVec(desc, 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) |
| } |
| |
| type constHistogram struct { |
| desc *Desc |
| count uint64 |
| sum float64 |
| buckets map[float64]uint64 |
| labelPairs []*dto.LabelPair |
| } |
| |
| func (h *constHistogram) Desc() *Desc { |
| return h.desc |
| } |
| |
| func (h *constHistogram) Write(out *dto.Metric) error { |
| his := &dto.Histogram{} |
| buckets := make([]*dto.Bucket, 0, len(h.buckets)) |
| |
| his.SampleCount = proto.Uint64(h.count) |
| his.SampleSum = proto.Float64(h.sum) |
| |
| for upperBound, count := range h.buckets { |
| buckets = append(buckets, &dto.Bucket{ |
| CumulativeCount: proto.Uint64(count), |
| UpperBound: proto.Float64(upperBound), |
| }) |
| } |
| |
| if len(buckets) > 0 { |
| sort.Sort(buckSort(buckets)) |
| } |
| his.Bucket = buckets |
| |
| out.Histogram = his |
| out.Label = h.labelPairs |
| |
| return nil |
| } |
| |
| // NewConstHistogram returns a metric representing a Prometheus histogram with |
| // fixed values for the count, sum, and bucket counts. As those parameters |
| // cannot be changed, the returned value does not implement the Histogram |
| // interface (but only the Metric interface). Users of this package will not |
| // have much use for it in regular operations. However, when implementing custom |
| // Collectors, it is useful as a throw-away metric that is generated on the fly |
| // to send it to Prometheus in the Collect method. |
| // |
| // buckets is a map of upper bounds to cumulative counts, excluding the +Inf |
| // bucket. |
| // |
| // NewConstHistogram returns an error if the length of labelValues is not |
| // consistent with the variable labels in Desc. |
| func NewConstHistogram( |
| desc *Desc, |
| count uint64, |
| sum float64, |
| buckets map[float64]uint64, |
| labelValues ...string, |
| ) (Metric, error) { |
| if len(desc.variableLabels) != len(labelValues) { |
| return nil, errInconsistentCardinality |
| } |
| return &constHistogram{ |
| desc: desc, |
| count: count, |
| sum: sum, |
| buckets: buckets, |
| labelPairs: makeLabelPairs(desc, labelValues), |
| }, nil |
| } |
| |
| // MustNewConstHistogram is a version of NewConstHistogram that panics where |
| // NewConstMetric would have returned an error. |
| func MustNewConstHistogram( |
| desc *Desc, |
| count uint64, |
| sum float64, |
| buckets map[float64]uint64, |
| labelValues ...string, |
| ) Metric { |
| m, err := NewConstHistogram(desc, count, sum, buckets, labelValues...) |
| if err != nil { |
| panic(err) |
| } |
| return m |
| } |
| |
| type buckSort []*dto.Bucket |
| |
| func (s buckSort) Len() int { |
| return len(s) |
| } |
| |
| func (s buckSort) Swap(i, j int) { |
| s[i], s[j] = s[j], s[i] |
| } |
| |
| func (s buckSort) Less(i, j int) bool { |
| return s[i].GetUpperBound() < s[j].GetUpperBound() |
| } |