diff --git a/promql/quantile.go b/promql/quantile.go index 28d78f4c47..7a010a5672 100644 --- a/promql/quantile.go +++ b/promql/quantile.go @@ -75,16 +75,18 @@ func bucketQuantile(q float64, buckets buckets) float64 { if q > 1 { return math.Inf(+1) } - if len(buckets) < 2 { - return math.NaN() - } sort.Sort(buckets) if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) { return math.NaN() } + buckets = coalesceBuckets(buckets) ensureMonotonic(buckets) + if len(buckets) < 2 { + return math.NaN() + } + rank := q * buckets[len(buckets)-1].count b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank }) @@ -107,6 +109,25 @@ func bucketQuantile(q float64, buckets buckets) float64 { return bucketStart + (bucketEnd-bucketStart)*(rank/count) } +// coalesceBuckets merges buckets with the same upper bound. +// +// The input buckets must be sorted. +func coalesceBuckets(buckets buckets) buckets { + last := buckets[0] + i := 0 + for _, b := range buckets[1:] { + if b.upperBound == last.upperBound { + last.count += b.count + } else { + buckets[i] = last + last = b + i++ + } + } + buckets[i] = last + return buckets[:i+1] +} + // The assumption that bucket counts increase monotonically with increasing // upperBound may be violated during: // diff --git a/promql/testdata/histograms.test b/promql/testdata/histograms.test index b1e76ab63e..594295c56e 100644 --- a/promql/testdata/histograms.test +++ b/promql/testdata/histograms.test @@ -31,6 +31,15 @@ load 5m request_duration_seconds_bucket{job="job2", instance="ins2", le="0.2"} 0+7x10 request_duration_seconds_bucket{job="job2", instance="ins2", le="+Inf"} 0+9x10 +# Different le representations in one histogram. +load 5m + mixed_bucket{job="job1", instance="ins1", le="0.1"} 0+1x10 + mixed_bucket{job="job1", instance="ins1", le="0.2"} 0+1x10 + mixed_bucket{job="job1", instance="ins1", le="2e-1"} 0+1x10 + mixed_bucket{job="job1", instance="ins1", le="2.0e-1"} 0+1x10 + mixed_bucket{job="job1", instance="ins1", le="+Inf"} 0+4x10 + mixed_bucket{job="job1", instance="ins2", le="+inf"} 0+0x10 + mixed_bucket{job="job1", instance="ins2", le="+Inf"} 0+0x10 # Quantile too low. eval instant at 50m histogram_quantile(-0.1, testhistogram_bucket) @@ -157,3 +166,16 @@ load 5m # Nonmonotonic buckets eval instant at 50m histogram_quantile(0.99, nonmonotonic_bucket) {} 0.989875 + +# Buckets with different representations of the same upper bound. +eval instant at 50m histogram_quantile(0.5, rate(mixed_bucket[5m])) + {instance="ins1", job="job1"} 0.15 + {instance="ins2", job="job1"} NaN + +eval instant at 50m histogram_quantile(0.75, rate(mixed_bucket[5m])) + {instance="ins1", job="job1"} 0.2 + {instance="ins2", job="job1"} NaN + +eval instant at 50m histogram_quantile(1, rate(mixed_bucket[5m])) + {instance="ins1", job="job1"} 0.2 + {instance="ins2", job="job1"} NaN