From 130cd024e0f3faec1631e121c6305219eb874252 Mon Sep 17 00:00:00 2001 From: Neeraj Gartia <80708727+NeerajGartia21@users.noreply.github.com> Date: Sat, 1 Feb 2025 03:41:03 +0530 Subject: [PATCH] [FEATURE] PromQL: Implements `idelta` and `irate` with histograms (#15853) Add native histogram support to idelta and irate functions --------- Signed-off-by: Neeraj Gartia --- promql/functions.go | 103 ++++++++++++++++++---- promql/promqltest/testdata/functions.test | 56 ++++++++++++ 2 files changed, 142 insertions(+), 17 deletions(-) diff --git a/promql/functions.go b/promql/functions.go index 605661e5a0..938eefdf00 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -286,46 +286,115 @@ func funcIncrease(vals []parser.Value, args parser.Expressions, enh *EvalNodeHel // === irate(node parser.ValueTypeMatrix) (Vector, Annotations) === func funcIrate(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { - return instantValue(vals, enh.Out, true) + return instantValue(vals, args, enh.Out, true) } // === idelta(node model.ValMatrix) (Vector, Annotations) === func funcIdelta(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { - return instantValue(vals, enh.Out, false) + return instantValue(vals, args, enh.Out, false) } -func instantValue(vals []parser.Value, out Vector, isRate bool) (Vector, annotations.Annotations) { - samples := vals[0].(Matrix)[0] +func instantValue(vals []parser.Value, args parser.Expressions, out Vector, isRate bool) (Vector, annotations.Annotations) { + var ( + samples = vals[0].(Matrix)[0] + metricName = samples.Metric.Get(labels.MetricName) + ss = make([]Sample, 0, 2) + annos annotations.Annotations + ) + // No sense in trying to compute a rate without at least two points. Drop // this Vector element. // TODO: add RangeTooShortWarning - if len(samples.Floats) < 2 { + if len(samples.Floats)+len(samples.Histograms) < 2 { return out, nil } - lastSample := samples.Floats[len(samples.Floats)-1] - previousSample := samples.Floats[len(samples.Floats)-2] - - var resultValue float64 - if isRate && lastSample.F < previousSample.F { - // Counter reset. - resultValue = lastSample.F - } else { - resultValue = lastSample.F - previousSample.F + // Add the last 2 float samples if they exist. + for i := max(0, len(samples.Floats)-2); i < len(samples.Floats); i++ { + ss = append(ss, Sample{ + F: samples.Floats[i].F, + T: samples.Floats[i].T, + }) } - sampledInterval := lastSample.T - previousSample.T + // Add the last 2 histogram samples into their correct position if they exist. + for i := max(0, len(samples.Histograms)-2); i < len(samples.Histograms); i++ { + s := Sample{ + H: samples.Histograms[i].H, + T: samples.Histograms[i].T, + } + switch { + case len(ss) == 0: + ss = append(ss, s) + case len(ss) == 1: + if s.T < ss[0].T { + ss = append([]Sample{s}, ss...) + } else { + ss = append(ss, s) + } + case s.T < ss[0].T: + // s is older than 1st, so discard it. + case s.T > ss[1].T: + // s is newest, so add it as 2nd and make the old 2nd the new 1st. + ss[0] = ss[1] + ss[1] = s + default: + // In all other cases, we just make s the new 1st. + // This establishes a correct order, even in the (irregular) + // case of equal timestamps. + ss[0] = s + } + } + + resultSample := ss[1] + sampledInterval := ss[1].T - ss[0].T if sampledInterval == 0 { // Avoid dividing by 0. return out, nil } + switch { + case ss[1].H == nil && ss[0].H == nil: + if !isRate || ss[1].F >= ss[0].F { + // Gauge or counter without reset. + resultSample.F = ss[1].F - ss[0].F + } + // In case of a counter reset, we leave resultSample at + // its current value, which is already ss[1]. + case ss[1].H != nil && ss[0].H != nil: + resultSample.H = ss[1].H.Copy() + // irate should only be applied to counters. + if isRate && (ss[1].H.CounterResetHint == histogram.GaugeType || ss[0].H.CounterResetHint == histogram.GaugeType) { + annos.Add(annotations.NewNativeHistogramNotCounterWarning(metricName, args.PositionRange())) + } + // idelta should only be applied to gauges. + if !isRate && (ss[1].H.CounterResetHint != histogram.GaugeType || ss[0].H.CounterResetHint != histogram.GaugeType) { + annos.Add(annotations.NewNativeHistogramNotGaugeWarning(metricName, args.PositionRange())) + } + if !isRate || !ss[1].H.DetectReset(ss[0].H) { + _, err := resultSample.H.Sub(ss[0].H) + if errors.Is(err, histogram.ErrHistogramsIncompatibleSchema) { + return out, annos.Add(annotations.NewMixedExponentialCustomHistogramsWarning(metricName, args.PositionRange())) + } else if errors.Is(err, histogram.ErrHistogramsIncompatibleBounds) { + return out, annos.Add(annotations.NewIncompatibleCustomBucketsHistogramsWarning(metricName, args.PositionRange())) + } + } + resultSample.H.CounterResetHint = histogram.GaugeType + resultSample.H.Compact(0) + default: + // Mix of a float and a histogram. + return out, annos.Add(annotations.NewMixedFloatsHistogramsWarning(metricName, args.PositionRange())) + } if isRate { // Convert to per-second. - resultValue /= float64(sampledInterval) / 1000 + if resultSample.H == nil { + resultSample.F /= float64(sampledInterval) / 1000 + } else { + resultSample.H.Div(float64(sampledInterval) / 1000) + } } - return append(out, Sample{F: resultValue}), nil + return append(out, resultSample), annos } // Calculate the trend value at the given index i in raw data d. diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index 9bcb7e6945..32838afe73 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -218,6 +218,13 @@ clear load 5m http_requests_total{path="/foo"} 0+10x10 http_requests_total{path="/bar"} 0+10x5 0+10x5 + http_requests_histogram{path="/a"} {{sum:2 count:2}}+{{sum:3 count:3}}x5 + http_requests_histogram{path="/b"} 0 0 {{sum:1 count:1}} {{sum:4 count:4}} + http_requests_histogram{path="/c"} 0 0 {{sum:1 count:1}} {{sum:4 count:4 counter_reset_hint:gauge}} + http_requests_histogram{path="/d"} 0 0 {{sum:1 count:1 counter_reset_hint:gauge}} {{sum:4 count:4}} + http_requests_histogram{path="/e"} 0 1 2 {{sum:4 count:4}} + http_requests_histogram{path="/f"} 0 0 {{sum:1 count:1}} {{schema:-53 sum:3 count:3 custom_values:[5 10] buckets:[3]}} + http_requests_histogram{path="/g"} 0 0 {{schema:-53 sum:3 count:3 custom_values:[1] buckets:[3]}} {{schema:-53 sum:3 count:3 custom_values:[5 10] buckets:[3]}} eval instant at 50m irate(http_requests_total[50m]) {path="/foo"} .03333333333333333333 @@ -228,6 +235,28 @@ eval instant at 30m irate(http_requests_total[50m]) {path="/foo"} .03333333333333333333 {path="/bar"} 0 +eval instant at 20m irate(http_requests_histogram{path="/a"}[20m]) + {path="/a"} {{sum:0.01 count:0.01 counter_reset_hint:gauge}} + +eval instant at 20m irate(http_requests_histogram{path="/b"}[20m]) + {path="/b"} {{sum:0.01 count:0.01 counter_reset_hint:gauge}} + +eval instant at 20m irate(http_requests_histogram{path="/b"}[6m]) + +eval_warn instant at 20m irate(http_requests_histogram{path="/c"}[20m]) + {path="/c"} {{sum:0.01 count:0.01 counter_reset_hint:gauge}} + +eval_warn instant at 20m irate(http_requests_histogram{path="/d"}[20m]) + {path="/d"} {{sum:0.01 count:0.01 counter_reset_hint:gauge}} + +eval_warn instant at 20m irate(http_requests_histogram{path="/e"}[20m]) + +eval instant at 20m irate(http_requests_histogram{path="/f"}[20m]) + {path="/f"} {{schema:-53 sum:0.01 count:0.01 custom_values:[5 10] buckets:[0.01]}} + +eval instant at 20m irate(http_requests_histogram{path="/g"}[20m]) + {path="/g"} {{schema:-53 sum:0.01 count:0.01 custom_values:[5 10] buckets:[0.01]}} + clear # Tests for delta(). @@ -259,11 +288,38 @@ clear load 5m http_requests{path="/foo"} 0 50 100 150 http_requests{path="/bar"} 0 50 100 50 + http_requests_histogram{path="/a"} {{sum:2 count:2 counter_reset_hint:gauge}}+{{sum:1 count:3 counter_reset_hint:gauge}}x5 + http_requests_histogram{path="/b"} 0 0 {{sum:1 count:1 counter_reset_hint:gauge}} {{sum:2 count:2 counter_reset_hint:gauge}} + http_requests_histogram{path="/c"} 0 0 {{sum:1 count:1}} {{sum:2 count:2 counter_reset_hint:gauge}} + http_requests_histogram{path="/d"} 0 0 {{sum:1 count:1 counter_reset_hint:gauge}} {{sum:2 count:2}} + http_requests_histogram{path="/e"} 0 1 2 {{sum:1 count:2 counter_reset_hint:gauge}} + http_requests_histogram{path="/f"} 0 0 {{sum:1 count:1 counter_reset_hint:gauge}} {{schema:-53 sum:1 count:1 custom_values:[5 10] buckets:[1] counter_reset_hint:gauge}} + http_requests_histogram{path="/g"} 0 0 {{schema:-53 sum:1 count:1 custom_values:[1] buckets:[2] counter_reset_hint:gauge}} {{schema:-53 sum:1 count:1 custom_values:[5 10] buckets:[1] counter_reset_hint:gauge}} eval instant at 20m idelta(http_requests[20m]) {path="/foo"} 50 {path="/bar"} -50 +eval instant at 20m idelta(http_requests_histogram{path="/a"}[20m]) + {path="/a"} {{sum:1 count:3 counter_reset_hint:gauge}} + +eval instant at 20m idelta(http_requests_histogram{path="/b"}[20m]) + {path="/b"} {{sum:1 count:1 counter_reset_hint:gauge}} + +eval instant at 20m idelta(http_requests_histogram{path="/b"}[6m]) + +eval_warn instant at 20m idelta(http_requests_histogram{path="/c"}[20m]) + {path="/c"} {{sum:1 count:1 counter_reset_hint:gauge}} + +eval_warn instant at 20m idelta(http_requests_histogram{path="/d"}[20m]) + {path="/d"} {{sum:1 count:1 counter_reset_hint:gauge}} + +eval_warn instant at 20m idelta(http_requests_histogram{path="/e"}[20m]) + +eval_warn instant at 20m idelta(http_requests_histogram{path="/f"}[20m]) + +eval_warn instant at 20m idelta(http_requests_histogram{path="/g"}[20m]) + clear # Tests for deriv() and predict_linear().