package metrics import ( "fmt" "io" "math" "sync" "time" ) // PrometheusHistogramDefaultBuckets is a list of the default bucket upper // bounds. Those default buckets are quite generic, and it is recommended to // pick custom buckets for improved accuracy. var PrometheusHistogramDefaultBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} // PrometheusHistogram is a histogram for non-negative values with pre-defined buckets // // Each bucket contains a counter for values in the given range. // Each bucket is exposed via the following metric: // // _bucket{,le="upper_bound"} // // Where: // // - is the metric name passed to NewPrometheusHistogram // - is optional tags for the , which are passed to NewPrometheusHistogram // - - upper bound of the current bucket. all samples <= upper_bound are in that bucket // - - the number of hits to the given bucket during Update* calls // // Next to the bucket metrics, two additional metrics track the total number of // samples (_count) and the total sum (_sum) of all samples: // // - _sum{} // - _count{} type PrometheusHistogram struct { // mu guarantees synchronous update for all the counters. // // Do not use sync.RWMutex, since it has zero sense from performance PoV. // It only complicates the code. mu sync.Mutex // upperBounds and buckets are aligned by element position: // upperBounds[i] defines the upper bound for buckets[i]. // buckets[i] contains the count of elements <= upperBounds[i] upperBounds []float64 buckets []uint64 // count is the counter for all observations on this histogram count uint64 // sum is the sum of all the values put into Histogram sum float64 } // Reset resets previous observations in h. func (h *PrometheusHistogram) Reset() { h.mu.Lock() for i := range h.buckets { h.buckets[i] = 0 } h.sum = 0 h.count = 0 h.mu.Unlock() } // Update updates h with v. // // Negative values and NaNs are ignored. func (h *PrometheusHistogram) Update(v float64) { if math.IsNaN(v) || v < 0 { // Skip NaNs and negative values. return } bucketIdx := -1 for i, ub := range h.upperBounds { if v <= ub { bucketIdx = i break } } h.mu.Lock() h.sum += v h.count++ if bucketIdx == -1 { // +Inf, nothing to do, already accounted for in the total sum h.mu.Unlock() return } h.buckets[bucketIdx]++ h.mu.Unlock() } // UpdateDuration updates request duration based on the given startTime. func (h *PrometheusHistogram) UpdateDuration(startTime time.Time) { d := time.Since(startTime).Seconds() h.Update(d) } // NewPrometheusHistogram creates and returns new PrometheusHistogram with the given name // and PrometheusHistogramDefaultBuckets. // // name must be valid Prometheus-compatible metric with possible labels. // For instance, // // - foo // - foo{bar="baz"} // - foo{bar="baz",aaa="b"} // // The returned histogram is safe to use from concurrent goroutines. func NewPrometheusHistogram(name string) *PrometheusHistogram { return defaultSet.NewPrometheusHistogram(name) } // NewPrometheusHistogramExt creates and returns new PrometheusHistogram with the given name // and given upperBounds. // // name must be valid Prometheus-compatible metric with possible labels. // For instance, // // - foo // - foo{bar="baz"} // - foo{bar="baz",aaa="b"} // // The returned histogram is safe to use from concurrent goroutines. func NewPrometheusHistogramExt(name string, upperBounds []float64) *PrometheusHistogram { return defaultSet.NewPrometheusHistogramExt(name, upperBounds) } // GetOrCreatePrometheusHistogram returns registered PrometheusHistogram with the given name // or creates a new PrometheusHistogram if the registry doesn't contain histogram with // the given name. // // name must be valid Prometheus-compatible metric with possible labels. // For instance, // // - foo // - foo{bar="baz"} // - foo{bar="baz",aaa="b"} // // The returned histogram is safe to use from concurrent goroutines. // // Performance tip: prefer NewPrometheusHistogram instead of GetOrCreatePrometheusHistogram. func GetOrCreatePrometheusHistogram(name string) *PrometheusHistogram { return defaultSet.GetOrCreatePrometheusHistogram(name) } // GetOrCreatePrometheusHistogramExt returns registered PrometheusHistogram with the given name and // upperBounds or creates new PrometheusHistogram if the registry doesn't contain histogram // with the given name. // // name must be valid Prometheus-compatible metric with possible labels. // For instance, // // - foo // - foo{bar="baz"} // - foo{bar="baz",aaa="b"} // // The returned histogram is safe to use from concurrent goroutines. // // Performance tip: prefer NewPrometheusHistogramExt instead of GetOrCreatePrometheusHistogramExt. func GetOrCreatePrometheusHistogramExt(name string, upperBounds []float64) *PrometheusHistogram { return defaultSet.GetOrCreatePrometheusHistogramExt(name, upperBounds) } func newPrometheusHistogram(upperBounds []float64) *PrometheusHistogram { mustValidateBuckets(upperBounds) last := len(upperBounds) - 1 if math.IsInf(upperBounds[last], +1) { upperBounds = upperBounds[:last] // ignore +Inf bucket as it is covered anyways } h := PrometheusHistogram{ upperBounds: upperBounds, buckets: make([]uint64, len(upperBounds)), } return &h } func mustValidateBuckets(upperBounds []float64) { if err := ValidateBuckets(upperBounds); err != nil { panic(err) } } // ValidateBuckets validates the given upperBounds and returns an error // if validation failed. func ValidateBuckets(upperBounds []float64) error { if len(upperBounds) == 0 { return fmt.Errorf("upperBounds can't be empty") } for i := 0; i < len(upperBounds)-1; i++ { if upperBounds[i] >= upperBounds[i+1] { return fmt.Errorf("upper bounds for the buckets must be strictly increasing") } } return nil } // LinearBuckets returns a list of upperBounds for PrometheusHistogram, // and whose distribution is as follows: // // [start, start + width, start + 2 * width, ... start + (count-1) * width] // // Panics if given start, width and count produce negative buckets or none buckets at all. func LinearBuckets(start, width float64, count int) []float64 { if count < 1 { panic("LinearBuckets: count can't be less than 1") } upperBounds := make([]float64, count) for i := range upperBounds { upperBounds[i] = start start += width } mustValidateBuckets(upperBounds) return upperBounds } // ExponentialBuckets returns a list of upperBounds for PrometheusHistogram, // and whose distribution is as follows: // // [start, start * factor pow 1, start * factor pow 2, ... start * factor pow (count-1)] // // Panics if given start, width and count produce negative buckets or none buckets at all. func ExponentialBuckets(start, factor float64, count int) []float64 { if count < 1 { panic("ExponentialBuckets: count can't be less than 1") } if factor <= 1 { panic("ExponentialBuckets: factor must be greater than 1") } if start <= 0 { panic("ExponentialBuckets: start can't be less than 0") } upperBounds := make([]float64, count) for i := range upperBounds { upperBounds[i] = start start *= factor } mustValidateBuckets(upperBounds) return upperBounds } func (h *PrometheusHistogram) marshalTo(prefix string, w io.Writer) { cumulativeSum := uint64(0) h.mu.Lock() count := h.count sum := h.sum for i, ub := range h.upperBounds { cumulativeSum += h.buckets[i] tag := fmt.Sprintf(`le="%v"`, ub) metricName := addTag(prefix, tag) name, labels := splitMetricName(metricName) fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, cumulativeSum) } h.mu.Unlock() tag := fmt.Sprintf("le=%q", "+Inf") metricName := addTag(prefix, tag) name, labels := splitMetricName(metricName) fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, count) name, labels = splitMetricName(prefix) if float64(int64(sum)) == sum { fmt.Fprintf(w, "%s_sum%s %d\n", name, labels, int64(sum)) } else { fmt.Fprintf(w, "%s_sum%s %g\n", name, labels, sum) } fmt.Fprintf(w, "%s_count%s %d\n", name, labels, count) } func (h *PrometheusHistogram) metricType() string { return "histogram" }