summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/pkg/metrics/histogram.go
blob: caabf09afeaa9c66f6e4c888be1ccb1fc3b4d3c5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// SPDX-License-Identifier: GPL-3.0-or-later

package metrics

import (
	"fmt"
	"sort"

	"github.com/netdata/netdata/go/go.d.plugin/pkg/stm"
)

type (
	// 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.
	//
	// Note that Histograms, in contrast to Summaries, can be aggregated.
	// 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.
	Histogram interface {
		Observer
	}

	histogram struct {
		buckets      []int64
		upperBounds  []float64
		sum          float64
		count        int64
		rangeBuckets bool
	}
)

var (
	_ stm.Value = histogram{}
)

// 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}

// 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
}

// NewHistogram creates a new Histogram.
func NewHistogram(buckets []float64) Histogram {
	if len(buckets) == 0 {
		buckets = DefBuckets
	} else {
		sort.Slice(buckets, func(i, j int) bool { return buckets[i] < buckets[j] })
	}

	return &histogram{
		buckets:     make([]int64, len(buckets)),
		upperBounds: buckets,
		count:       0,
		sum:         0,
	}
}

func NewHistogramWithRangeBuckets(buckets []float64) Histogram {
	if len(buckets) == 0 {
		buckets = DefBuckets
	} else {
		sort.Slice(buckets, func(i, j int) bool { return buckets[i] < buckets[j] })
	}

	return &histogram{
		buckets:      make([]int64, len(buckets)),
		upperBounds:  buckets,
		count:        0,
		sum:          0,
		rangeBuckets: true,
	}
}

// WriteTo writes its values into given map.
// It adds those key-value pairs:
//
//	${key}_sum        gauge, for sum of it's observed values
//	${key}_count      counter, for count of it's observed values (equals to +Inf bucket)
//	${key}_bucket_1   counter, for 1st bucket count
//	${key}_bucket_2   counter, for 2nd bucket count
//	...
//	${key}_bucket_N   counter, for Nth bucket count
func (h histogram) WriteTo(rv map[string]int64, key string, mul, div int) {
	rv[key+"_sum"] = int64(h.sum * float64(mul) / float64(div))
	rv[key+"_count"] = h.count
	var conn int64
	for i, bucket := range h.buckets {
		name := fmt.Sprintf("%s_bucket_%d", key, i+1)
		conn += bucket
		if h.rangeBuckets {
			rv[name] = bucket
		} else {
			rv[name] = conn
		}
	}
	if h.rangeBuckets {
		name := fmt.Sprintf("%s_bucket_inf", key)
		rv[name] = h.count - conn
	}
}

// Observe observes a value
func (h *histogram) Observe(v float64) {
	hotIdx := h.searchBucketIndex(v)
	if hotIdx < len(h.buckets) {
		h.buckets[hotIdx]++
	}
	h.sum += v
	h.count++
}

func (h *histogram) searchBucketIndex(v float64) int {
	if len(h.upperBounds) < 30 {
		for i, upper := range h.upperBounds {
			if upper >= v {
				return i
			}
		}
		return len(h.upperBounds)
	}
	return sort.SearchFloat64s(h.upperBounds, v)
}