[BUGFIX] PromQL: Corrects the behaviour of some operator and aggregators with Native Histograms (#15245)
Some checks failed
buf.build / lint and publish (push) Has been cancelled
CI / Go tests (push) Has been cancelled
CI / More Go tests (push) Has been cancelled
CI / Go tests with previous Go version (push) Has been cancelled
CI / UI tests (push) Has been cancelled
CI / Go tests on Windows (push) Has been cancelled
CI / Mixins tests (push) Has been cancelled
CI / Build Prometheus for common architectures (0) (push) Has been cancelled
CI / Build Prometheus for common architectures (1) (push) Has been cancelled
CI / Build Prometheus for common architectures (2) (push) Has been cancelled
CI / Build Prometheus for all architectures (0) (push) Has been cancelled
CI / Build Prometheus for all architectures (1) (push) Has been cancelled
CI / Build Prometheus for all architectures (10) (push) Has been cancelled
CI / Build Prometheus for all architectures (11) (push) Has been cancelled
CI / Build Prometheus for all architectures (2) (push) Has been cancelled
CI / Build Prometheus for all architectures (3) (push) Has been cancelled
CI / Build Prometheus for all architectures (4) (push) Has been cancelled
CI / Build Prometheus for all architectures (5) (push) Has been cancelled
CI / Build Prometheus for all architectures (6) (push) Has been cancelled
CI / Build Prometheus for all architectures (7) (push) Has been cancelled
CI / Build Prometheus for all architectures (8) (push) Has been cancelled
CI / Build Prometheus for all architectures (9) (push) Has been cancelled
CI / Check generated parser (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
CI / fuzzing (push) Has been cancelled
CI / codeql (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CI / Report status of build Prometheus for all architectures (push) Has been cancelled
CI / Publish main branch artifacts (push) Has been cancelled
CI / Publish release artefacts (push) Has been cancelled
CI / Publish UI on npm Registry (push) Has been cancelled

PromQL: Correct the behaviour of some operator and aggregators with Native Histograms

---------

Signed-off-by: Neeraj Gartia <neerajgartia211002@gmail.com>
This commit is contained in:
Neeraj Gartia 2024-11-12 20:07:05 +05:30 committed by GitHub
parent c599d37668
commit 789c9b1a5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 173 additions and 67 deletions

View file

@ -2793,77 +2793,84 @@ func scalarBinop(op parser.ItemType, lhs, rhs float64) float64 {
// vectorElemBinop evaluates a binary operation between two Vector elements.
func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram.FloatHistogram, pos posrange.PositionRange) (float64, *histogram.FloatHistogram, bool, error) {
switch op {
case parser.ADD:
if hlhs != nil && hrhs != nil {
res, err := hlhs.Copy().Add(hrhs)
if err != nil {
return 0, nil, false, err
opName := parser.ItemTypeStr[op]
switch {
case hlhs == nil && hrhs == nil:
{
switch op {
case parser.ADD:
return lhs + rhs, nil, true, nil
case parser.SUB:
return lhs - rhs, nil, true, nil
case parser.MUL:
return lhs * rhs, nil, true, nil
case parser.DIV:
return lhs / rhs, nil, true, nil
case parser.POW:
return math.Pow(lhs, rhs), nil, true, nil
case parser.MOD:
return math.Mod(lhs, rhs), nil, true, nil
case parser.EQLC:
return lhs, nil, lhs == rhs, nil
case parser.NEQ:
return lhs, nil, lhs != rhs, nil
case parser.GTR:
return lhs, nil, lhs > rhs, nil
case parser.LSS:
return lhs, nil, lhs < rhs, nil
case parser.GTE:
return lhs, nil, lhs >= rhs, nil
case parser.LTE:
return lhs, nil, lhs <= rhs, nil
case parser.ATAN2:
return math.Atan2(lhs, rhs), nil, true, nil
}
return 0, res.Compact(0), true, nil
}
if hlhs == nil && hrhs == nil {
return lhs + rhs, nil, true, nil
}
if hlhs != nil {
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("histogram", "+", "float", pos)
}
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("float", "+", "histogram", pos)
case parser.SUB:
if hlhs != nil && hrhs != nil {
res, err := hlhs.Copy().Sub(hrhs)
if err != nil {
return 0, nil, false, err
case hlhs == nil && hrhs != nil:
{
switch op {
case parser.MUL:
return 0, hrhs.Copy().Mul(lhs).Compact(0), true, nil
case parser.ADD, parser.SUB, parser.DIV, parser.POW, parser.MOD, parser.EQLC, parser.NEQ, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2:
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("float", opName, "histogram", pos)
}
return 0, res.Compact(0), true, nil
}
if hlhs == nil && hrhs == nil {
return lhs - rhs, nil, true, nil
}
if hlhs != nil {
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("histogram", "-", "float", pos)
}
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("float", "-", "histogram", pos)
case parser.MUL:
if hlhs != nil && hrhs == nil {
return 0, hlhs.Copy().Mul(rhs), true, nil
}
if hlhs == nil && hrhs != nil {
return 0, hrhs.Copy().Mul(lhs), true, nil
}
if hlhs != nil && hrhs != nil {
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("histogram", "*", "histogram", pos)
}
return lhs * rhs, nil, true, nil
case parser.DIV:
if hlhs != nil && hrhs == nil {
return 0, hlhs.Copy().Div(rhs), true, nil
}
if hrhs != nil {
if hlhs != nil {
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("histogram", "/", "histogram", pos)
case hlhs != nil && hrhs == nil:
{
switch op {
case parser.MUL:
return 0, hlhs.Copy().Mul(rhs).Compact(0), true, nil
case parser.DIV:
return 0, hlhs.Copy().Div(rhs).Compact(0), true, nil
case parser.ADD, parser.SUB, parser.POW, parser.MOD, parser.EQLC, parser.NEQ, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2:
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("histogram", opName, "float", pos)
}
}
case hlhs != nil && hrhs != nil:
{
switch op {
case parser.ADD:
res, err := hlhs.Copy().Add(hrhs)
if err != nil {
return 0, nil, false, err
}
return 0, res.Compact(0), true, nil
case parser.SUB:
res, err := hlhs.Copy().Sub(hrhs)
if err != nil {
return 0, nil, false, err
}
return 0, res.Compact(0), true, nil
case parser.EQLC:
// This operation expects that both histograms are compacted.
return 0, hlhs, hlhs.Equals(hrhs), nil
case parser.NEQ:
// This operation expects that both histograms are compacted.
return 0, hlhs, !hlhs.Equals(hrhs), nil
case parser.MUL, parser.DIV, parser.POW, parser.MOD, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2:
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("histogram", opName, "histogram", pos)
}
return 0, nil, false, annotations.NewIncompatibleTypesInBinOpInfo("float", "/", "histogram", pos)
}
return lhs / rhs, nil, true, nil
case parser.POW:
return math.Pow(lhs, rhs), nil, true, nil
case parser.MOD:
return math.Mod(lhs, rhs), nil, true, nil
case parser.EQLC:
return lhs, nil, lhs == rhs, nil
case parser.NEQ:
return lhs, nil, lhs != rhs, nil
case parser.GTR:
return lhs, nil, lhs > rhs, nil
case parser.LSS:
return lhs, nil, lhs < rhs, nil
case parser.GTE:
return lhs, nil, lhs >= rhs, nil
case parser.LTE:
return lhs, nil, lhs <= rhs, nil
case parser.ATAN2:
return math.Atan2(lhs, rhs), nil, true, nil
}
panic(fmt.Errorf("operator %q not allowed for operations between Vectors", op))
}
@ -2925,16 +2932,34 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
case h != nil:
// Ignore histograms for STDVAR and STDDEV.
group.seen = false
if op == parser.STDVAR {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("stdvar", e.Expr.PositionRange()))
} else {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("stddev", e.Expr.PositionRange()))
}
case math.IsNaN(f), math.IsInf(f, 0):
group.floatValue = math.NaN()
default:
group.floatValue = 0
}
case parser.QUANTILE:
if h != nil {
group.seen = false
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("quantile", e.Expr.PositionRange()))
}
group.heap = make(vectorByValueHeap, 1)
group.heap[0] = Sample{F: f}
case parser.GROUP:
group.floatValue = 1
case parser.MIN, parser.MAX:
if h != nil {
group.seen = false
if op == parser.MIN {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("min", e.Expr.PositionRange()))
} else {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("max", e.Expr.PositionRange()))
}
}
}
continue
}
@ -3033,11 +3058,19 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
// Do nothing. Required to avoid the panic in `default:` below.
case parser.MAX:
if h != nil {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("min", e.Expr.PositionRange()))
continue
}
if group.floatValue < f || math.IsNaN(group.floatValue) {
group.floatValue = f
}
case parser.MIN:
if h != nil {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("max", e.Expr.PositionRange()))
continue
}
if group.floatValue > f || math.IsNaN(group.floatValue) {
group.floatValue = f
}
@ -3051,9 +3084,18 @@ func (ev *evaluator) aggregation(e *parser.AggregateExpr, q float64, inputMatrix
delta := f - group.floatMean
group.floatMean += delta / group.groupCount
group.floatValue += delta * (f - group.floatMean)
if op == parser.STDVAR {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("stdvar", e.Expr.PositionRange()))
} else {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("stddev", e.Expr.PositionRange()))
}
}
case parser.QUANTILE:
if h != nil {
annos.Add(annotations.NewHistogramIgnoredInAggregationInfo("quantile", e.Expr.PositionRange()))
continue
}
group.heap = append(group.heap, Sample{F: f})
default:

View file

@ -229,13 +229,28 @@ load 5m
http_requests{job="api-server", instance="0", group="canary"} NaN
http_requests{job="api-server", instance="1", group="canary"} 3
http_requests{job="api-server", instance="2", group="canary"} 4
http_requests_histogram{job="api-server", instance="3", group="canary"} {{schema:2 count:4 sum:10 buckets:[1 0 0 0 1 0 0 1 1]}}
eval instant at 0m max(http_requests)
{} 4
# The histogram is ignored here so the result doesn't change but it has an info annotation now.
eval_info instant at 0m max({job="api-server"})
{} 4
# The histogram is ignored here so there is no result but it has an info annotation now.
eval_info instant at 0m max(http_requests_histogram)
eval instant at 0m min(http_requests)
{} 1
# The histogram is ignored here so the result doesn't change but it has an info annotation now.
eval_info instant at 0m min({job="api-server"})
{} 1
# The histogram is ignored here so there is no result but it has an info annotation now.
eval_info instant at 0m min(http_requests_histogram)
eval instant at 0m max by (group) (http_requests)
{group="production"} 2
{group="canary"} 4
@ -380,6 +395,7 @@ load 10s
data{test="uneven samples",point="a"} 0
data{test="uneven samples",point="b"} 1
data{test="uneven samples",point="c"} 4
data_histogram{test="histogram sample", point="c"} {{schema:2 count:4 sum:10 buckets:[1 0 0 0 1 0 0 1 1]}}
foo .8
eval instant at 1m quantile without(point)(0.8, data)
@ -387,6 +403,15 @@ eval instant at 1m quantile without(point)(0.8, data)
{test="three samples"} 1.6
{test="uneven samples"} 2.8
# The histogram is ignored here so the result doesn't change but it has an info annotation now.
eval_info instant at 1m quantile without(point)(0.8, {__name__=~"data(_histogram)?"})
{test="two samples"} 0.8
{test="three samples"} 1.6
{test="uneven samples"} 2.8
# The histogram is ignored here so there is no result but it has an info annotation now.
eval_info instant at 1m quantile(0.8, data_histogram)
# Bug #5276.
eval instant at 1m quantile without(point)(scalar(foo), data)
{test="two samples"} 0.8
@ -581,12 +606,18 @@ load 5m
series{label="b"} 2
series{label="c"} {{schema:1 sum:15 count:10 buckets:[3 2 5 7 9]}}
eval instant at 0m stddev(series)
# The histogram is ignored here so the result doesn't change but it has an info annotation now.
eval_info instant at 0m stddev(series)
{} 0.5
eval instant at 0m stdvar(series)
eval_info instant at 0m stdvar(series)
{} 0.25
# The histogram is ignored here so there is no result but it has an info annotation now.
eval_info instant at 0m stddev({label="c"})
eval_info instant at 0m stdvar({label="c"})
eval instant at 0m stddev by (label) (series)
{label="a"} 0
{label="b"} 0

View file

@ -1184,3 +1184,5 @@ eval instant at 3m sum_over_time(histogram_sum_over_time[4m:1m])
eval instant at 3m avg_over_time(histogram_sum_over_time[4m:1m])
{} {{schema:0 count:26.75 sum:1172.8 z_bucket:3.5 z_bucket_w:0.001 buckets:[0.75 2 0.5 1.25 0.75 0.5 0.5] n_buckets:[0.5 1.5 2 1 3.75 2.25 0 0 0 2.5 2.5 1]}}
clear

View file

@ -7,6 +7,7 @@ load 5m
http_requests{job="app-server", instance="1", group="production"} 0+60x10
http_requests{job="app-server", instance="0", group="canary"} 0+70x10
http_requests{job="app-server", instance="1", group="canary"} 0+80x10
http_requests_histogram{job="app-server", instance="1", group="production"} {{schema:1 sum:15 count:10 buckets:[3 2 5 7 9]}}x11
load 5m
vector_matching_a{l="x"} 0+1x100
@ -287,6 +288,26 @@ eval instant at 50m 1 == bool 1
eval instant at 50m http_requests{job="api-server", instance="0", group="production"} == bool 100
{job="api-server", instance="0", group="production"} 1
# The histogram is ignored here so the result doesn't change but it has an info annotation now.
eval_info instant at 5m {job="app-server"} == 80
http_requests{group="canary", instance="1", job="app-server"} 80
eval_info instant at 5m http_requests_histogram != 80
eval_info instant at 5m http_requests_histogram > 80
eval_info instant at 5m http_requests_histogram < 80
eval_info instant at 5m http_requests_histogram >= 80
eval_info instant at 5m http_requests_histogram <= 80
# Should produce valid results in case of (in)equality between two histograms.
eval instant at 5m http_requests_histogram == http_requests_histogram
http_requests_histogram{job="app-server", instance="1", group="production"} {{schema:1 sum:15 count:10 buckets:[3 2 5 7 9]}}
eval instant at 5m http_requests_histogram != http_requests_histogram
# group_left/group_right.
clear

View file

@ -147,6 +147,7 @@ var (
PossibleNonCounterInfo = fmt.Errorf("%w: metric might not be a counter, name does not end in _total/_sum/_count/_bucket:", PromQLInfo)
HistogramQuantileForcedMonotonicityInfo = fmt.Errorf("%w: input to histogram_quantile needed to be fixed for monotonicity (see https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile) for metric name", PromQLInfo)
IncompatibleTypesInBinOpInfo = fmt.Errorf("%w: incompatible sample types encountered for binary operator", PromQLInfo)
HistogramIgnoredInAggregationInfo = fmt.Errorf("%w: ignored histogram in", PromQLInfo)
)
type annoErr struct {
@ -283,3 +284,12 @@ func NewIncompatibleTypesInBinOpInfo(lhsType, operator, rhsType string, pos posr
Err: fmt.Errorf("%w %q: %s %s %s", IncompatibleTypesInBinOpInfo, operator, lhsType, operator, rhsType),
}
}
// NewHistogramIgnoredInAggregationInfo is used when a histogram is ignored by
// an aggregation operator that cannot handle histograms.
func NewHistogramIgnoredInAggregationInfo(aggregation string, pos posrange.PositionRange) error {
return annoErr{
PositionRange: pos,
Err: fmt.Errorf("%w %s aggregation", HistogramIgnoredInAggregationInfo, aggregation),
}
}