add min/max/count info to annotations

Signed-off-by: Jeanette Tan <jeanette.tan@grafana.com>
pull/14339/head
Jeanette Tan 5 months ago
parent 3d54bcc018
commit 437babc1f3

@ -1301,13 +1301,13 @@ func funcHistogramQuantile(vals []parser.Value, args parser.Expressions, enh *Ev
for _, mb := range enh.signatureToMetricWithBuckets {
if len(mb.buckets) > 0 {
res, forcedMonotonicity, _ := bucketQuantile(q, mb.buckets)
res, forcedMonotonicMinBucket, forcedMonotonicMaxBucket, forcedMonotonicMaxDiff, forcedMonotonicity, _ := bucketQuantile(q, mb.buckets)
enh.Out = append(enh.Out, Sample{
Metric: mb.metric,
F: res,
})
if forcedMonotonicity {
annos.Add(annotations.NewHistogramQuantileForcedMonotonicityInfo(mb.metric.Get(labels.MetricName), args[1].PositionRange()))
annos.Add(annotations.NewHistogramQuantileForcedMonotonicityInfo(mb.metric.Get(labels.MetricName), args[1].PositionRange(), enh.Ts, forcedMonotonicMinBucket, forcedMonotonicMaxBucket, forcedMonotonicMaxDiff))
}
}
}

@ -95,15 +95,15 @@ type metricWithBuckets struct {
// and another bool to indicate if small differences between buckets (that
// are likely artifacts of floating point precision issues) have been
// ignored.
func bucketQuantile(q float64, buckets buckets) (float64, bool, bool) {
func bucketQuantile(q float64, buckets buckets) (float64, float64, float64, float64, bool, bool) {
if math.IsNaN(q) {
return math.NaN(), false, false
return math.NaN(), 0, 0, 0, false, false
}
if q < 0 {
return math.Inf(-1), false, false
return math.Inf(-1), 0, 0, 0, false, false
}
if q > 1 {
return math.Inf(+1), false, false
return math.Inf(+1), 0, 0, 0, false, false
}
slices.SortFunc(buckets, func(a, b bucket) int {
// We don't expect the bucket boundary to be a NaN.
@ -116,39 +116,42 @@ func bucketQuantile(q float64, buckets buckets) (float64, bool, bool) {
return 0
})
if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) {
return math.NaN(), false, false
return math.NaN(), 0, 0, 0, false, false
}
buckets = coalesceBuckets(buckets)
forcedMonotonic, fixedPrecision := ensureMonotonicAndIgnoreSmallDeltas(buckets, smallDeltaTolerance)
forcedMonotonicMinBucket, forcedMonotonicMaxBucket, forcedMonotonicMaxDiff, forcedMonotonic, fixedPrecision := ensureMonotonicAndIgnoreSmallDeltas(buckets, smallDeltaTolerance)
if len(buckets) < 2 {
return math.NaN(), false, false
return math.NaN(), 0, 0, 0, false, false
}
observations := buckets[len(buckets)-1].count
if observations == 0 {
return math.NaN(), false, false
return math.NaN(), 0, 0, 0, false, false
}
rank := q * observations
b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })
if b == len(buckets)-1 {
return buckets[len(buckets)-2].upperBound, forcedMonotonic, fixedPrecision
}
if b == 0 && buckets[0].upperBound <= 0 {
return buckets[0].upperBound, forcedMonotonic, fixedPrecision
}
var (
bucketStart float64
bucketEnd = buckets[b].upperBound
count = buckets[b].count
)
if b > 0 {
bucketStart = buckets[b-1].upperBound
count -= buckets[b-1].count
rank -= buckets[b-1].count
var res float64
switch {
case b == len(buckets)-1:
res = buckets[len(buckets)-2].upperBound
case b == 0 && buckets[0].upperBound <= 0:
res = buckets[0].upperBound
default:
var (
bucketStart float64
bucketEnd = buckets[b].upperBound
count = buckets[b].count
)
if b > 0 {
bucketStart = buckets[b-1].upperBound
count -= buckets[b-1].count
rank -= buckets[b-1].count
}
res = bucketStart + (bucketEnd-bucketStart)*(rank/count)
}
return bucketStart + (bucketEnd-bucketStart)*(rank/count), forcedMonotonic, fixedPrecision
return res, forcedMonotonicMinBucket, forcedMonotonicMaxBucket, forcedMonotonicMaxDiff, forcedMonotonic, fixedPrecision
}
// histogramQuantile calculates the quantile 'q' based on the given histogram.
@ -403,8 +406,11 @@ func coalesceBuckets(buckets buckets) buckets {
//
// We return a bool to indicate if this monotonicity was forced or not, and
// another bool to indicate if small deltas were ignored or not.
func ensureMonotonicAndIgnoreSmallDeltas(buckets buckets, tolerance float64) (bool, bool) {
func ensureMonotonicAndIgnoreSmallDeltas(buckets buckets, tolerance float64) (float64, float64, float64, bool, bool) {
var forcedMonotonic, fixedPrecision bool
var forcedMonotonicMinBucket, forcedMonotonicMaxBucket, forcedMonotonicMaxDiff float64
forcedMonotonicMinBucket = math.Inf(+1)
forcedMonotonicMaxBucket = math.Inf(-1)
prev := buckets[0].count
for i := 1; i < len(buckets); i++ {
curr := buckets[i].count // Assumed always positive.
@ -425,11 +431,20 @@ func ensureMonotonicAndIgnoreSmallDeltas(buckets buckets, tolerance float64) (bo
// Do not update the 'prev' value as we are ignoring the decrease.
buckets[i].count = prev
forcedMonotonic = true
if buckets[i].upperBound < forcedMonotonicMinBucket {
forcedMonotonicMinBucket = buckets[i].upperBound
}
if buckets[i].upperBound > forcedMonotonicMaxBucket {
forcedMonotonicMaxBucket = buckets[i].upperBound
}
if diff := prev - curr; diff > forcedMonotonicMaxDiff {
forcedMonotonicMaxDiff = diff
}
continue
}
prev = curr
}
return forcedMonotonic, fixedPrecision
return forcedMonotonicMinBucket, forcedMonotonicMaxBucket, forcedMonotonicMaxDiff, forcedMonotonic, fixedPrecision
}
// quantile calculates the given quantile of a vector of samples.

@ -308,7 +308,7 @@ func TestBucketQuantile_ForcedMonotonicity(t *testing.T) {
} {
t.Run(name, func(t *testing.T) {
for q, v := range tc.expectedValues {
res, forced, fixed := bucketQuantile(q, tc.getInput())
res, _, _, _, forced, fixed := bucketQuantile(q, tc.getInput())
require.Equal(t, tc.expectedForced, forced)
require.Equal(t, tc.expectedFixed, fixed)
require.InEpsilon(t, v, res, eps)

@ -16,6 +16,7 @@ package annotations
import (
"errors"
"fmt"
"time"
"github.com/prometheus/common/model"
@ -42,6 +43,10 @@ func (a *Annotations) Add(err error) Annotations {
if *a == nil {
*a = Annotations{}
}
prevErr, exists := (*a)[err.Error()]
if exists {
err = merge(prevErr, err)
}
(*a)[err.Error()] = err
return *a
}
@ -56,6 +61,10 @@ func (a *Annotations) Merge(aa Annotations) Annotations {
*a = Annotations{}
}
for key, val := range aa {
prevVal, exists := (*a)[key]
if exists {
val = merge(prevVal, val)
}
(*a)[key] = val
}
return *a
@ -107,6 +116,28 @@ func (a Annotations) CountWarningsAndInfo() (int, int) {
return countWarnings, countInfo
}
// merge tries to merge two annoErrs into one but only if they have the same structure,
// i.e. the same error and the same number of min and max values, otherwise it just returns
// the second error.
func merge(a, b error) error {
var aErr, bErr annoErr
if errors.As(a, &aErr) && errors.As(b, &bErr) && aErr.Err.Error() == bErr.Err.Error() && len(aErr.Min) == len(bErr.Min) && len(aErr.Max) == len(bErr.Max) {
for i, aMin := range aErr.Min {
if aMin < bErr.Min[i] {
bErr.Min[i] = aMin
}
}
for i, aMax := range aErr.Max {
if aMax > bErr.Max[i] {
bErr.Max[i] = aMax
}
}
bErr.Count += aErr.Count + 1
return bErr
}
return b
}
//nolint:revive // error-naming.
var (
// Currently there are only 2 types, warnings and info.
@ -133,12 +164,20 @@ type annoErr struct {
PositionRange posrange.PositionRange
Err error
Query string
Min []float64
Max []float64
Count int
}
func (e annoErr) Error() string {
if e.Query == "" {
return e.Err.Error()
}
if errors.Is(e.Err, HistogramQuantileForcedMonotonicityInfo) {
startTime := time.Unix(int64(e.Min[0]/1000), 0).Format(time.RFC3339)
endTime := time.Unix(int64(e.Max[0]/1000), 0).Format(time.RFC3339)
return fmt.Sprintf("%s, from buckets %.2f to %.2f, with a max diff of %.2f, over %d samples from %s to %s (%s)", e.Err, e.Min[1], e.Max[1], e.Max[2], e.Count+1, startTime, endTime, e.PositionRange.StartPosInput(e.Query, 0))
}
return fmt.Sprintf("%s (%s)", e.Err, e.PositionRange.StartPosInput(e.Query, 0))
}
@ -239,9 +278,12 @@ func NewPossibleNonCounterInfo(metricName string, pos posrange.PositionRange) er
// NewHistogramQuantileForcedMonotonicityInfo is used when the input (classic histograms) to
// histogram_quantile needs to be forced to be monotonic.
func NewHistogramQuantileForcedMonotonicityInfo(metricName string, pos posrange.PositionRange) error {
func NewHistogramQuantileForcedMonotonicityInfo(metricName string, pos posrange.PositionRange, ts int64, forcedMonotonicMinBucket, forcedMonotonicMaxBucket, forcedMonotonicMaxDiff float64) error {
floatTs := float64(ts)
return annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %q", HistogramQuantileForcedMonotonicityInfo, metricName),
Min: []float64{floatTs, forcedMonotonicMinBucket},
Max: []float64{floatTs, forcedMonotonicMaxBucket, forcedMonotonicMaxDiff},
}
}

Loading…
Cancel
Save