Reduce histogram buckets from 0.1 to 0.02 of the size for the current decimal exponent

Also increase upper bound for histogram range from 10^12 to 10^18
This commit is contained in:
Aliaksandr Valialkin 2019-11-25 13:03:26 +02:00
parent eb780f4caa
commit 3306660b50
3 changed files with 287 additions and 246 deletions

View File

@ -5,28 +5,22 @@ import (
"io"
"math"
"sync"
"sync/atomic"
"time"
)
// Histogram is a histogram that covers values with the following buckets:
//
// 0
// (0...1e-9]
// (1e-9...2e-9]
// (2e-9...3e-9]
// ...
// (9e-9...1e-8]
// (1e-8...2e-8]
// ...
// (1e11...2e11]
// (2e11...3e11]
// ...
// (9e11...1e12]
// (1e12...Inf]
const (
e10Min = -9
e10Max = 18
decimalMultiplier = 5
bucketSize = 9 * decimalMultiplier
bucketsCount = e10Max - e10Min
decimalPrecision = 0.01 / decimalMultiplier
)
// Histogram is a histogram for non-negative values with automatically created buckets.
//
// Each bucket contains a counter for values in the given range.
// Each non-zero bucket is exposed with the following name:
// Each non-empty bucket is exposed via the following metric:
//
// <metric_name>_bucket{<optional_tags>,vmrange="<start>...<end>"} <counter>
//
@ -35,24 +29,127 @@ import (
// - <metric_name> is the metric name passed to NewHistogram
// - <optional_tags> is optional tags for the <metric_name>, which are passed to NewHistogram
// - <start> and <end> - start and end values for the given bucket
// - <counter> - the number of hits to the given bucket during Update* calls.
// - <counter> - the number of hits to the given bucket during Update* calls
//
// Histogram buckets can be converted to Prometheus-like buckets with `le` labels
// with `prometheus_buckets(<metric_name>_bucket)` function in VictoriaMetrics:
//
// prometheus_buckets(request_duration_bucket)
//
// Histogram cannot be used for negative values.
//
// Time series produced by the Histogram have better compression ratio comparing to
// Prometheus histogram buckets with `le` labels, since they don't include counters
// for all the previous buckets.
//
// Zero histogram is usable.
type Histogram struct {
buckets [bucketsCount]uint64
// Mu gurantees synchronous update for all the counters and sum.
mu sync.Mutex
sumMu sync.Mutex
sum float64
count uint64
buckets [bucketsCount]*histogramBucket
zeros uint64
lower uint64
upper uint64
sum float64
}
// Reset resets the given histogram.
func (h *Histogram) Reset() {
h.mu.Lock()
h.resetLocked()
h.mu.Unlock()
}
func (h *Histogram) resetLocked() {
for _, hb := range h.buckets[:] {
if hb == nil {
continue
}
for offset := range hb.counts[:] {
hb.counts[offset] = 0
}
}
h.zeros = 0
h.lower = 0
h.upper = 0
}
// Update updates h with v.
//
// Negative values and NaNs are ignored.
func (h *Histogram) Update(v float64) {
if math.IsNaN(v) || v < 0 {
// Skip NaNs and negative values.
return
}
bucketIdx, offset := getBucketIdxAndOffset(v)
h.mu.Lock()
h.updateLocked(v, bucketIdx, offset)
h.mu.Unlock()
}
func (h *Histogram) updateLocked(v float64, bucketIdx int, offset uint) {
h.sum += v
if bucketIdx < 0 {
// Special cases for zero, too small or too big value
if offset == 0 {
h.zeros++
} else if offset == 1 {
h.lower++
} else {
h.upper++
}
return
}
hb := h.buckets[bucketIdx]
if hb == nil {
hb = &histogramBucket{}
h.buckets[bucketIdx] = hb
}
hb.counts[offset]++
}
// VisitNonZeroBuckets calls f for all buckets with non-zero counters.
//
// vmrange contains "<start>...<end>" string with bucket bounds. The lower bound
// isn't included in the bucket, while the upper bound is included.
// This is required to be compatible with Prometheus-style histogram buckets
// with `le` (less or equal) labels.
func (h *Histogram) VisitNonZeroBuckets(f func(vmrange string, count uint64)) {
h.mu.Lock()
h.visitNonZeroBucketsLocked(f)
h.mu.Unlock()
}
func (h *Histogram) visitNonZeroBucketsLocked(f func(vmrange string, count uint64)) {
if h.zeros > 0 {
vmrange := getVMRange(-1, 0)
f(vmrange, h.zeros)
}
if h.lower > 0 {
vmrange := getVMRange(-1, 1)
f(vmrange, h.lower)
}
for bucketIdx, hb := range h.buckets[:] {
if hb == nil {
continue
}
for offset, count := range hb.counts[:] {
if count > 0 {
vmrange := getVMRange(bucketIdx, uint(offset))
f(vmrange, count)
}
}
}
if h.upper > 0 {
vmrange := getVMRange(-1, 2)
f(vmrange, h.upper)
}
}
type histogramBucket struct {
counts [bucketSize]uint64
}
// NewHistogram creates and returns new histogram with the given name.
@ -87,175 +184,112 @@ func GetOrCreateHistogram(name string) *Histogram {
return defaultSet.GetOrCreateHistogram(name)
}
// Update updates h with v.
//
// v cannot be negative.
func (h *Histogram) Update(v float64) {
if math.IsNaN(v) || v < 0 {
// Skip NaNs and negative values.
return
}
idx := getBucketIdx(v)
if idx >= uint(len(h.buckets)) {
panic(fmt.Errorf("BUG: idx cannot exceed %d; got %d", len(h.buckets), idx))
}
atomic.AddUint64(&h.buckets[idx], 1)
atomic.AddUint64(&h.count, 1)
h.sumMu.Lock()
h.sum += v
h.sumMu.Unlock()
}
// UpdateDuration updates request duration based on the given startTime.
func (h *Histogram) UpdateDuration(startTime time.Time) {
d := time.Since(startTime).Seconds()
h.Update(d)
}
// VisitNonZeroBuckets calls f for all buckets with non-zero counters.
func (h *Histogram) VisitNonZeroBuckets(f func(vmrange string, count uint64)) {
for i, v := range h.buckets[:] {
if v == 0 {
continue
}
vmrange := getRangeForBucketIdx(uint(i))
f(vmrange, v)
}
}
func getRangeForBucketIdx(idx uint) string {
func getVMRange(bucketIdx int, offset uint) string {
bucketRangesOnce.Do(initBucketRanges)
if bucketIdx < 0 {
if offset > 2 {
panic(fmt.Errorf("BUG: offset must be in range [0...2] for negative bucketIdx; got %d", offset))
}
return bucketRanges[offset]
}
idx := 3 + uint(bucketIdx)*bucketSize + offset
return bucketRanges[idx]
}
func initBucketRanges() {
start := "0"
for i := 0; i < bucketsCount; i++ {
end := getRangeEndFromBucketIdx(uint(i))
bucketRanges[i] = start + "..." + end
start = end
bucketRanges[0] = "0...0"
bucketRanges[1] = fmt.Sprintf("0...%.1fe%d", 1.0, e10Min)
bucketRanges[2] = fmt.Sprintf("%.1fe%d...+Inf", 1.0, e10Max)
idx := 3
start := fmt.Sprintf("%.1fe%d", 1.0, e10Min)
for bucketIdx := 0; bucketIdx < bucketsCount; bucketIdx++ {
for offset := 0; offset < bucketSize; offset++ {
e10 := e10Min + bucketIdx
m := 1 + float64(offset+1)/decimalMultiplier
if math.Abs(m-10) < decimalPrecision {
m = 1
e10++
}
end := fmt.Sprintf("%.1fe%d", m, e10)
bucketRanges[idx] = start + "..." + end
idx++
start = end
}
}
}
var (
bucketRanges [bucketsCount]string
// 3 additional buckets for zero, lower and upper.
bucketRanges [3 + bucketsCount*bucketSize]string
bucketRangesOnce sync.Once
)
func getTagForBucketIdx(idx uint) string {
bucketTagsOnce.Do(initBucketTags)
return bucketTags[idx]
}
func initBucketTags() {
for i := 0; i < bucketsCount; i++ {
vmrange := getRangeForBucketIdx(uint(i))
bucketTags[i] = fmt.Sprintf(`vmrange=%q`, vmrange)
}
}
var (
bucketTags [bucketsCount]string
bucketTagsOnce sync.Once
)
func (h *Histogram) marshalTo(prefix string, w io.Writer) {
count := atomic.LoadUint64(&h.count)
if count == 0 {
countTotal := uint64(0)
h.VisitNonZeroBuckets(func(vmrange string, count uint64) {
tag := fmt.Sprintf("vmrange=%q", vmrange)
metricName := addTag(prefix, tag)
name, filters := splitMetricName(metricName)
fmt.Fprintf(w, "%s_bucket%s %d\n", name, filters, count)
countTotal += count
})
if countTotal == 0 {
return
}
for i := range h.buckets[:] {
h.marshalBucket(prefix, w, uint(i))
}
// Marshal `_sum` and `_count` metrics.
name, filters := splitMetricName(prefix)
h.sumMu.Lock()
sum := h.sum
h.sumMu.Unlock()
if float64(int64(sum)) == sum {
fmt.Fprintf(w, "%s_sum%s %d\n", name, filters, int64(sum))
if float64(int64(h.sum)) == h.sum {
fmt.Fprintf(w, "%s_sum%s %d\n", name, filters, int64(h.sum))
} else {
fmt.Fprintf(w, "%s_sum%s %g\n", name, filters, sum)
fmt.Fprintf(w, "%s_sum%s %g\n", name, filters, h.sum)
}
fmt.Fprintf(w, "%s_count%s %d\n", name, filters, count)
fmt.Fprintf(w, "%s_count%s %d\n", name, filters, countTotal)
}
func (h *Histogram) marshalBucket(prefix string, w io.Writer, idx uint) {
v := h.buckets[idx]
if v == 0 {
return
}
tag := getTagForBucketIdx(idx)
prefix = addTag(prefix, tag)
name, filters := splitMetricName(prefix)
fmt.Fprintf(w, "%s_bucket%s %d\n", name, filters, v)
}
func getBucketIdx(v float64) uint {
func getBucketIdxAndOffset(v float64) (int, uint) {
if v < 0 {
panic(fmt.Errorf("BUG: v cannot be negative; got %v", v))
panic(fmt.Errorf("BUG: v must be positive; got %g", v))
}
if v == 0 {
// Fast path for zero.
return 0
return -1, 0
}
if math.IsInf(v, 1) {
return bucketsCount - 1
return -1, 2
}
e10 := int(math.Floor(math.Log10(v)))
if e10 < e10Min {
return 1
bucketIdx := e10 - e10Min
if bucketIdx < 0 {
return -1, 1
}
if e10 > e10Max {
if e10 == e10Max+1 && math.Pow10(e10) == v {
if bucketIdx >= bucketsCount {
if bucketIdx == bucketsCount && math.Abs(math.Pow10(e10)-v) < decimalPrecision {
// Adjust m to be on par with Prometheus 'le' buckets (aka 'less or equal')
return bucketsCount - 2
return bucketsCount - 1, bucketSize - 1
}
return bucketsCount - 1
return -1, 2
}
mf := v / math.Pow10(e10)
m := uint(mf)
// Handle possible rounding errors
if m < 1 {
m = 1
} else if m > 9 {
m = 9
m := ((v / math.Pow10(e10)) - 1) * decimalMultiplier
offset := int(m)
if offset < 0 {
offset = 0
} else if offset >= bucketSize {
offset = bucketSize - 1
}
if float64(m) == mf {
// Adjust m to be on par with Prometheus 'le' buckets (aka 'less or equal')
m--
if math.Abs(float64(offset)-m) < decimalPrecision {
// Adjust offset to be on par with Prometheus 'le' buckets (aka 'less or equal')
offset--
if offset < 0 {
bucketIdx--
offset = bucketSize - 1
if bucketIdx < 0 {
return -1, 1
}
}
}
return 1 + m + uint(e10-e10Min)*9
return bucketIdx, uint(offset)
}
func getRangeEndFromBucketIdx(idx uint) string {
if idx == 0 {
return "0"
}
if idx == 1 {
return fmt.Sprintf("1e%d", e10Min)
}
if idx >= bucketsCount-1 {
return "+Inf"
}
idx -= 2
e10 := e10Min + int(idx/9)
m := 2 + (idx % 9)
if m == 10 {
e10++
m = 1
}
if e10 == 0 {
return fmt.Sprintf("%d", m)
}
return fmt.Sprintf("%de%d", m, e10)
}
// Each range (10^n..10^(n+1)] for e10Min<=n<=e10Max is split into 9 equal sub-ranges, plus 3 additional buckets:
// - a bucket for zeros
// - a bucket for the range (0..10^e10Min]
// - a bucket for the range (10^(e10Max+1)..Inf]
const bucketsCount = 3 + 9*(1+e10Max-e10Min)
const e10Min = -9
const e10Max = 11

View File

@ -10,87 +10,75 @@ import (
"time"
)
func TestGetBucketIdx(t *testing.T) {
f := func(v float64, idxExpected uint) {
func TestGetBucketIdxAndOffset(t *testing.T) {
f := func(v float64, bucketIdxExpected int, offsetExpected uint) {
t.Helper()
idx := getBucketIdx(v)
if idx != idxExpected {
t.Fatalf("unexpected getBucketIdx(%g); got %d; want %d", v, idx, idxExpected)
bucketIdx, offset := getBucketIdxAndOffset(v)
if bucketIdx != bucketIdxExpected {
t.Fatalf("unexpected bucketIdx for %g; got %d; want %d", v, bucketIdx, bucketIdxExpected)
}
if offset != offsetExpected {
t.Fatalf("unexpected offset for %g; got %d; want %d", v, offset, offsetExpected)
}
}
f(0, 0)
f(math.Pow10(e10Min-10), 1)
f(math.Pow10(e10Min-1), 1)
f(1.5*math.Pow10(e10Min-1), 1)
f(2*math.Pow10(e10Min-1), 1)
f(3*math.Pow10(e10Min-1), 1)
f(9*math.Pow10(e10Min-1), 1)
f(9.999*math.Pow10(e10Min-1), 1)
f(math.Pow10(e10Min), 1)
f(1.00001*math.Pow10(e10Min), 2)
f(1.5*math.Pow10(e10Min), 2)
f(1.999999*math.Pow10(e10Min), 2)
f(2*math.Pow10(e10Min), 2)
f(2.0000001*math.Pow10(e10Min), 3)
f(2.999*math.Pow10(e10Min), 3)
f(8.999*math.Pow10(e10Min), 9)
f(9*math.Pow10(e10Min), 9)
f(9.01*math.Pow10(e10Min), 10)
f(9.99999*math.Pow10(e10Min), 10)
f(math.Pow10(e10Min+1), 10)
f(1.9*math.Pow10(e10Min+1), 11)
f(9.9*math.Pow10(e10Min+1), 19)
f(math.Pow10(e10Min+2), 19)
f(math.Pow10(e10Min+3), 28)
f(5*math.Pow10(e10Min+3), 32)
f(0.1, 1-9*(e10Min+1))
f(0.11, 2-9*(e10Min+1))
f(0.95, 1-9*e10Min)
f(1, 1-9*e10Min)
f(2, 2-9*e10Min)
f(math.Pow10(e10Max), 1+9*(e10Max-e10Min))
f(2*math.Pow10(e10Max), 2+9*(e10Max-e10Min))
f(9.999*math.Pow10(e10Max), 10+9*(e10Max-e10Min))
f(math.Pow10(e10Max+1), 10+9*(e10Max-e10Min))
f(2*math.Pow10(e10Max+1), 11+9*(e10Max-e10Min))
f(9*math.Pow10(e10Max+1), 11+9*(e10Max-e10Min))
f(math.Pow10(e10Max+5), 11+9*(e10Max-e10Min))
f(12.34*math.Pow10(e10Max+7), 11+9*(e10Max-e10Min))
f(math.Inf(1), 11+9*(e10Max-e10Min))
const step = 1.0 / decimalMultiplier
const prec = 2 * decimalPrecision
f(0, -1, 0)
f(math.Pow10(e10Min-10), -1, 1)
f(math.Pow10(e10Min-1), -1, 1)
f(3*math.Pow10(e10Min-1), -1, 1)
f(9*math.Pow10(e10Min-1), -1, 1)
f(9.999*math.Pow10(e10Min-1), -1, 1)
f(math.Pow10(e10Min), -1, 1)
f((1+prec)*math.Pow10(e10Min), 0, 0)
f((1+step)*math.Pow10(e10Min), 0, 0)
f((1+step+prec)*math.Pow10(e10Min), 0, 1)
f((1+2*step+prec)*math.Pow10(e10Min), 0, 2)
f((1+3*step+prec)*math.Pow10(e10Min), 0, 3)
f(math.Pow10(e10Min+1), 0, bucketSize-1)
f((1+prec)*math.Pow10(e10Min+1), 1, 0)
f((1+step)*math.Pow10(e10Min+1), 1, 0)
f((1+step+prec)*math.Pow10(e10Min+1), 1, 1)
f(0.1, -e10Min-2, bucketSize-1)
f((1+prec)*0.1, -e10Min-1, 0)
f((1+step)*0.1, -e10Min-1, 0)
f((1+step+prec)*0.1, -e10Min-1, 1)
f((1+(bucketSize-1)*step)*0.1, -e10Min-1, bucketSize-2)
f((1+(bucketSize-1)*step+prec)*0.1, -e10Min-1, bucketSize-1)
f(math.Pow10(e10Max-2), bucketsCount-3, bucketSize-1)
f((1+prec)*math.Pow10(e10Max-2), bucketsCount-2, 0)
f(math.Pow10(e10Max-1), bucketsCount-2, bucketSize-1)
f((1+prec)*math.Pow10(e10Max-1), bucketsCount-1, 0)
f((1+(bucketSize-1)*step)*math.Pow10(e10Max-1), bucketsCount-1, bucketSize-2)
f((1+(bucketSize-1)*step+prec)*math.Pow10(e10Max-1), bucketsCount-1, bucketSize-1)
f(math.Pow10(e10Max), bucketsCount-1, bucketSize-1)
f((1+prec)*math.Pow10(e10Max), -1, 2)
f((1+3*step+prec)*math.Pow10(e10Max), -1, 2)
f(math.Inf(1), -1, 2)
}
func TestGetRangeEndFromBucketIdx(t *testing.T) {
f := func(idx uint, endExpected string) {
func TestGetVMRange(t *testing.T) {
f := func(bucketIdx int, offset uint, vmrangeExpected string) {
t.Helper()
end := getRangeEndFromBucketIdx(idx)
if end != endExpected {
t.Fatalf("unexpected getRangeEndFromBucketIdx(%d); got %s; want %s", idx, end, endExpected)
vmrange := getVMRange(bucketIdx, offset)
if vmrange != vmrangeExpected {
t.Fatalf("unexpected vmrange for bucketIdx=%d, offset=%d; got %s; want %s", bucketIdx, offset, vmrange, vmrangeExpected)
}
}
f(0, "0")
f(1, fmt.Sprintf("1e%d", e10Min))
f(2, fmt.Sprintf("2e%d", e10Min))
f(3, fmt.Sprintf("3e%d", e10Min))
f(9, fmt.Sprintf("9e%d", e10Min))
f(10, fmt.Sprintf("1e%d", e10Min+1))
f(11, fmt.Sprintf("2e%d", e10Min+1))
f(16, fmt.Sprintf("7e%d", e10Min+1))
f(17, fmt.Sprintf("8e%d", e10Min+1))
f(18, fmt.Sprintf("9e%d", e10Min+1))
f(19, fmt.Sprintf("1e%d", e10Min+2))
f(20, fmt.Sprintf("2e%d", e10Min+2))
f(21, fmt.Sprintf("3e%d", e10Min+2))
f(bucketsCount-21, fmt.Sprintf("9e%d", e10Max-2))
f(bucketsCount-20, fmt.Sprintf("1e%d", e10Max-1))
f(bucketsCount-16, fmt.Sprintf("5e%d", e10Max-1))
f(bucketsCount-13, fmt.Sprintf("8e%d", e10Max-1))
f(bucketsCount-12, fmt.Sprintf("9e%d", e10Max-1))
f(bucketsCount-11, fmt.Sprintf("1e%d", e10Max))
f(bucketsCount-10, fmt.Sprintf("2e%d", e10Max))
f(bucketsCount-4, fmt.Sprintf("8e%d", e10Max))
f(bucketsCount-3, fmt.Sprintf("9e%d", e10Max))
f(bucketsCount-2, fmt.Sprintf("1e%d", e10Max+1))
f(bucketsCount-1, "+Inf")
const step = 1.0 / decimalMultiplier
f(-1, 0, "0...0")
f(-1, 1, fmt.Sprintf("0...1.0e%d", e10Min))
f(-1, 2, fmt.Sprintf("1.0e%d...+Inf", e10Max))
f(0, 0, fmt.Sprintf("1.0e%d...%.1fe%d", e10Min, 1+step, e10Min))
f(0, 1, fmt.Sprintf("%.1fe%d...%.1fe%d", 1+step, e10Min, 1+2*step, e10Min))
f(0, bucketSize-2, fmt.Sprintf("%.1fe%d...%.1fe%d", 1+(bucketSize-2)*step, e10Min, 1+(bucketSize-1)*step, e10Min))
f(0, bucketSize-1, fmt.Sprintf("%.1fe%d...%.1fe%d", 1+(bucketSize-1)*step, e10Min, 1.0, e10Min+1))
f(-e10Min, 0, fmt.Sprintf("%.1fe%d...%.1fe%d", 1.0, 0, 1+step, 0))
f(-e10Min, 1, fmt.Sprintf("%.1fe%d...%.1fe%d", 1+step, 0, 1+2*step, 0))
f(-e10Min, bucketSize-2, fmt.Sprintf("%.1fe%d...%.1fe%d", 1+(bucketSize-2)*step, 0, 1+(bucketSize-1)*step, 0))
f(-e10Min, bucketSize-1, fmt.Sprintf("%.1fe%d...%.1fe%d", 1+(bucketSize-1)*step, 0, 1.0, 1))
f(bucketsCount-1, bucketSize-2, fmt.Sprintf("%.1fe%d...%.1fe%d", 1+(bucketSize-2)*step, e10Max-1, 1+(bucketSize-1)*step, e10Max-1))
f(bucketsCount-1, bucketSize-1, fmt.Sprintf("%.1fe%d...%.1fe%d", 1+(bucketSize-1)*step, e10Max-1, 1.0, e10Max))
}
func TestHistogramSerial(t *testing.T) {
@ -106,22 +94,41 @@ func TestHistogramSerial(t *testing.T) {
}
// Write data to histogram
for i := 84; i < 324; i++ {
for i := 98; i < 218; i++ {
h.Update(float64(i))
}
// Make sure the histogram prints <prefix>_xbucket on marshalTo call
testMarshalTo(t, h, "prefix", "prefix_bucket{vmrange=\"8e1...9e1\"} 7\nprefix_bucket{vmrange=\"9e1...1e2\"} 10\nprefix_bucket{vmrange=\"1e2...2e2\"} 100\nprefix_bucket{vmrange=\"2e2...3e2\"} 100\nprefix_bucket{vmrange=\"3e2...4e2\"} 23\nprefix_sum 48840\nprefix_count 240\n")
testMarshalTo(t, h, ` m{foo="bar"}`, "\t m_bucket{foo=\"bar\",vmrange=\"8e1...9e1\"} 7\n\t m_bucket{foo=\"bar\",vmrange=\"9e1...1e2\"} 10\n\t m_bucket{foo=\"bar\",vmrange=\"1e2...2e2\"} 100\n\t m_bucket{foo=\"bar\",vmrange=\"2e2...3e2\"} 100\n\t m_bucket{foo=\"bar\",vmrange=\"3e2...4e2\"} 23\n\t m_sum{foo=\"bar\"} 48840\n\t m_count{foo=\"bar\"} 240\n")
testMarshalTo(t, h, "prefix", "prefix_bucket{vmrange=\"9.6e1...9.8e1\"} 1\nprefix_bucket{vmrange=\"9.8e1...1.0e2\"} 2\nprefix_bucket{vmrange=\"1.0e2...1.2e2\"} 20\nprefix_bucket{vmrange=\"1.2e2...1.4e2\"} 20\nprefix_bucket{vmrange=\"1.4e2...1.6e2\"} 20\nprefix_bucket{vmrange=\"1.6e2...1.8e2\"} 20\nprefix_bucket{vmrange=\"1.8e2...2.0e2\"} 20\nprefix_bucket{vmrange=\"2.0e2...2.2e2\"} 17\nprefix_sum 18900\nprefix_count 120\n")
testMarshalTo(t, h, ` m{foo="bar"}`, "\t m_bucket{foo=\"bar\",vmrange=\"9.6e1...9.8e1\"} 1\n\t m_bucket{foo=\"bar\",vmrange=\"9.8e1...1.0e2\"} 2\n\t m_bucket{foo=\"bar\",vmrange=\"1.0e2...1.2e2\"} 20\n\t m_bucket{foo=\"bar\",vmrange=\"1.2e2...1.4e2\"} 20\n\t m_bucket{foo=\"bar\",vmrange=\"1.4e2...1.6e2\"} 20\n\t m_bucket{foo=\"bar\",vmrange=\"1.6e2...1.8e2\"} 20\n\t m_bucket{foo=\"bar\",vmrange=\"1.8e2...2.0e2\"} 20\n\t m_bucket{foo=\"bar\",vmrange=\"2.0e2...2.2e2\"} 17\n\t m_sum{foo=\"bar\"} 18900\n\t m_count{foo=\"bar\"} 120\n")
// Verify Reset
h.Reset()
bb.Reset()
WritePrometheus(&bb, false)
result = bb.String()
if strings.Contains(result, name) {
t.Fatalf("unexpected histogram %s in the WritePrometheus output; got\n%s", name, result)
}
// Verify supported ranges
for i := -100; i < 100; i++ {
h.Update(1.23 * math.Pow10(i))
for e10 := -100; e10 < 100; e10++ {
for offset := 0; offset < bucketSize; offset++ {
m := 1 + float64(offset+1)/decimalMultiplier
f1 := m * math.Pow10(e10)
h.Update(f1)
f2 := (m + 0.5/decimalMultiplier) * math.Pow10(e10)
h.Update(f2)
f3 := (m + 2*decimalPrecision) * math.Pow10(e10)
h.Update(f3)
}
}
h.UpdateDuration(time.Now().Add(-time.Minute))
// Verify edge cases
h.Update(0)
h.Update(math.Inf(1))
h.Update(math.Inf(-1))
h.Update(math.NaN())
h.Update(-123)
@ -139,29 +146,29 @@ func TestHistogramConcurrent(t *testing.T) {
name := "HistogramConcurrent"
h := NewHistogram(name)
err := testConcurrent(func() error {
for i := 0; i < 10; i++ {
h.Update(float64(i))
for f := 0.6; f < 1.4; f += 0.1 {
h.Update(f)
}
return nil
})
if err != nil {
t.Fatal(err)
}
testMarshalTo(t, h, "prefix", "prefix_bucket{vmrange=\"0...0\"} 5\nprefix_bucket{vmrange=\"9e-1...1\"} 5\nprefix_bucket{vmrange=\"1...2\"} 5\nprefix_bucket{vmrange=\"2...3\"} 5\nprefix_bucket{vmrange=\"3...4\"} 5\nprefix_bucket{vmrange=\"4...5\"} 5\nprefix_bucket{vmrange=\"5...6\"} 5\nprefix_bucket{vmrange=\"6...7\"} 5\nprefix_bucket{vmrange=\"7...8\"} 5\nprefix_bucket{vmrange=\"8...9\"} 5\nprefix_sum 225\nprefix_count 50\n")
testMarshalTo(t, h, "prefix", "prefix_bucket{vmrange=\"5.8e-1...6.0e-1\"} 5\nprefix_bucket{vmrange=\"6.8e-1...7.0e-1\"} 5\nprefix_bucket{vmrange=\"7.8e-1...8.0e-1\"} 5\nprefix_bucket{vmrange=\"8.8e-1...9.0e-1\"} 5\nprefix_bucket{vmrange=\"9.8e-1...1.0e0\"} 5\nprefix_bucket{vmrange=\"1.0e0...1.2e0\"} 10\nprefix_bucket{vmrange=\"1.2e0...1.4e0\"} 5\nprefix_sum 38\nprefix_count 40\n")
var labels []string
var values []uint64
h.VisitNonZeroBuckets(func(label string, value uint64) {
var counts []uint64
h.VisitNonZeroBuckets(func(label string, count uint64) {
labels = append(labels, label)
values = append(values, value)
counts = append(counts, count)
})
labelsExpected := []string{"0...0", "9e-1...1", "1...2", "2...3", "3...4", "4...5", "5...6", "6...7", "7...8", "8...9"}
labelsExpected := []string{"5.8e-1...6.0e-1", "6.8e-1...7.0e-1", "7.8e-1...8.0e-1", "8.8e-1...9.0e-1", "9.8e-1...1.0e0", "1.0e0...1.2e0", "1.2e0...1.4e0"}
if !reflect.DeepEqual(labels, labelsExpected) {
t.Fatalf("unexpected labels; got %v; want %v", labels, labelsExpected)
}
valuesExpected := []uint64{5, 5, 5, 5, 5, 5, 5, 5, 5, 5}
if !reflect.DeepEqual(values, valuesExpected) {
t.Fatalf("unexpected values; got %v; want %v", values, valuesExpected)
countsExpected := []uint64{5, 5, 5, 5, 5, 10, 5}
if !reflect.DeepEqual(counts, countsExpected) {
t.Fatalf("unexpected counts; got %v; want %v", counts, countsExpected)
}
}
@ -173,7 +180,7 @@ func TestHistogramWithTags(t *testing.T) {
var bb bytes.Buffer
WritePrometheus(&bb, false)
result := bb.String()
namePrefixWithTag := `TestHistogram_bucket{tag="foo",vmrange="1e2...2e2"} 1` + "\n"
namePrefixWithTag := `TestHistogram_bucket{tag="foo",vmrange="1.2e2...1.4e2"} 1` + "\n"
if !strings.Contains(result, namePrefixWithTag) {
t.Fatalf("missing histogram %s in the WritePrometheus output; got\n%s", namePrefixWithTag, result)
}

View File

@ -141,6 +141,6 @@ func testMarshalTo(t *testing.T, m metric, prefix, resultExpected string) {
m.marshalTo(prefix, &bb)
result := bb.String()
if result != resultExpected {
t.Fatalf("unexpected marshaled metric; got %q; want %q", result, resultExpected)
t.Fatalf("unexpected marshaled metric;\ngot\n%q\nwant\n%q", result, resultExpected)
}
}