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  }