| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package oldmetrix |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "slices" |
| 8 | "sort" |
| 9 | |
| 10 | "github.com/netdata/netdata/go/plugins/pkg/stm" |
| 11 | ) |
| 12 | |
| 13 | type ( |
| 14 | // A Histogram counts individual observations from an event or sample stream in |
| 15 | // configurable buckets. Similar to a summary, it also provides a sum of |
| 16 | // observations and an observation count. |
| 17 | // |
| 18 | // Note that Histograms, in contrast to Summaries, can be aggregated. |
| 19 | // However, Histograms require the user to pre-define suitable |
| 20 | // buckets, and they are in general less accurate. The Observe method of a |
| 21 | // histogram has a very low performance overhead in comparison with the Observe |
| 22 | // method of a summary. |
| 23 | // |
| 24 | // To create histogram instances, use NewHistogram. |
| 25 | Histogram interface { |
| 26 | Observer |
| 27 | } |
| 28 | |
| 29 | histogram struct { |
| 30 | buckets []int64 |
| 31 | upperBounds []float64 |
| 32 | sum float64 |
| 33 | count int64 |
| 34 | rangeBuckets bool |
| 35 | } |
| 36 | ) |
| 37 | |
| 38 | var ( |
| 39 | _ stm.Value = histogram{} |
| 40 | ) |
| 41 | |
| 42 | // DefBuckets are the default histogram buckets. The default buckets are |
| 43 | // tailored to broadly measure the response time (in seconds) of a network |
| 44 | // service. Most likely, however, you will be required to define buckets |
| 45 | // customized to your use case. |
| 46 | var DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} |
| 47 | |
| 48 | // LinearBuckets creates 'count' buckets, each 'width' wide, where the lowest |
| 49 | // bucket has an upper bound of 'start'. The final +Inf bucket is not counted |
| 50 | // and not included in the returned slice. The returned slice is meant to be |
| 51 | // used for the Buckets field of HistogramOpts. |
| 52 | // |
| 53 | // The function panics if 'count' is zero or negative. |
| 54 | func LinearBuckets(start, width float64, count int) []float64 { |
| 55 | if count < 1 { |
| 56 | panic("LinearBuckets needs a positive count") |
| 57 | } |
| 58 | buckets := make([]float64, count) |
| 59 | for i := range buckets { |
| 60 | buckets[i] = start |
| 61 | start += width |
| 62 | } |
| 63 | return buckets |
| 64 | } |
| 65 | |
| 66 | // ExponentialBuckets creates 'count' buckets, where the lowest bucket has an |
| 67 | // upper bound of 'start' and each following bucket's upper bound is 'factor' |
| 68 | // times the previous bucket's upper bound. The final +Inf bucket is not counted |
| 69 | // and not included in the returned slice. The returned slice is meant to be |
| 70 | // used for the Buckets field of HistogramOpts. |
| 71 | // |
| 72 | // The function panics if 'count' is 0 or negative, if 'start' is 0 or negative, |
| 73 | // or if 'factor' is less than or equal 1. |
| 74 | func ExponentialBuckets(start, factor float64, count int) []float64 { |
| 75 | if count < 1 { |
| 76 | panic("ExponentialBuckets needs a positive count") |
| 77 | } |
| 78 | if start <= 0 { |
| 79 | panic("ExponentialBuckets needs a positive start value") |
| 80 | } |
| 81 | if factor <= 1 { |
| 82 | panic("ExponentialBuckets needs a factor greater than 1") |
| 83 | } |
| 84 | buckets := make([]float64, count) |
| 85 | for i := range buckets { |
| 86 | buckets[i] = start |
| 87 | start *= factor |
| 88 | } |
| 89 | return buckets |
| 90 | } |
| 91 | |
| 92 | // NewHistogram creates a new Histogram. |
| 93 | func NewHistogram(buckets []float64) Histogram { |
| 94 | if len(buckets) == 0 { |
| 95 | buckets = DefBuckets |
| 96 | } else { |
| 97 | slices.Sort(buckets) |
| 98 | } |
| 99 | |
| 100 | return &histogram{ |
| 101 | buckets: make([]int64, len(buckets)), |
| 102 | upperBounds: buckets, |
| 103 | count: 0, |
| 104 | sum: 0, |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | func NewHistogramWithRangeBuckets(buckets []float64) Histogram { |
| 109 | if len(buckets) == 0 { |
| 110 | buckets = DefBuckets |
| 111 | } else { |
| 112 | slices.Sort(buckets) |
| 113 | } |
| 114 | |
| 115 | return &histogram{ |
| 116 | buckets: make([]int64, len(buckets)), |
| 117 | upperBounds: buckets, |
| 118 | count: 0, |
| 119 | sum: 0, |
| 120 | rangeBuckets: true, |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | // WriteTo writes its values into given map. |
| 125 | // It adds those key-value pairs: |
| 126 | // |
| 127 | // ${key}_sum gauge, for sum of it's observed values |
| 128 | // ${key}_count counter, for count of it's observed values (equals to +Inf bucket) |
| 129 | // ${key}_bucket_1 counter, for 1st bucket count |
| 130 | // ${key}_bucket_2 counter, for 2nd bucket count |
| 131 | // ... |
| 132 | // ${key}_bucket_N counter, for Nth bucket count |
| 133 | func (h histogram) WriteTo(rv map[string]int64, key string, mul, div int) { |
| 134 | rv[key+"_sum"] = int64(h.sum * float64(mul) / float64(div)) |
| 135 | rv[key+"_count"] = h.count |
| 136 | var conn int64 |
| 137 | for i, bucket := range h.buckets { |
| 138 | name := fmt.Sprintf("%s_bucket_%d", key, i+1) |
| 139 | conn += bucket |
| 140 | if h.rangeBuckets { |
| 141 | rv[name] = bucket |
| 142 | } else { |
| 143 | rv[name] = conn |
| 144 | } |
| 145 | } |
| 146 | if h.rangeBuckets { |
| 147 | name := fmt.Sprintf("%s_bucket_inf", key) |
| 148 | rv[name] = h.count - conn |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | // Observe observes a value |
| 153 | func (h *histogram) Observe(v float64) { |
| 154 | hotIdx := h.searchBucketIndex(v) |
| 155 | if hotIdx < len(h.buckets) { |
| 156 | h.buckets[hotIdx]++ |
| 157 | } |
| 158 | h.sum += v |
| 159 | h.count++ |
| 160 | } |
| 161 | |
| 162 | func (h *histogram) searchBucketIndex(v float64) int { |
| 163 | if len(h.upperBounds) < 30 { |
| 164 | for i, upper := range h.upperBounds { |
| 165 | if upper >= v { |
| 166 | return i |
| 167 | } |
| 168 | } |
| 169 | return len(h.upperBounds) |
| 170 | } |
| 171 | return sort.SearchFloat64s(h.upperBounds, v) |
| 172 | } |