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