All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 50s
274 lines
8.1 KiB
Go
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"
|
|
}
|