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