| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- // 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()
- }
|