|
| 1 | +package metrics |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "io" |
| 6 | + "math" |
| 7 | + "sync" |
| 8 | + "time" |
| 9 | +) |
| 10 | + |
| 11 | +// PrometheusHistogramDefaultBuckets is a list of the default bucket upper |
| 12 | +// bounds. Those default buckets are quite generic, and it is recommended to |
| 13 | +// pick custom buckets for improved accuracy. |
| 14 | +var PrometheusHistogramDefaultBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} |
| 15 | + |
| 16 | +// PrometheusHistogram is a histogram for non-negative values with pre-defined buckets |
| 17 | +// |
| 18 | +// Each bucket contains a counter for values in the given range. |
| 19 | +// Each bucket is exposed via the following metric: |
| 20 | +// |
| 21 | +// <metric_name>_bucket{<optional_tags>,le="upper_bound"} <counter> |
| 22 | +// |
| 23 | +// Where: |
| 24 | +// |
| 25 | +// - <metric_name> is the metric name passed to NewPrometheusHistogram |
| 26 | +// - <optional_tags> is optional tags for the <metric_name>, which are passed to NewPrometheusHistogram |
| 27 | +// - <upper_bound> - upper bound of the current bucket. all samples <= upper_bound are in that bucket |
| 28 | +// - <counter> - the number of hits to the given bucket during Update* calls |
| 29 | +// |
| 30 | +// Next to the bucket metrics, two additional metrics track the total number of |
| 31 | +// samples (_count) and the total sum (_sum) of all samples: |
| 32 | +// |
| 33 | +// - <metric_name>_sum{<optional_tags>} <counter> |
| 34 | +// - <metric_name>_count{<optional_tags>} <counter> |
| 35 | +type PrometheusHistogram struct { |
| 36 | + // mu guarantees synchronous update for all the counters. |
| 37 | + // |
| 38 | + // Do not use sync.RWMutex, since it has zero sense from performance PoV. |
| 39 | + // It only complicates the code. |
| 40 | + mu sync.Mutex |
| 41 | + |
| 42 | + // upperBounds and buckets are aligned by element position: |
| 43 | + // upperBounds[i] defines the upper bound for buckets[i]. |
| 44 | + // buckets[i] contains the count of elements <= upperBounds[i] |
| 45 | + upperBounds []float64 |
| 46 | + buckets []uint64 |
| 47 | + |
| 48 | + // count is the counter for all observations on this histogram |
| 49 | + count uint64 |
| 50 | + |
| 51 | + // sum is the sum of all the values put into Histogram |
| 52 | + sum float64 |
| 53 | +} |
| 54 | + |
| 55 | +// Reset resets previous observations in h. |
| 56 | +func (h *PrometheusHistogram) Reset() { |
| 57 | + h.mu.Lock() |
| 58 | + for i := range h.buckets { |
| 59 | + h.buckets[i] = 0 |
| 60 | + } |
| 61 | + h.sum = 0 |
| 62 | + h.count = 0 |
| 63 | + h.mu.Unlock() |
| 64 | +} |
| 65 | + |
| 66 | +// Update updates h with v. |
| 67 | +// |
| 68 | +// Negative values and NaNs are ignored. |
| 69 | +func (h *PrometheusHistogram) Update(v float64) { |
| 70 | + if math.IsNaN(v) || v < 0 { |
| 71 | + // Skip NaNs and negative values. |
| 72 | + return |
| 73 | + } |
| 74 | + bucketIdx := -1 |
| 75 | + for i, ub := range h.upperBounds { |
| 76 | + if v <= ub { |
| 77 | + bucketIdx = i |
| 78 | + break |
| 79 | + } |
| 80 | + } |
| 81 | + h.mu.Lock() |
| 82 | + h.sum += v |
| 83 | + h.count++ |
| 84 | + if bucketIdx == -1 { |
| 85 | + // +Inf, nothing to do, already accounted for in the total sum |
| 86 | + h.mu.Unlock() |
| 87 | + return |
| 88 | + } |
| 89 | + h.buckets[bucketIdx]++ |
| 90 | + h.mu.Unlock() |
| 91 | +} |
| 92 | + |
| 93 | +// UpdateDuration updates request duration based on the given startTime. |
| 94 | +func (h *PrometheusHistogram) UpdateDuration(startTime time.Time) { |
| 95 | + d := time.Since(startTime).Seconds() |
| 96 | + h.Update(d) |
| 97 | +} |
| 98 | + |
| 99 | +// NewPrometheusHistogram creates and returns new PrometheusHistogram with the given name |
| 100 | +// and PrometheusHistogramDefaultBuckets. |
| 101 | +// |
| 102 | +// name must be valid Prometheus-compatible metric with possible labels. |
| 103 | +// For instance, |
| 104 | +// |
| 105 | +// - foo |
| 106 | +// - foo{bar="baz"} |
| 107 | +// - foo{bar="baz",aaa="b"} |
| 108 | +// |
| 109 | +// The returned histogram is safe to use from concurrent goroutines. |
| 110 | +func NewPrometheusHistogram(name string) *PrometheusHistogram { |
| 111 | + return defaultSet.NewPrometheusHistogram(name) |
| 112 | +} |
| 113 | + |
| 114 | +// NewPrometheusHistogramExt creates and returns new PrometheusHistogram with the given name |
| 115 | +// and given upperBounds. |
| 116 | +// |
| 117 | +// name must be valid Prometheus-compatible metric with possible labels. |
| 118 | +// For instance, |
| 119 | +// |
| 120 | +// - foo |
| 121 | +// - foo{bar="baz"} |
| 122 | +// - foo{bar="baz",aaa="b"} |
| 123 | +// |
| 124 | +// The returned histogram is safe to use from concurrent goroutines. |
| 125 | +func NewPrometheusHistogramExt(name string, upperBounds []float64) *PrometheusHistogram { |
| 126 | + return defaultSet.NewPrometheusHistogramExt(name, upperBounds) |
| 127 | +} |
| 128 | + |
| 129 | +// GetOrCreatePrometheusHistogram returns registered PrometheusHistogram with the given name |
| 130 | +// or creates a new PrometheusHistogram if the registry doesn't contain histogram with |
| 131 | +// the given name. |
| 132 | +// |
| 133 | +// name must be valid Prometheus-compatible metric with possible labels. |
| 134 | +// For instance, |
| 135 | +// |
| 136 | +// - foo |
| 137 | +// - foo{bar="baz"} |
| 138 | +// - foo{bar="baz",aaa="b"} |
| 139 | +// |
| 140 | +// The returned histogram is safe to use from concurrent goroutines. |
| 141 | +// |
| 142 | +// Performance tip: prefer NewPrometheusHistogram instead of GetOrCreatePrometheusHistogram. |
| 143 | +func GetOrCreatePrometheusHistogram(name string) *PrometheusHistogram { |
| 144 | + return defaultSet.GetOrCreatePrometheusHistogram(name) |
| 145 | +} |
| 146 | + |
| 147 | +// GetOrCreatePrometheusHistogramExt returns registered PrometheusHistogram with the given name and |
| 148 | +// upperBounds or creates new PrometheusHistogram if the registry doesn't contain histogram |
| 149 | +// with the given name. |
| 150 | +// |
| 151 | +// name must be valid Prometheus-compatible metric with possible labels. |
| 152 | +// For instance, |
| 153 | +// |
| 154 | +// - foo |
| 155 | +// - foo{bar="baz"} |
| 156 | +// - foo{bar="baz",aaa="b"} |
| 157 | +// |
| 158 | +// The returned histogram is safe to use from concurrent goroutines. |
| 159 | +// |
| 160 | +// Performance tip: prefer NewPrometheusHistogramExt instead of GetOrCreatePrometheusHistogramExt. |
| 161 | +func GetOrCreatePrometheusHistogramExt(name string, upperBounds []float64) *PrometheusHistogram { |
| 162 | + return defaultSet.GetOrCreatePrometheusHistogramExt(name, upperBounds) |
| 163 | +} |
| 164 | + |
| 165 | +func newPrometheusHistogram(upperBounds []float64) *PrometheusHistogram { |
| 166 | + mustValidateBuckets(upperBounds) |
| 167 | + last := len(upperBounds) - 1 |
| 168 | + if math.IsInf(upperBounds[last], +1) { |
| 169 | + upperBounds = upperBounds[:last] // ignore +Inf bucket as it is covered anyways |
| 170 | + } |
| 171 | + h := PrometheusHistogram{ |
| 172 | + upperBounds: upperBounds, |
| 173 | + buckets: make([]uint64, len(upperBounds)), |
| 174 | + } |
| 175 | + |
| 176 | + return &h |
| 177 | +} |
| 178 | + |
| 179 | +func mustValidateBuckets(upperBounds []float64) { |
| 180 | + if err := ValidateBuckets(upperBounds); err != nil { |
| 181 | + panic(err) |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +// ValidateBuckets validates the given upperBounds and returns an error |
| 186 | +// if validation failed. |
| 187 | +func ValidateBuckets(upperBounds []float64) error { |
| 188 | + if len(upperBounds) == 0 { |
| 189 | + return fmt.Errorf("upperBounds can't be empty") |
| 190 | + } |
| 191 | + for i := 0; i < len(upperBounds)-1; i++ { |
| 192 | + if upperBounds[i] >= upperBounds[i+1] { |
| 193 | + return fmt.Errorf("upper bounds for the buckets must be strictly increasing") |
| 194 | + } |
| 195 | + } |
| 196 | + return nil |
| 197 | +} |
| 198 | + |
| 199 | +// LinearBuckets returns a list of upperBounds for PrometheusHistogram, |
| 200 | +// and whose distribution is as follows: |
| 201 | +// |
| 202 | +// [start, start + width, start + 2 * width, ... start + (count-1) * width] |
| 203 | +// |
| 204 | +// Panics if given start, width and count produce negative buckets or none buckets at all. |
| 205 | +func LinearBuckets(start, width float64, count int) []float64 { |
| 206 | + if count < 1 { |
| 207 | + panic("LinearBuckets: count can't be less than 1") |
| 208 | + } |
| 209 | + upperBounds := make([]float64, count) |
| 210 | + for i := range upperBounds { |
| 211 | + upperBounds[i] = start |
| 212 | + start += width |
| 213 | + } |
| 214 | + mustValidateBuckets(upperBounds) |
| 215 | + return upperBounds |
| 216 | +} |
| 217 | + |
| 218 | +// ExponentialBuckets returns a list of upperBounds for PrometheusHistogram, |
| 219 | +// and whose distribution is as follows: |
| 220 | +// |
| 221 | +// [start, start * factor pow 1, start * factor pow 2, ... start * factor pow (count-1)] |
| 222 | +// |
| 223 | +// Panics if given start, width and count produce negative buckets or none buckets at all. |
| 224 | +func ExponentialBuckets(start, factor float64, count int) []float64 { |
| 225 | + if count < 1 { |
| 226 | + panic("ExponentialBuckets: count can't be less than 1") |
| 227 | + } |
| 228 | + if factor <= 1 { |
| 229 | + panic("ExponentialBuckets: factor must be greater than 1") |
| 230 | + } |
| 231 | + if start <= 0 { |
| 232 | + panic("ExponentialBuckets: start can't be less than 0") |
| 233 | + } |
| 234 | + upperBounds := make([]float64, count) |
| 235 | + for i := range upperBounds { |
| 236 | + upperBounds[i] = start |
| 237 | + start *= factor |
| 238 | + } |
| 239 | + mustValidateBuckets(upperBounds) |
| 240 | + return upperBounds |
| 241 | +} |
| 242 | + |
| 243 | +func (h *PrometheusHistogram) marshalTo(prefix string, w io.Writer) { |
| 244 | + cumulativeSum := uint64(0) |
| 245 | + h.mu.Lock() |
| 246 | + count := h.count |
| 247 | + sum := h.sum |
| 248 | + for i, ub := range h.upperBounds { |
| 249 | + cumulativeSum += h.buckets[i] |
| 250 | + tag := fmt.Sprintf(`le="%v"`, ub) |
| 251 | + metricName := addTag(prefix, tag) |
| 252 | + name, labels := splitMetricName(metricName) |
| 253 | + fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, cumulativeSum) |
| 254 | + } |
| 255 | + h.mu.Unlock() |
| 256 | + |
| 257 | + tag := fmt.Sprintf("le=%q", "+Inf") |
| 258 | + metricName := addTag(prefix, tag) |
| 259 | + name, labels := splitMetricName(metricName) |
| 260 | + fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, count) |
| 261 | + |
| 262 | + name, labels = splitMetricName(prefix) |
| 263 | + if float64(int64(sum)) == sum { |
| 264 | + fmt.Fprintf(w, "%s_sum%s %d\n", name, labels, int64(sum)) |
| 265 | + } else { |
| 266 | + fmt.Fprintf(w, "%s_sum%s %g\n", name, labels, sum) |
| 267 | + } |
| 268 | + fmt.Fprintf(w, "%s_count%s %d\n", name, labels, count) |
| 269 | +} |
| 270 | + |
| 271 | +func (h *PrometheusHistogram) metricType() string { |
| 272 | + return "histogram" |
| 273 | +} |
0 commit comments