@@ -20,6 +20,7 @@ import (
20
20
"sort"
21
21
"sync"
22
22
"sync/atomic"
23
+ "time"
23
24
24
25
"github.com/golang/protobuf/proto"
25
26
@@ -47,6 +48,16 @@ type Histogram interface {
47
48
48
49
// Observe adds a single observation to the histogram.
49
50
Observe (float64 )
51
+ // ObserveWithExemplar works like Observe but also replaces the
52
+ // currently saved exemplar for the relevant bucket (possibly none) with
53
+ // a new one, created from the provided value, the current time as
54
+ // timestamp, and the provided Labels. Empty Labels will lead to a valid
55
+ // (label-less) exemplar. But if Labels is nil, the current exemplar in
56
+ // the relevant bucket is left in place. This method panics if any of
57
+ // the provided labels are invalid or if the provided labels contain
58
+ // more than 64 runes in total. ObserveWithExemplar should not be called
59
+ // in a hot path as it is significantly more costly than Observe.
60
+ ObserveWithExemplar (value float64 , exemplar Labels )
50
61
}
51
62
52
63
// bucketLabel is used for the label that defines the upper bound of a
@@ -205,9 +216,10 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr
205
216
}
206
217
}
207
218
// Finally we know the final length of h.upperBounds and can make buckets
208
- // for both counts:
219
+ // for both counts as well as exemplars :
209
220
h .counts [0 ].buckets = make ([]uint64 , len (h .upperBounds ))
210
221
h .counts [1 ].buckets = make ([]uint64 , len (h .upperBounds ))
222
+ h .exemplars = make ([]atomic.Value , len (h .upperBounds )+ 1 )
211
223
212
224
h .init (h ) // Init self-collection.
213
225
return h
@@ -254,43 +266,21 @@ type histogram struct {
254
266
255
267
upperBounds []float64
256
268
labelPairs []* dto.LabelPair
269
+ exemplars []atomic.Value // One more than buckets (to include +Inf), each a *dto.Exemplar.
257
270
}
258
271
259
272
func (h * histogram ) Desc () * Desc {
260
273
return h .desc
261
274
}
262
275
263
276
func (h * histogram ) Observe (v float64 ) {
264
- // TODO(beorn7): For small numbers of buckets (<30), a linear search is
265
- // slightly faster than the binary search. If we really care, we could
266
- // switch from one search strategy to the other depending on the number
267
- // of buckets.
268
- //
269
- // Microbenchmarks (BenchmarkHistogramNoLabels):
270
- // 11 buckets: 38.3 ns/op linear - binary 48.7 ns/op
271
- // 100 buckets: 78.1 ns/op linear - binary 54.9 ns/op
272
- // 300 buckets: 154 ns/op linear - binary 61.6 ns/op
273
- i := sort .SearchFloat64s (h .upperBounds , v )
274
-
275
- // We increment h.countAndHotIdx so that the counter in the lower
276
- // 63 bits gets incremented. At the same time, we get the new value
277
- // back, which we can use to find the currently-hot counts.
278
- n := atomic .AddUint64 (& h .countAndHotIdx , 1 )
279
- hotCounts := h .counts [n >> 63 ]
277
+ h .observe (v , h .findBucket (v ))
278
+ }
280
279
281
- if i < len (h .upperBounds ) {
282
- atomic .AddUint64 (& hotCounts .buckets [i ], 1 )
283
- }
284
- for {
285
- oldBits := atomic .LoadUint64 (& hotCounts .sumBits )
286
- newBits := math .Float64bits (math .Float64frombits (oldBits ) + v )
287
- if atomic .CompareAndSwapUint64 (& hotCounts .sumBits , oldBits , newBits ) {
288
- break
289
- }
290
- }
291
- // Increment count last as we take it as a signal that the observation
292
- // is complete.
293
- atomic .AddUint64 (& hotCounts .count , 1 )
280
+ func (h * histogram ) ObserveWithExemplar (v float64 , e Labels ) {
281
+ i := h .findBucket (v )
282
+ h .observe (v , i )
283
+ h .updateExemplar (v , i , e )
294
284
}
295
285
296
286
func (h * histogram ) Write (out * dto.Metric ) error {
@@ -329,6 +319,18 @@ func (h *histogram) Write(out *dto.Metric) error {
329
319
CumulativeCount : proto .Uint64 (cumCount ),
330
320
UpperBound : proto .Float64 (upperBound ),
331
321
}
322
+ if e := h .exemplars [i ].Load (); e != nil {
323
+ his .Bucket [i ].Exemplar = e .(* dto.Exemplar )
324
+ }
325
+ }
326
+ // If there is an exemplar for the +Inf bucket, we have to add that bucket explicitly.
327
+ if e := h .exemplars [len (h .upperBounds )].Load (); e != nil {
328
+ b := & dto.Bucket {
329
+ CumulativeCount : proto .Uint64 (count ),
330
+ UpperBound : proto .Float64 (math .Inf (1 )),
331
+ Exemplar : e .(* dto.Exemplar ),
332
+ }
333
+ his .Bucket = append (his .Bucket , b )
332
334
}
333
335
334
336
out .Histogram = his
@@ -352,6 +354,57 @@ func (h *histogram) Write(out *dto.Metric) error {
352
354
return nil
353
355
}
354
356
357
+ // findBucket returns the index of the bucket for the provided value, or
358
+ // len(h.upperBounds) for the +Inf bucket.
359
+ func (h * histogram ) findBucket (v float64 ) int {
360
+ // TODO(beorn7): For small numbers of buckets (<30), a linear search is
361
+ // slightly faster than the binary search. If we really care, we could
362
+ // switch from one search strategy to the other depending on the number
363
+ // of buckets.
364
+ //
365
+ // Microbenchmarks (BenchmarkHistogramNoLabels):
366
+ // 11 buckets: 38.3 ns/op linear - binary 48.7 ns/op
367
+ // 100 buckets: 78.1 ns/op linear - binary 54.9 ns/op
368
+ // 300 buckets: 154 ns/op linear - binary 61.6 ns/op
369
+ return sort .SearchFloat64s (h .upperBounds , v )
370
+ }
371
+
372
+ // observe is the implementation for Observe without the findBucket part.
373
+ func (h * histogram ) observe (v float64 , bucket int ) {
374
+ // We increment h.countAndHotIdx so that the counter in the lower
375
+ // 63 bits gets incremented. At the same time, we get the new value
376
+ // back, which we can use to find the currently-hot counts.
377
+ n := atomic .AddUint64 (& h .countAndHotIdx , 1 )
378
+ hotCounts := h .counts [n >> 63 ]
379
+
380
+ if bucket < len (h .upperBounds ) {
381
+ atomic .AddUint64 (& hotCounts .buckets [bucket ], 1 )
382
+ }
383
+ for {
384
+ oldBits := atomic .LoadUint64 (& hotCounts .sumBits )
385
+ newBits := math .Float64bits (math .Float64frombits (oldBits ) + v )
386
+ if atomic .CompareAndSwapUint64 (& hotCounts .sumBits , oldBits , newBits ) {
387
+ break
388
+ }
389
+ }
390
+ // Increment count last as we take it as a signal that the observation
391
+ // is complete.
392
+ atomic .AddUint64 (& hotCounts .count , 1 )
393
+ }
394
+
395
+ // updateExemplar replaces the exemplar for the provided bucket. With empty
396
+ // labels, it's a no-op. It panics if any of the labels is invalid.
397
+ func (h * histogram ) updateExemplar (v float64 , bucket int , l Labels ) {
398
+ if l == nil {
399
+ return
400
+ }
401
+ e , err := newExemplar (v , time .Now (), l )
402
+ if err != nil {
403
+ panic (err )
404
+ }
405
+ h .exemplars [bucket ].Store (e )
406
+ }
407
+
355
408
// HistogramVec is a Collector that bundles a set of Histograms that all share the
356
409
// same Desc, but have different values for their variable labels. This is used
357
410
// if you want to count the same thing partitioned by various dimensions
0 commit comments