promql: Implement </ and >/ operators for trimming native histograms.

This implements the TRIM_UPPER (</) and TRIM_LOWER (>/) operators
that allow removing observations below or above a threshold from
a histogram. The implementation zeros out buckets outside the desired
range. It also recalculates the sum, including only bucket counts within
the specified threshold range.

Fixes #14651.

Signed-off-by: sujal shah <sujalshah28092004@gmail.com>
This commit is contained in:
sujal shah 2025-03-27 04:24:18 +05:30 committed by Linas Medziunas
parent d9ccd70ac1
commit e8bfcfcf1a
12 changed files with 1156 additions and 711 deletions

View file

@ -3140,6 +3140,301 @@ func scalarBinop(op parser.ItemType, lhs, rhs float64) float64 {
panic(fmt.Errorf("operator %q not allowed for Scalar operations", op))
}
// processCustomBucket handles custom bucket processing for histogram trimming.
// It returns the count to keep and the bucket midpoint for sum calculations.
func processCustomBucket(
bucket histogram.Bucket[float64],
rhs float64,
op parser.ItemType,
) (keepCount, bucketMidpoint float64) {
// Midpoint calculation
switch {
case math.IsInf(bucket.Lower, -1):
// First bucket: no lower bound, assume midpoint is near upper bound.
bucketMidpoint = bucket.Upper
case math.IsInf(bucket.Upper, 1):
bucketMidpoint = bucket.Lower
default:
bucketMidpoint = (bucket.Lower + bucket.Upper) / 2
}
// Fractional keepCount calculation
switch op {
case parser.TRIM_UPPER:
switch {
case math.IsInf(bucket.Lower, -1):
// Special case for -Inf lower bound
if rhs >= bucket.Upper {
// Trim point is above bucket upper bound, keep all
keepCount = bucket.Count
} else {
// Trim point is within bucket or below, keep none
keepCount = 0
}
case math.IsInf(bucket.Upper, 1):
// Special case for +Inf upper bound
if rhs <= bucket.Lower {
// Trim point is below bucket lower bound, keep none
keepCount = 0
} else {
// Trim point is within the bucket, keep a portion
// Since we can't interpolate with +Inf, assume keep half for simplicity
// Another approach would be to use a different interpolation scheme
keepCount = bucket.Count * 0.5
}
default:
// Normal case - finite bounds
switch {
case bucket.Upper <= rhs:
// Bucket entirely below trim point - keep all
keepCount = bucket.Count
case bucket.Lower < rhs:
// Bucket contains trim point - interpolate
fraction := (rhs - bucket.Lower) / (bucket.Upper - bucket.Lower)
keepCount = bucket.Count * fraction
default:
// Bucket entirely above trim point - discard
keepCount = 0
}
}
case parser.TRIM_LOWER:
switch {
case math.IsInf(bucket.Upper, 1):
// Special case for +Inf upper bound
if rhs <= bucket.Lower {
keepCount = bucket.Count
} else {
keepCount = 0
}
case math.IsInf(bucket.Lower, -1):
// Special case for -Inf lower bound
if rhs >= bucket.Upper {
keepCount = 0
} else {
keepCount = bucket.Count * 0.5
}
default:
switch {
case bucket.Lower >= rhs:
keepCount = bucket.Count
case bucket.Upper > rhs:
fraction := (bucket.Upper - rhs) / (bucket.Upper - bucket.Lower)
keepCount = bucket.Count * fraction
default:
keepCount = 0
}
}
}
return keepCount, bucketMidpoint
}
func computeBucketTrim(op parser.ItemType, bucket histogram.Bucket[float64], rhs float64, isPostive, isCustomBucket bool) (float64, float64) {
if isCustomBucket {
return processCustomBucket(bucket, rhs, op)
}
return computeExponentialTrim(bucket, rhs, isPostive, op)
}
// Helper function to trim native histogram buckets.
func trimHistogram(trimmedHist *histogram.FloatHistogram, rhs float64, op parser.ItemType) {
updatedCount := 0.0
origSum := trimmedHist.Sum
removedSum := 0.0
hasPositive, hasNegative := false, false
isCustomBucket := trimmedHist.UsesCustomBuckets()
// Calculate the fraction to keep for buckets that contain the trim value
// For TRIM_UPPER, we keep observations below the trim point (rhs)
switch op {
case parser.TRIM_UPPER:
for i, iter := 0, trimmedHist.PositiveBucketIterator(); iter.Next(); i++ {
hasPositive = true
bucket := iter.At()
var keepCount, bucketMidpoint float64
keepCount, bucketMidpoint = computeBucketTrim(op, bucket, rhs, true, isCustomBucket)
// Bucket is entirely below the trim point - keep all
switch {
case bucket.Upper <= rhs:
updatedCount += bucket.Count
case bucket.Lower < rhs:
// Bucket contains the trim point - interpolate
removedCount := bucket.Count - keepCount
removedMid := bucketMidpoint
removedSum += removedCount * removedMid
updatedCount += keepCount
trimmedHist.PositiveBuckets[i] = keepCount
default:
if !isCustomBucket {
bucketMidpoint = math.Sqrt(bucket.Lower * bucket.Upper)
}
removedSum += bucket.Count * bucketMidpoint
// Bucket is entirely above the trim point - discard
trimmedHist.PositiveBuckets[i] = 0
}
}
for i, iter := 0, trimmedHist.NegativeBucketIterator(); iter.Next(); i++ {
hasNegative = true
bucket := iter.At()
var keepCount, bucketMidpoint float64
keepCount, bucketMidpoint = computeBucketTrim(op, bucket, rhs, false, isCustomBucket)
switch {
case bucket.Upper <= rhs:
updatedCount += bucket.Count
case bucket.Lower < rhs:
removedCount := bucket.Count - keepCount
removedMid := bucketMidpoint
removedSum += removedCount * removedMid
trimmedHist.NegativeBuckets[i] = keepCount
updatedCount += keepCount
default:
bucketMidpoint = math.Sqrt(bucket.Lower * bucket.Upper)
removedSum += bucket.Count * bucketMidpoint
trimmedHist.NegativeBuckets[i] = 0
}
}
// For TRIM_LOWER, we keep observations above the trim point (rhs)
case parser.TRIM_LOWER:
for i, iter := 0, trimmedHist.PositiveBucketIterator(); iter.Next(); i++ {
hasPositive = true
bucket := iter.At()
var keepCount, bucketMidpoint float64
keepCount, bucketMidpoint = computeBucketTrim(op, bucket, rhs, true, isCustomBucket)
switch {
case bucket.Lower >= rhs:
updatedCount += bucket.Count
case bucket.Upper > rhs:
removedCount := bucket.Count - keepCount
removedMid := bucketMidpoint
removedSum += removedCount * removedMid
trimmedHist.PositiveBuckets[i] = keepCount
updatedCount += keepCount
default:
if !isCustomBucket {
bucketMidpoint = math.Sqrt(bucket.Lower * bucket.Upper)
}
removedSum += bucket.Count * bucketMidpoint
trimmedHist.PositiveBuckets[i] = 0
}
}
for i, iter := 0, trimmedHist.NegativeBucketIterator(); iter.Next(); i++ {
hasNegative = true
bucket := iter.At()
var keepCount, bucketMidpoint float64
keepCount, bucketMidpoint = computeBucketTrim(op, bucket, rhs, false, isCustomBucket)
switch {
case bucket.Lower >= rhs:
updatedCount += bucket.Count
case bucket.Upper > rhs:
removedCount := bucket.Count - keepCount
removedMid := bucketMidpoint
removedSum += removedCount * removedMid
trimmedHist.NegativeBuckets[i] = keepCount
updatedCount += keepCount
default:
bucketMidpoint = math.Sqrt(bucket.Lower * bucket.Upper)
removedSum += bucket.Count * bucketMidpoint
trimmedHist.NegativeBuckets[i] = 0
}
}
}
// Handle the zero count bucket
if trimmedHist.ZeroCount > 0 {
zeroBucket := trimmedHist.ZeroBucket()
zLower := zeroBucket.Lower
zUpper := zeroBucket.Upper
switch op {
case parser.TRIM_UPPER:
switch {
case rhs < zLower:
trimmedHist.ZeroCount = 0
case rhs > zUpper:
updatedCount += trimmedHist.ZeroCount
default:
fraction := (rhs - zLower) / (zUpper - zLower)
keepCount := trimmedHist.ZeroCount * fraction
trimmedHist.ZeroCount = keepCount
updatedCount += keepCount
}
case parser.TRIM_LOWER:
switch {
case rhs > zUpper:
trimmedHist.ZeroCount = 0
case rhs < zLower:
updatedCount += trimmedHist.ZeroCount
default:
fraction := (zUpper - rhs) / (zUpper - zLower)
keepCount := trimmedHist.ZeroCount * fraction
trimmedHist.ZeroCount = keepCount
updatedCount += keepCount
}
}
}
// Apply new sum
newSum := origSum - removedSum
// Clamp correction
if !hasNegative && newSum < 0 {
newSum = 0
}
if !hasPositive && newSum > 0 {
newSum = 0
}
// Update the histogram's count and sum
trimmedHist.Count = updatedCount
trimmedHist.Sum = newSum
trimmedHist.Compact(0)
}
func computeExponentialTrim(bucket histogram.Bucket[float64], rhs float64, isPositive bool, op parser.ItemType) (float64, float64) {
var fraction, bucketMidpoint, keepCount float64
logLower := math.Log2(math.Abs(bucket.Lower))
logUpper := math.Log2(math.Abs(bucket.Upper))
logRHS := math.Log2(math.Abs(rhs))
switch op {
case parser.TRIM_UPPER:
if isPositive {
fraction = (logRHS - logLower) / (logUpper - logLower)
bucketMidpoint = math.Sqrt(bucket.Lower * rhs)
} else {
fraction = 1 - ((logRHS - logUpper) / (logLower - logUpper))
bucketMidpoint = -math.Sqrt(math.Abs(bucket.Lower) * math.Abs(rhs))
}
case parser.TRIM_LOWER:
if isPositive {
fraction = (logUpper - logRHS) / (logUpper - logLower)
bucketMidpoint = math.Sqrt(rhs * bucket.Upper)
} else {
fraction = (logRHS - logUpper) / (logLower - logUpper)
bucketMidpoint = -math.Sqrt(math.Abs(rhs) * math.Abs(bucket.Upper))
}
}
keepCount = bucket.Count * fraction
return keepCount, bucketMidpoint
}
// vectorElemBinop evaluates a binary operation between two Vector elements.
func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram.FloatHistogram, pos posrange.PositionRange) (res float64, resH *histogram.FloatHistogram, keep bool, info, err error) {
switch {
@ -3172,6 +3467,8 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram
return lhs, nil, lhs <= rhs, nil, nil
case parser.ATAN2:
return math.Atan2(lhs, rhs), nil, true, nil, nil
case parser.TRIM_LOWER, parser.TRIM_UPPER:
return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("float", parser.ItemTypeStr[op], "float", pos)
}
}
case hlhs == nil && hrhs != nil:
@ -3179,7 +3476,7 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram
switch op {
case parser.MUL:
return 0, hrhs.Copy().Mul(lhs).Compact(0), true, nil, 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:
case parser.ADD, parser.SUB, parser.DIV, parser.POW, parser.MOD, parser.EQLC, parser.NEQ, parser.GTR, parser.TRIM_LOWER, parser.TRIM_UPPER, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2:
return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("float", parser.ItemTypeStr[op], "histogram", pos)
}
}
@ -3190,6 +3487,14 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram
return 0, hlhs.Copy().Mul(rhs).Compact(0), true, nil, nil
case parser.DIV:
return 0, hlhs.Copy().Div(rhs).Compact(0), true, nil, nil
case parser.TRIM_UPPER:
trimmedHist := hlhs.Copy()
trimHistogram(trimmedHist, rhs, op)
return 0, trimmedHist, true, nil, nil
case parser.TRIM_LOWER:
trimmedHist := hlhs.Copy()
trimHistogram(trimmedHist, rhs, op)
return 0, trimmedHist, true, nil, 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, nil, annotations.NewIncompatibleTypesInBinOpInfo("histogram", parser.ItemTypeStr[op], "float", pos)
}
@ -3230,7 +3535,7 @@ func vectorElemBinop(op parser.ItemType, lhs, rhs float64, hlhs, hrhs *histogram
case parser.NEQ:
// This operation expects that both histograms are compacted.
return 0, hlhs, !hlhs.Equals(hrhs), nil, nil
case parser.MUL, parser.DIV, parser.POW, parser.MOD, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2:
case parser.MUL, parser.DIV, parser.POW, parser.MOD, parser.GTR, parser.LSS, parser.GTE, parser.LTE, parser.ATAN2, parser.TRIM_LOWER, parser.TRIM_UPPER:
return 0, nil, false, nil, annotations.NewIncompatibleTypesInBinOpInfo("histogram", parser.ItemTypeStr[op], "histogram", pos)
}
}

View file

@ -98,6 +98,8 @@ EQLC
EQL_REGEX
GTE
GTR
TRIM_UPPER
TRIM_LOWER
LAND
LOR
LSS
@ -200,7 +202,7 @@ START_METRIC_SELECTOR
// Operators are listed with increasing precedence.
%left LOR
%left LAND LUNLESS
%left EQLC GTE GTR LSS LTE NEQ
%left EQLC GTE GTR LSS LTE NEQ TRIM_UPPER TRIM_LOWER
%left ADD SUB
%left MUL DIV MOD ATAN2
%right POW
@ -291,6 +293,8 @@ binary_expr : expr ADD bin_modifier expr { $$ = yylex.(*parser).newBinar
| expr EQLC bin_modifier expr { $$ = yylex.(*parser).newBinaryExpression($1, $2, $3, $4) }
| expr GTE bin_modifier expr { $$ = yylex.(*parser).newBinaryExpression($1, $2, $3, $4) }
| expr GTR bin_modifier expr { $$ = yylex.(*parser).newBinaryExpression($1, $2, $3, $4) }
| expr TRIM_UPPER bin_modifier expr { $$ = yylex.(*parser).newBinaryExpression($1, $2, $3, $4) }
| expr TRIM_LOWER bin_modifier expr { $$ = yylex.(*parser).newBinaryExpression($1, $2, $3, $4) }
| expr LAND bin_modifier expr { $$ = yylex.(*parser).newBinaryExpression($1, $2, $3, $4) }
| expr LOR bin_modifier expr { $$ = yylex.(*parser).newBinaryExpression($1, $2, $3, $4) }
| expr LSS bin_modifier expr { $$ = yylex.(*parser).newBinaryExpression($1, $2, $3, $4) }

File diff suppressed because it is too large Load diff

View file

@ -189,21 +189,23 @@ var ItemTypeStr = map[ItemType]string{
TIMES: "x",
SPACE: "<space>",
SUB: "-",
ADD: "+",
MUL: "*",
MOD: "%",
DIV: "/",
EQLC: "==",
NEQ: "!=",
LTE: "<=",
LSS: "<",
GTE: ">=",
GTR: ">",
EQL_REGEX: "=~",
NEQ_REGEX: "!~",
POW: "^",
AT: "@",
SUB: "-",
ADD: "+",
MUL: "*",
MOD: "%",
DIV: "/",
EQLC: "==",
NEQ: "!=",
LTE: "<=",
LSS: "<",
GTE: ">=",
GTR: ">",
TRIM_UPPER: "</",
TRIM_LOWER: ">/",
EQL_REGEX: "=~",
NEQ_REGEX: "!~",
POW: "^",
AT: "@",
}
func init() {
@ -446,6 +448,9 @@ func lexStatements(l *Lexer) stateFn {
if t := l.peek(); t == '=' {
l.next()
l.emit(LTE)
} else if t := l.peek(); t == '/' {
l.next()
l.emit(TRIM_UPPER)
} else {
l.emit(LSS)
}
@ -453,6 +458,9 @@ func lexStatements(l *Lexer) stateFn {
if t := l.peek(); t == '=' {
l.next()
l.emit(GTE)
} else if t := l.peek(); t == '/' {
l.next()
l.emit(TRIM_LOWER)
} else {
l.emit(GTR)
}

View file

@ -1872,3 +1872,58 @@ eval instant at 1m irate(nhcb_add_bucket[2m]) * 60
expect no_warn
expect no_info
{} {{schema:-53 sum:56 count:15 custom_values:[2 3 4 6] buckets:[1 0 1 5 8] counter_reset_hint:gauge}}
# Test native histogram with trim operators ("</": TRIM_UPPER, ">/": TRIM_LOWER)
load 1m
h_test {{schema:0 sum:123.75 count:34 z_bucket:1 z_bucket_w:0.001 buckets:[2 4 8 16] n_buckets:[1 2]}}
h_test_2 {{schema:2 sum:12.8286080906 count:28 z_bucket:1 z_bucket_w:0.001 buckets:[1 2 4 7 3] n_buckets:[1 5 3 1]}}
cbh {{schema:-53 sum:172.5 count:15 custom_values:[5 10 15 20] buckets:[1 6 4 3 1]}}
zero_bucket {{schema:0 sum:-6.75 z_bucket:5 z_bucket_w:0.01 buckets:[2 3] n_buckets:[1 2 3]}}
# Native Histogram: Exponential Bucket Interpolation Tests
eval instant at 1m h_test_2 </ 1.13
{__name__="h_test_2"} {{schema:2 count:13.410582181123704 sum:-9.282809901015558 z_bucket:1 z_bucket_w:0.001 buckets:[1 1.410582181123704] n_buckets:[1 5 3 1]}}
eval instant at 1m h_test_2 >/ 1.13
{__name__="h_test_2"} {{schema:2 count:14.589417818876296 sum:-1.5258511531197865 z_bucket_w:0.001 offset:1 buckets:[0.589417818876296 4 7 3]}}
eval instant at 1m h_test_2 >/ -1.3
{__name__="h_test_2"} {{schema:2 count:25.54213947904476 sum:13.099057472672072 z_bucket:1 z_bucket_w:0.001 buckets:[1 2 4 7 3] n_buckets:[1 5 1.54213947904476]}}
eval instant at 1m h_test_2 </ -1.3
{__name__="h_test_2"} {{schema:2 count:2.45786052095524 sum:-16.03281816946792 z_bucket_w:0.001 n_offset:2 n_buckets:[1.45786052095524 1]}}
# Native Histogram: Linear Bucket Trimming Tests
eval instant at 1m h_test </ 2
{__name__="h_test"} {{count:10 sum:10.612915010152392 z_bucket:1 z_bucket_w:0.001 buckets:[2 4] n_buckets:[1 2]}}
eval instant at 1m h_test >/ 2
{__name__="h_test"} {{count:24 sum:113.14339828220179 z_bucket_w:0.001 offset:2 buckets:[8 16]}}
eval instant at 1m h_test >/ -1
{__name__="h_test"} {{count:32 sum:120.92157287525382 z_bucket:1 z_bucket_w:0.001 buckets:[2 4 8 16] n_buckets:[1]}}
eval instant at 1m h_test </ -1
{__name__="h_test"} {{count:2 sum:2.834740417100363 z_bucket_w:0.001 n_offset:1 n_buckets:[2]}}
# Custom Buckets: Trim Operation Tests
eval instant at 1m cbh </ 13
{__name__="cbh"} {{schema:-53 count:9.4 sum:80 custom_values:[5 10 15 20] buckets:[1 6 2.4]}}
eval instant at 1m cbh >/ 13
{__name__="cbh"} {{schema:-53 count:5.6 sum:92.5 custom_values:[5 10 15 20] offset:2 buckets:[1.6 3 1]}}
eval instant at 1m cbh </ 15
{__name__="cbh"} {{schema:-53 count:11 sum:100 custom_values:[5 10 15 20] buckets:[1 6 4]}}
eval instant at 1m cbh >/ 15
{__name__="cbh"} {{schema:-53 count:4 sum:72.5 custom_values:[5 10 15 20] offset:3 buckets:[3 1]}}
# Zero Bucket Edge Case: Interpolation Around Zero
eval instant at 1m zero_bucket </ -0.005
{__name__="zero_bucket"} {{count:7.25 sum:-12.40685424949238 z_bucket:1.25 z_bucket_w:0.01 n_buckets:[1 2 3]}}
eval instant at 1m zero_bucket >/ 0
{__name__="zero_bucket"} {{count:7.5 sum:-18.77081528017131 z_bucket:2.5 z_bucket_w:0.01 buckets:[2 3]}}
clear

View file

@ -40,6 +40,8 @@ export enum binaryOperatorType {
neq = "!=",
gtr = ">",
lss = "<",
trimUpper = "</",
trimLower = ">/",
gte = ">=",
lte = "<=",
and = "and",

View file

@ -37,6 +37,8 @@ const binOpPrecedence = {
[binaryOperatorType.lss]: 4,
[binaryOperatorType.gte]: 4,
[binaryOperatorType.lte]: 4,
[binaryOperatorType.trimLower]: 4,
[binaryOperatorType.trimUpper]: 4,
[binaryOperatorType.and]: 5,
[binaryOperatorType.or]: 6,
[binaryOperatorType.unless]: 5,

View file

@ -60,6 +60,8 @@ import {
LimitK,
LimitRatio,
CountValues,
TrimLower,
TrimUpper,
} from '@prometheus-io/lezer-promql';
import { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import { EditorState } from '@codemirror/state';
@ -408,7 +410,7 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode, pos: num
// BinaryExpr( ..., Gtr , ... )
// When the `bool` keyword is present, then the expression looks like this:
// BinaryExpr( ..., Gtr , BoolModifier(...), ... )
if (containsAtLeastOneChild(parent, Eql, Gte, Gtr, Lte, Lss, Neq) && !containsAtLeastOneChild(parent, BoolModifier)) {
if (containsAtLeastOneChild(parent, Eql, Gte, Gtr, TrimLower, TrimUpper, Lte, Lss, Neq) && !containsAtLeastOneChild(parent, BoolModifier)) {
result.push({ kind: ContextKind.Bool });
}
}
@ -579,6 +581,8 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode, pos: num
case Eql:
case Gte:
case Gtr:
case TrimLower:
case TrimUpper:
case Lte:
case Lss:
case And:

View file

@ -26,6 +26,8 @@ export const binOpTerms = [
{ label: '>=' },
{ label: '>' },
{ label: '<' },
{ label: '</' },
{ label: '>/' },
{ label: '<=' },
{ label: '!=' },
{ label: 'atan2' },

View file

@ -49,12 +49,14 @@ import {
StepInvariantExpr,
SubqueryExpr,
Topk,
TrimLower,
TrimUpper,
UnaryExpr,
Unless,
UnquotedLabelMatcher,
VectorSelector,
} from '@prometheus-io/lezer-promql';
import { containsAtLeastOneChild } from './path-finder';
import { containsAtLeastOneChild, containsChild } from './path-finder';
import { getType } from './type';
import { buildLabelMatchers } from './matcher';
import { EditorState } from '@codemirror/state';
@ -214,17 +216,25 @@ export class Parser {
const lt = this.checkAST(lExpr);
const rt = this.checkAST(rExpr);
const boolModifierUsed = node.getChild(BoolModifier);
const isComparisonOperator = containsAtLeastOneChild(node, Eql, Neq, Lte, Lss, Gte, Gtr);
const isComparisonOperator = containsAtLeastOneChild(node, Eql, Neq, Lte, Lss, Gte, Gtr, TrimLower, TrimUpper);
const isTrimLowerOperator = containsChild(node, TrimLower);
const isTrimUpperOperator = containsChild(node, TrimUpper);
const isSetOperator = containsAtLeastOneChild(node, And, Or, Unless);
// BOOL modifier check
if (boolModifierUsed) {
if (!isComparisonOperator) {
if (!isComparisonOperator || isTrimLowerOperator || isTrimUpperOperator) {
this.addDiagnostic(node, 'bool modifier can only be used on comparison operators');
}
} else {
if (isComparisonOperator && lt === ValueType.scalar && rt === ValueType.scalar) {
this.addDiagnostic(node, 'comparisons between scalars must use BOOL modifier');
if (isTrimLowerOperator) {
this.addDiagnostic(node, 'operator ">/" not allowed for Scalar operations');
} else if (isTrimUpperOperator) {
this.addDiagnostic(node, 'operator "</" not allowed for Scalar operations');
} else {
this.addDiagnostic(node, 'comparisons between scalars must use BOOL modifier');
}
}
}

View file

@ -88,6 +88,8 @@ BinaryExpr {
expr !eql Eql binModifiers expr |
expr !eql Gte binModifiers expr |
expr !eql Gtr binModifiers expr |
expr !eql TrimUpper binModifiers expr |
expr !eql TrimLower binModifiers expr |
expr !eql Lte binModifiers expr |
expr !eql Lss binModifiers expr |
expr !eql Neq binModifiers expr |
@ -337,6 +339,8 @@ NumberDurationLiteralInDurationContext {
Lss { "<" }
Gte { ">=" }
Gtr { ">" }
TrimUpper { "</" }
TrimLower { ">/" }
EqlRegex { "=~" }
EqlSingle { "=" }
NeqRegex { "!~" }

View file

@ -716,3 +716,31 @@ rate(caddy_http_requests_total[5m] smoothed)
==>
PromQL(FunctionCall(FunctionIdentifier(Rate),FunctionCallBody(SmoothedExpr(MatrixSelector(VectorSelector(Identifier),NumberDurationLiteralInDurationContext),Smoothed))))
# TrimUpper binary operator
metric1 </ metric2
==>
PromQL(
BinaryExpr(
VectorSelector(Identifier),
TrimUpper,
VectorSelector(Identifier)
)
)
# TrimLower binary operator
metric1 >/ metric2
==>
PromQL(
BinaryExpr(
VectorSelector(Identifier),
TrimLower,
VectorSelector(Identifier)
)
)