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  }