Files
test-actions/vendor/github.com/VictoriaMetrics/metrics/prometheus_histogram.go
Alex Matsuev 9438cc0129
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 50s
Add vendoring
2025-11-21 23:05:32 +03:00

274 lines
8.1 KiB
Go

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:
//
// <metric_name>_bucket{<optional_tags>,le="upper_bound"} <counter>
//
// Where:
//
// - <metric_name> is the metric name passed to NewPrometheusHistogram
// - <optional_tags> is optional tags for the <metric_name>, which are passed to NewPrometheusHistogram
// - <upper_bound> - upper bound of the current bucket. all samples <= upper_bound are in that bucket
// - <counter> - 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:
//
// - <metric_name>_sum{<optional_tags>} <counter>
// - <metric_name>_count{<optional_tags>} <counter>
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"
}