This commit is contained in:
Michael Hoffmann 2025-03-05 21:56:18 +01:00 committed by GitHub
commit cd2b39d348
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 152 additions and 3 deletions

View file

@ -1433,16 +1433,65 @@ func funcHistogramStdVar(vals []parser.Value, _ parser.Expressions, enh *EvalNod
} }
// === histogram_fraction(lower, upper parser.ValueTypeScalar, Vector parser.ValueTypeVector) (Vector, Annotations) === // === histogram_fraction(lower, upper parser.ValueTypeScalar, Vector parser.ValueTypeVector) (Vector, Annotations) ===
func funcHistogramFraction(vals []parser.Value, _ parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { func funcHistogramFraction(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
lower := vals[0].(Vector)[0].F lower := vals[0].(Vector)[0].F
upper := vals[1].(Vector)[0].F upper := vals[1].(Vector)[0].F
inVec := vals[2].(Vector) inVec := vals[2].(Vector)
if enh.signatureToMetricWithBuckets == nil {
enh.signatureToMetricWithBuckets = map[string]*metricWithBuckets{}
} else {
for _, v := range enh.signatureToMetricWithBuckets {
v.buckets = v.buckets[:0]
}
}
var (
annos annotations.Annotations
histogramSamples []Sample
)
for _, sample := range inVec { for _, sample := range inVec {
// Skip non-histogram samples. // We are only looking for classic buckets here. Remember
if sample.H == nil { // the histograms for later treatment.
if sample.H != nil {
histogramSamples = append(histogramSamples, sample)
continue continue
} }
upperBound, err := strconv.ParseFloat(
sample.Metric.Get(model.BucketLabel), 64,
)
if err != nil {
annos.Add(annotations.NewBadBucketLabelWarning(sample.Metric.Get(labels.MetricName), sample.Metric.Get(model.BucketLabel), args[2].PositionRange()))
continue
}
enh.lblBuf = sample.Metric.BytesWithoutLabels(enh.lblBuf, labels.BucketLabel)
mb, ok := enh.signatureToMetricWithBuckets[string(enh.lblBuf)]
if !ok {
sample.Metric = labels.NewBuilder(sample.Metric).
Del(excludedLabels...).
Labels()
mb = &metricWithBuckets{sample.Metric, nil}
enh.signatureToMetricWithBuckets[string(enh.lblBuf)] = mb
}
mb.buckets = append(mb.buckets, Bucket{upperBound, sample.F})
}
// Now deal with the native histograms.
for _, sample := range histogramSamples {
// We have to reconstruct the exact same signature as above for
// a classic histogram, just ignoring any le label.
enh.lblBuf = sample.Metric.Bytes(enh.lblBuf)
if mb, ok := enh.signatureToMetricWithBuckets[string(enh.lblBuf)]; ok && len(mb.buckets) > 0 {
// At this data point, we have classic histogram
// buckets and a native histogram with the same name and
// labels. Do not evaluate anything.
annos.Add(annotations.NewMixedClassicNativeHistogramsWarning(sample.Metric.Get(labels.MetricName), args[1].PositionRange()))
delete(enh.signatureToMetricWithBuckets, string(enh.lblBuf))
continue
}
if !enh.enableDelayedNameRemoval { if !enh.enableDelayedNameRemoval {
sample.Metric = sample.Metric.DropMetricName() sample.Metric = sample.Metric.DropMetricName()
} }
@ -1452,6 +1501,23 @@ func funcHistogramFraction(vals []parser.Value, _ parser.Expressions, enh *EvalN
DropName: true, DropName: true,
}) })
} }
// Now do classic histograms that have already been filtered for conflicting native histograms.
for _, mb := range enh.signatureToMetricWithBuckets {
if len(mb.buckets) == 0 {
continue
}
if !enh.enableDelayedNameRemoval {
mb.metric = mb.metric.DropMetricName()
}
enh.Out = append(enh.Out, Sample{
Metric: mb.metric,
F: BucketFraction(lower, upper, mb.buckets),
DropName: true,
})
}
return enh.Out, nil return enh.Out, nil
} }

View file

@ -113,6 +113,11 @@ eval instant at 50m histogram_fraction(0, 0.2, rate(testhistogram3[10m]))
{start="positive"} 0.6363636363636364 {start="positive"} 0.6363636363636364
{start="negative"} 0 {start="negative"} 0
eval instant at 50m histogram_fraction(0, 0.2, rate(testhistogram3_bucket[10m]))
{start="positive"} 0.6363636363636364
{start="negative"} 0
# In the classic histogram, we can access the corresponding bucket (if # In the classic histogram, we can access the corresponding bucket (if
# it exists) and divide by the count to get the same result. # it exists) and divide by the count to get the same result.

View file

@ -448,6 +448,84 @@ func HistogramFraction(lower, upper float64, h *histogram.FloatHistogram) float6
return (upperRank - lowerRank) / h.Count return (upperRank - lowerRank) / h.Count
} }
// BucketFraction is a version of HistogramFraction for classic histograms.
func BucketFraction(lower, upper float64, buckets Buckets) float64 {
slices.SortFunc(buckets, func(a, b Bucket) int {
// We don't expect the bucket boundary to be a NaN.
if a.UpperBound < b.UpperBound {
return -1
}
if a.UpperBound > b.UpperBound {
return +1
}
return 0
})
if !math.IsInf(buckets[len(buckets)-1].UpperBound, +1) {
return math.NaN()
}
buckets = coalesceBuckets(buckets)
count := buckets[len(buckets)-1].Count
if count == 0 || math.IsNaN(lower) || math.IsNaN(upper) {
return math.NaN()
}
if lower >= upper {
return 0
}
var (
rank, lowerRank, upperRank float64
lowerSet, upperSet bool
)
for i, b := range buckets {
lowerBound := math.Inf(-1)
if i > 0 {
lowerBound = buckets[i-1].UpperBound
}
upperBound := b.UpperBound
interpolateLinearly := func(v float64) float64 {
return rank + b.Count*(v-lowerBound)/(upperBound-lowerBound)
}
if !lowerSet && lowerBound >= lower {
// We have hit the lower value at the lower bucket boundary.
lowerRank = rank
lowerSet = true
}
if !upperSet && lowerBound >= upper {
// We have hit the upper value at the lower bucket boundary.
upperRank = rank
upperSet = true
}
if lowerSet && upperSet {
break
}
if !lowerSet && lowerBound < lower && upperBound > lower {
// The lower value is in this bucket.
lowerRank = interpolateLinearly(lower)
lowerSet = true
}
if !upperSet && lowerBound < upper && upperBound > upper {
// The upper value is in this bucket.
upperRank = interpolateLinearly(upper)
upperSet = true
}
if lowerSet && upperSet {
break
}
rank = b.Count
}
if !lowerSet || lowerRank > count {
lowerRank = count
}
if !upperSet || upperRank > count {
upperRank = count
}
return (upperRank - lowerRank) / count
}
// coalesceBuckets merges buckets with the same upper bound. // coalesceBuckets merges buckets with the same upper bound.
// //
// The input buckets must be sorted. // The input buckets must be sorted.