/* Copyright (c) 2012, Matt T. Proud All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. */ package metrics import ( "fmt" "github.com/prometheus/client_golang/maths" "math" "sync" ) const ( lowerThird = 1.0 / 3.0 upperThird = 2.0 * lowerThird ) /* A TallyingIndexEstimator is responsible for estimating the value of index for a given TallyingBucket, even though a TallyingBucket does not possess a collection of samples. There are a few strategies listed below for how this value should be approximated. */ type TallyingIndexEstimator func(minimum, maximum float64, index, observations int) float64 /* Provide a filter for handling empty buckets. */ func emptyFilter(e TallyingIndexEstimator) TallyingIndexEstimator { return func(minimum, maximum float64, index, observations int) float64 { if observations == 0 { return math.NaN() } return e(minimum, maximum, index, observations) } } /* Report the smallest observed value in the bucket. */ var Minimum TallyingIndexEstimator = emptyFilter(func(minimum, maximum float64, _, observations int) float64 { return minimum }) /* Report the largest observed value in the bucket. */ var Maximum TallyingIndexEstimator = emptyFilter(func(minimum, maximum float64, _, observations int) float64 { return maximum }) /* Report the average of the extrema. */ var Average TallyingIndexEstimator = emptyFilter(func(minimum, maximum float64, _, observations int) float64 { return maths.Average([]float64{minimum, maximum}) }) /* Report the minimum value of the index is in the lower-third of observations, the average if in the middle-third, and the maximum if in the largest third. */ var Uniform TallyingIndexEstimator = emptyFilter(func(minimum, maximum float64, index, observations int) float64 { if observations == 1 { return minimum } location := float64(index) / float64(observations) if location > upperThird { return maximum } else if location < lowerThird { return minimum } return maths.Average([]float64{minimum, maximum}) }) /* A TallyingBucket is a Bucket that tallies when an object is added to it. Upon insertion, an object is compared against collected extrema and noted as a new minimum or maximum if appropriate. */ type TallyingBucket struct { estimator TallyingIndexEstimator largestObserved float64 mutex sync.RWMutex observations int smallestObserved float64 } func (b *TallyingBucket) Add(value float64) { b.mutex.Lock() defer b.mutex.Unlock() b.observations += 1 b.smallestObserved = math.Min(value, b.smallestObserved) b.largestObserved = math.Max(value, b.largestObserved) } func (b *TallyingBucket) String() string { b.mutex.RLock() defer b.mutex.RUnlock() observations := b.observations if observations == 0 { return fmt.Sprintf("[TallyingBucket (Empty)]") } return fmt.Sprintf("[TallyingBucket (%f, %f); %d items]", b.smallestObserved, b.largestObserved, observations) } func (b *TallyingBucket) Observations() int { b.mutex.RLock() defer b.mutex.RUnlock() return b.observations } func (b *TallyingBucket) ValueForIndex(index int) float64 { b.mutex.RLock() defer b.mutex.RUnlock() return b.estimator(b.smallestObserved, b.largestObserved, index, b.observations) } func (b *TallyingBucket) Reset() { b.mutex.Lock() defer b.mutex.Unlock() b.largestObserved = math.SmallestNonzeroFloat64 b.observations = 0 b.smallestObserved = math.MaxFloat64 } /* Produce a TallyingBucket with sane defaults. */ func DefaultTallyingBucket() TallyingBucket { return TallyingBucket{ estimator: Minimum, largestObserved: math.SmallestNonzeroFloat64, smallestObserved: math.MaxFloat64, } } func CustomTallyingBucket(estimator TallyingIndexEstimator) TallyingBucket { return TallyingBucket{ estimator: estimator, largestObserved: math.SmallestNonzeroFloat64, smallestObserved: math.MaxFloat64, } } /* This is used strictly for testing. */ func TallyingBucketBuilder() Bucket { b := DefaultTallyingBucket() return &b }