metrics/histogram.go

327 lines
8.4 KiB
Go
Raw Permalink Normal View History

2019-11-23 01:16:17 +03:00
package metrics
import (
"fmt"
"io"
"math"
2023-02-07 09:27:40 +03:00
"strings"
2019-11-23 01:16:17 +03:00
"sync"
"time"
)
const (
e10Min = -9
e10Max = 18
bucketsPerDecimal = 18
decimalBucketsCount = e10Max - e10Min
bucketsCount = decimalBucketsCount * bucketsPerDecimal
)
var bucketMultiplier = math.Pow(10, 1.0/bucketsPerDecimal)
// Histogram is a histogram for non-negative values with automatically created buckets.
2019-11-23 01:16:17 +03:00
//
// See https://medium.com/@valyala/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350
//
2019-11-23 01:16:17 +03:00
// Each bucket contains a counter for values in the given range.
// Each non-empty bucket is exposed via the following metric:
2019-11-23 01:16:17 +03:00
//
2022-08-08 17:15:06 +03:00
// <metric_name>_bucket{<optional_tags>,vmrange="<start>...<end>"} <counter>
2019-11-23 01:16:17 +03:00
//
// Where:
//
2022-08-08 17:15:06 +03:00
// - <metric_name> is the metric name passed to NewHistogram
// - <optional_tags> is optional tags for the <metric_name>, which are passed to NewHistogram
// - <start> and <end> - start and end values for the given bucket
// - <counter> - the number of hits to the given bucket during Update* calls
2019-11-23 01:16:17 +03:00
//
2019-11-23 14:04:24 +03:00
// Histogram buckets can be converted to Prometheus-like buckets with `le` labels
// with `prometheus_buckets(<metric_name>_bucket)` function from PromQL extensions in VictoriaMetrics.
// (see https://docs.victoriametrics.com/metricsql/ ):
2019-11-23 01:16:17 +03:00
//
2022-08-08 17:15:06 +03:00
// prometheus_buckets(request_duration_bucket)
2019-11-23 01:16:17 +03:00
//
2019-11-23 14:04:24 +03:00
// Time series produced by the Histogram have better compression ratio comparing to
// Prometheus histogram buckets with `le` labels, since they don't include counters
// for all the previous buckets.
//
// Zero histogram is usable.
2019-11-23 01:16:17 +03:00
type Histogram struct {
// Mu gurantees synchronous update for all the counters and sum.
//
// Do not use sync.RWMutex, since it has zero sense from performance PoV.
// It only complicates the code.
mu sync.Mutex
// decimalBuckets contains counters for histogram buckets
decimalBuckets [decimalBucketsCount]*[bucketsPerDecimal]uint64
// lower is the number of values, which hit the lower bucket
lower uint64
// upper is the number of values, which hit the upper bucket
upper uint64
// sum is the sum of all the values put into Histogram
sum float64
2023-02-07 09:27:40 +03:00
compatible bool
}
// Reset resets the given histogram.
func (h *Histogram) Reset() {
h.mu.Lock()
for _, db := range h.decimalBuckets[:] {
if db == nil {
continue
}
for i := range db[:] {
db[i] = 0
}
}
h.lower = 0
h.upper = 0
h.sum = 0
2021-03-16 13:14:36 +03:00
h.mu.Unlock()
}
func (h *Histogram) GetSum() float64 {
return h.sum
}
func (h *Histogram) GetDecimalBuckets() [decimalBucketsCount]*[bucketsPerDecimal]uint64 {
return h.decimalBuckets
}
// Update updates h with v.
//
// Negative values and NaNs are ignored.
func (h *Histogram) Update(v float64) {
if math.IsNaN(v) || v < 0 {
// Skip NaNs and negative values.
return
}
bucketIdx := (math.Log10(v) - e10Min) * bucketsPerDecimal
h.mu.Lock()
h.sum += v
if bucketIdx < 0 {
h.lower++
} else if bucketIdx >= bucketsCount {
h.upper++
} else {
idx := uint(bucketIdx)
if bucketIdx == float64(idx) && idx > 0 {
// Edge case for 10^n values, which must go to the lower bucket
// according to Prometheus logic for `le`-based histograms.
idx--
}
decimalBucketIdx := idx / bucketsPerDecimal
offset := idx % bucketsPerDecimal
db := h.decimalBuckets[decimalBucketIdx]
if db == nil {
var b [bucketsPerDecimal]uint64
db = &b
h.decimalBuckets[decimalBucketIdx] = db
}
db[offset]++
}
h.mu.Unlock()
}
// Merge merges src to h
func (h *Histogram) Merge(src *Histogram) {
2024-05-19 11:06:13 +03:00
h.mu.Lock()
defer h.mu.Unlock()
src.mu.Lock()
defer src.mu.Unlock()
2024-05-19 11:06:13 +03:00
h.lower += src.lower
h.upper += src.upper
h.sum += src.sum
2024-05-19 11:06:13 +03:00
for i, dbSrc := range src.decimalBuckets {
if dbSrc == nil {
2024-05-19 11:06:13 +03:00
continue
}
dbDst := h.decimalBuckets[i]
if dbDst == nil {
var b [bucketsPerDecimal]uint64
dbDst = &b
h.decimalBuckets[i] = dbDst
}
for j := range dbSrc {
dbDst[j] += dbSrc[j]
2024-05-19 11:06:13 +03:00
}
}
}
// VisitNonZeroBuckets calls f for all buckets with non-zero counters.
//
// vmrange contains "<start>...<end>" string with bucket bounds. The lower bound
// isn't included in the bucket, while the upper bound is included.
// This is required to be compatible with Prometheus-style histogram buckets
// with `le` (less or equal) labels.
func (h *Histogram) VisitNonZeroBuckets(f func(vmrange string, count uint64)) {
h.mu.Lock()
if h.lower > 0 {
f(lowerBucketRange, h.lower)
}
for decimalBucketIdx, db := range h.decimalBuckets[:] {
if db == nil {
continue
}
for offset, count := range db[:] {
if count > 0 {
bucketIdx := decimalBucketIdx*bucketsPerDecimal + offset
vmrange := getVMRange(bucketIdx)
f(vmrange, count)
}
}
}
if h.upper > 0 {
f(upperBucketRange, h.upper)
}
h.mu.Unlock()
2019-11-23 01:16:17 +03:00
}
// NewHistogram creates and returns new histogram with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
2022-08-08 17:15:06 +03:00
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
2019-11-23 01:16:17 +03:00
//
// The returned histogram is safe to use from concurrent goroutines.
func NewHistogram(name string) *Histogram {
return defaultSet.NewHistogram(name)
}
2023-02-07 10:38:08 +03:00
func NewCompatibleHistogram(name string) *Histogram {
2023-02-20 07:54:52 +03:00
return defaultSet.NewCompatibleHistogram(name)
2023-02-07 10:38:08 +03:00
}
2019-11-23 01:16:17 +03:00
// GetOrCreateHistogram returns registered histogram with the given name
// or creates new histogram if the registry doesn't contain histogram with
// the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
2022-08-08 17:15:06 +03:00
// - foo
// - foo{bar="baz"}
// - foo{bar="baz",aaa="b"}
2019-11-23 01:16:17 +03:00
//
// The returned histogram is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewHistogram instead of GetOrCreateHistogram.
func GetOrCreateHistogram(name string) *Histogram {
return defaultSet.GetOrCreateHistogram(name)
}
2023-02-07 10:38:08 +03:00
func GetOrCreateCompatibleHistogram(name string) *Histogram {
2023-02-20 07:54:52 +03:00
return defaultSet.GetOrCreateCompatibleHistogram(name)
2023-02-07 10:38:08 +03:00
}
2019-11-23 01:16:17 +03:00
// UpdateDuration updates request duration based on the given startTime.
func (h *Histogram) UpdateDuration(startTime time.Time) {
d := time.Since(startTime).Seconds()
h.Update(d)
}
func getVMRange(bucketIdx int) string {
bucketRangesOnce.Do(initBucketRanges)
return bucketRanges[bucketIdx]
2019-11-24 00:58:18 +03:00
}
func initBucketRanges() {
v := math.Pow10(e10Min)
start := fmt.Sprintf("%.3e", v)
for i := 0; i < bucketsCount; i++ {
v *= bucketMultiplier
end := fmt.Sprintf("%.3e", v)
bucketRanges[i] = start + "..." + end
start = end
2019-11-24 00:58:18 +03:00
}
}
var (
lowerBucketRange = fmt.Sprintf("0...%.3e", math.Pow10(e10Min))
upperBucketRange = fmt.Sprintf("%.3e...+Inf", math.Pow10(e10Max))
bucketRanges [bucketsCount]string
2019-11-24 00:58:18 +03:00
bucketRangesOnce sync.Once
)
2019-11-23 01:16:17 +03:00
func (h *Histogram) marshalTo(prefix string, w io.Writer) {
2023-02-07 09:27:40 +03:00
if h.compatible {
h.marshalToPrometheus(prefix, w)
return
}
countTotal := uint64(0)
h.VisitNonZeroBuckets(func(vmrange string, count uint64) {
tag := fmt.Sprintf("vmrange=%q", vmrange)
metricName := addTag(prefix, tag)
name, labels := splitMetricName(metricName)
fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, count)
countTotal += count
})
if countTotal == 0 {
2019-11-23 01:16:17 +03:00
return
}
name, labels := splitMetricName(prefix)
sum := h.getSum()
if float64(int64(sum)) == sum {
fmt.Fprintf(w, "%s_sum%s %d\n", name, labels, int64(sum))
2019-11-23 01:16:17 +03:00
} else {
fmt.Fprintf(w, "%s_sum%s %g\n", name, labels, sum)
2019-11-23 01:16:17 +03:00
}
fmt.Fprintf(w, "%s_count%s %d\n", name, labels, countTotal)
2019-11-23 01:16:17 +03:00
}
2023-02-07 09:27:40 +03:00
func (h *Histogram) marshalToPrometheus(prefix string, w io.Writer) {
countTotal := uint64(0)
2023-02-07 10:00:56 +03:00
inf := false
2023-02-07 09:27:40 +03:00
h.VisitNonZeroBuckets(func(vmrange string, count uint64) {
v := strings.Split(vmrange, "...")
if len(v) != 2 {
return
}
2023-02-07 10:00:56 +03:00
if v[1] == "+Inf" {
inf = true
}
2023-02-07 09:27:40 +03:00
tag := fmt.Sprintf("le=%q", v[1])
metricName := addTag(prefix, tag)
name, labels := splitMetricName(metricName)
countTotal += count
fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, countTotal)
})
if countTotal == 0 {
return
}
2023-02-07 10:00:56 +03:00
if !inf {
tag := fmt.Sprintf("le=%q", "+Inf")
metricName := addTag(prefix, tag)
name, labels := splitMetricName(metricName)
fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, countTotal)
}
2023-02-07 09:27:40 +03:00
name, labels := splitMetricName(prefix)
sum := h.getSum()
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, countTotal)
}
2019-11-23 01:16:17 +03:00
func (h *Histogram) getSum() float64 {
h.mu.Lock()
sum := h.sum
h.mu.Unlock()
return sum
}
func (h *Histogram) metricType() string {
return "histogram"
}