github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/monitor/stats/histogram.go (about) 1 /* 2 * 3 * Copyright 2017 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 // Copyright (c) 2015 Arista Networks, Inc. 20 // Use of this source code is governed by the Apache License 2.0 21 // that can be found in the COPYING file. 22 23 package stats 24 25 import ( 26 "bytes" 27 "encoding/json" 28 "fmt" 29 "io" 30 "strconv" 31 "strings" 32 "time" 33 ) 34 35 // HistogramValue is the value of Histogram objects. 36 type HistogramValue struct { 37 // Count is the total number of values added to the histogram. 38 Count int64 39 // Sum is the sum of all the values added to the histogram. 40 Sum int64 41 // Min is the minimum of all the values added to the histogram. 42 Min int64 43 // Max is the maximum of all the values added to the histogram. 44 Max int64 45 // Buckets contains all the buckets of the histogram. 46 Buckets []HistogramBucket 47 } 48 49 // HistogramBucket is one histogram bucket. 50 type HistogramBucket struct { 51 // LowBound is the lower bound of the bucket. 52 LowBound int64 53 // Count is the number of values in the bucket. 54 Count int64 55 } 56 57 // PrintChart writes textual output of the histogram values. 58 func (v HistogramValue) PrintChart(w io.Writer) { 59 avg := float64(v.Sum) / float64(v.Count) 60 fmt.Fprintf(w, "Count: %d Min: %d Max: %d Avg: %.2f\n", v.Count, v.Min, v.Max, avg) 61 fmt.Fprintf(w, "%s\n", strings.Repeat("-", 60)) 62 if v.Count <= 0 { 63 return 64 } 65 66 maxBucketDigitLen := len(strconv.FormatInt(v.Buckets[len(v.Buckets)-1].LowBound, 10)) 67 if maxBucketDigitLen < 3 { 68 // For "inf". 69 maxBucketDigitLen = 3 70 } 71 maxCountDigitLen := len(strconv.FormatInt(v.Count, 10)) 72 percentMulti := 100 / float64(v.Count) 73 74 accCount := int64(0) 75 for i, b := range v.Buckets { 76 fmt.Fprintf(w, "[%*d, ", maxBucketDigitLen, b.LowBound) 77 if i+1 < len(v.Buckets) { 78 fmt.Fprintf(w, "%*d)", maxBucketDigitLen, v.Buckets[i+1].LowBound) 79 } else { 80 fmt.Fprintf(w, "%*s)", maxBucketDigitLen, "inf") 81 } 82 83 accCount += b.Count 84 fmt.Fprintf(w, " %*d %5.1f%% %5.1f%%", maxCountDigitLen, b.Count, 85 float64(b.Count)*percentMulti, float64(accCount)*percentMulti) 86 87 const barScale = 0.1 88 barLength := int(float64(b.Count)*percentMulti*barScale + 0.5) 89 fmt.Fprintf(w, " %s\n", strings.Repeat("#", barLength)) 90 } 91 } 92 93 // MarshalJSON marshal the HistogramValue into JSON. 94 func (v HistogramValue) MarshalJSON() ([]byte, error) { 95 var b bytes.Buffer 96 var min int64 97 var max int64 98 var avg float64 99 var percentMulti float64 100 if v.Count != 0 { 101 min = v.Min 102 max = v.Max 103 avg = float64(v.Sum) / float64(v.Count) 104 percentMulti = 100 / float64(v.Count) 105 } 106 fmt.Fprintf(&b, 107 `{"stats":{"count":%d,"min":%d,"max":%d,"avg":%.2f}, "buckets": [`, 108 v.Count, min, max, avg) 109 110 for i, bucket := range v.Buckets { 111 fmt.Fprintf(&b, `{"range":"[%d,`, bucket.LowBound) 112 if i+1 < len(v.Buckets) { 113 fmt.Fprintf(&b, `%d)",`, v.Buckets[i+1].LowBound) 114 } else { 115 fmt.Fprintf(&b, `inf)",`) 116 } 117 118 fmt.Fprintf(&b, `"count":%d,"percentage":%.1f}`, 119 bucket.Count, float64(bucket.Count)*percentMulti) 120 if i != len(v.Buckets)-1 { 121 fmt.Fprintf(&b, ",") 122 } 123 } 124 fmt.Fprint(&b, `]}`) 125 return b.Bytes(), nil 126 } 127 128 // String returns the textual output of the histogram values as string. 129 func (v HistogramValue) String() string { 130 var b strings.Builder 131 v.PrintChart(&b) 132 return b.String() 133 } 134 135 // A Histogram accumulates values in the form of a histogram. The type of the 136 // values is int64, which is suitable for keeping track of things like RPC 137 // latency in milliseconds. New histogram objects should be obtained via the 138 // New() function. 139 type Histogram struct { 140 opts HistogramOptions 141 buckets []bucketInternal 142 count *Counter 143 sum *Counter 144 tracker *Tracker 145 } 146 147 // HistogramOptions contains the parameters that define the histogram's buckets. 148 type HistogramOptions struct { 149 // NumBuckets is the number of buckets. 150 NumBuckets int 151 // GrowthFactor is the growth factor of the buckets. A value of 0.1 152 // indicates that bucket N+1 will be 10% larger than bucket N. 153 GrowthFactor float64 154 // SmallestBucketSize is the size of the first bucket. Bucket sizes are 155 // rounded down to the nearest integer. 156 SmallestBucketSize float64 157 // MinValue is the lower bound of the first bucket. 158 MinValue int64 159 } 160 161 // bucketInternal is the internal representation of a bucket, which includes a 162 // rate counter. 163 type bucketInternal struct { 164 lowBound int64 165 count *Counter 166 } 167 168 // NewHistogram returns a pointer to a new Histogram object that was created 169 // with the provided options. 170 func NewHistogram(opts HistogramOptions) *Histogram { 171 if opts.NumBuckets == 0 { 172 opts.NumBuckets = 32 173 } 174 if opts.SmallestBucketSize == 0.0 { 175 opts.SmallestBucketSize = 1.0 176 } 177 h := Histogram{ 178 opts: opts, 179 buckets: make([]bucketInternal, opts.NumBuckets), 180 count: newCounter(), 181 sum: newCounter(), 182 tracker: newTracker(), 183 } 184 low := opts.MinValue 185 delta := opts.SmallestBucketSize 186 for i := 0; i < opts.NumBuckets; i++ { 187 h.buckets[i].lowBound = low 188 h.buckets[i].count = newCounter() 189 low = low + int64(delta) 190 delta = delta * (1.0 + opts.GrowthFactor) 191 } 192 return &h 193 } 194 195 // Opts returns a copy of the options used to create the Histogram. 196 func (h *Histogram) Opts() HistogramOptions { 197 return h.opts 198 } 199 200 // Print returns the histogram as a chart. 201 func (h *Histogram) Print() string { 202 return h.Value().String() 203 } 204 205 // String returns a JSON representation of the histogram for expvars. 206 func (h *Histogram) String() string { 207 j, err := json.Marshal(h.Value()) 208 if err != nil { 209 return "" 210 } 211 return string(j) 212 } 213 214 // Add adds a value to the histogram. 215 func (h *Histogram) Add(value int64) error { 216 bucket, err := h.findBucket(value) 217 if err != nil { 218 return err 219 } 220 h.buckets[bucket].count.Incr(1) 221 h.count.Incr(1) 222 h.sum.Incr(value) 223 h.tracker.Push(value) 224 return nil 225 } 226 227 // LastUpdate returns the time at which the object was last updated. 228 func (h *Histogram) LastUpdate() time.Time { 229 return h.count.LastUpdate() 230 } 231 232 // Value returns the accumulated state of the histogram since it was created. 233 func (h *Histogram) Value() HistogramValue { 234 b := make([]HistogramBucket, len(h.buckets)) 235 for i, v := range h.buckets { 236 b[i] = HistogramBucket{ 237 LowBound: v.lowBound, 238 Count: v.count.Value(), 239 } 240 } 241 242 v := HistogramValue{ 243 Count: h.count.Value(), 244 Sum: h.sum.Value(), 245 Min: h.tracker.Min(), 246 Max: h.tracker.Max(), 247 Buckets: b, 248 } 249 return v 250 } 251 252 // Delta1h returns the change in the last hour. 253 func (h *Histogram) Delta1h() HistogramValue { 254 b := make([]HistogramBucket, len(h.buckets)) 255 for i, v := range h.buckets { 256 b[i] = HistogramBucket{ 257 LowBound: v.lowBound, 258 Count: v.count.Delta1h(), 259 } 260 } 261 262 v := HistogramValue{ 263 Count: h.count.Delta1h(), 264 Sum: h.sum.Delta1h(), 265 Min: h.tracker.Min1h(), 266 Max: h.tracker.Max1h(), 267 Buckets: b, 268 } 269 return v 270 } 271 272 // Delta10m returns the change in the last 10 minutes. 273 func (h *Histogram) Delta10m() HistogramValue { 274 b := make([]HistogramBucket, len(h.buckets)) 275 for i, v := range h.buckets { 276 b[i] = HistogramBucket{ 277 LowBound: v.lowBound, 278 Count: v.count.Delta10m(), 279 } 280 } 281 282 v := HistogramValue{ 283 Count: h.count.Delta10m(), 284 Sum: h.sum.Delta10m(), 285 Min: h.tracker.Min10m(), 286 Max: h.tracker.Max10m(), 287 Buckets: b, 288 } 289 return v 290 } 291 292 // Delta1m returns the change in the last 10 minutes. 293 func (h *Histogram) Delta1m() HistogramValue { 294 b := make([]HistogramBucket, len(h.buckets)) 295 for i, v := range h.buckets { 296 b[i] = HistogramBucket{ 297 LowBound: v.lowBound, 298 Count: v.count.Delta1m(), 299 } 300 } 301 302 v := HistogramValue{ 303 Count: h.count.Delta1m(), 304 Sum: h.sum.Delta1m(), 305 Min: h.tracker.Min1m(), 306 Max: h.tracker.Max1m(), 307 Buckets: b, 308 } 309 return v 310 } 311 312 // findBucket does a binary search to find in which bucket the value goes. 313 func (h *Histogram) findBucket(value int64) (int, error) { 314 lastBucket := len(h.buckets) - 1 315 min, max := 0, lastBucket 316 for max >= min { 317 b := (min + max) / 2 318 if value >= h.buckets[b].lowBound && (b == lastBucket || value < h.buckets[b+1].lowBound) { 319 return b, nil 320 } 321 if value < h.buckets[b].lowBound { 322 max = b - 1 323 continue 324 } 325 min = b + 1 326 } 327 return 0, fmt.Errorf("no bucket for value: %d", value) 328 }