Merge pull request #17904 from linasm/trim_histogram
Some checks are pending
buf.build / lint and publish (push) Waiting to run
CI / Go tests (push) Waiting to run
CI / More Go tests (push) Waiting to run
CI / Go tests with previous Go version (push) Waiting to run
CI / UI tests (push) Waiting to run
CI / Go tests on Windows (push) Waiting to run
CI / Mixins tests (push) Waiting to run
CI / Compliance testing (push) Waiting to run
CI / Build Prometheus for common architectures (push) Waiting to run
CI / Build Prometheus for all architectures (push) Waiting to run
CI / Report status of build Prometheus for all architectures (push) Blocked by required conditions
CI / Check generated parser (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / fuzzing (push) Waiting to run
CI / codeql (push) Waiting to run
CI / Publish main branch artifacts (push) Blocked by required conditions
CI / Publish release artefacts (push) Blocked by required conditions
CI / Publish UI on npm Registry (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

promql: Implement </ and >/ operators for trimming native histograms
This commit is contained in:
George Krajcsovits 2026-02-24 17:16:24 +01:00 committed by GitHub
commit 5d3f9ee39b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1508 additions and 708 deletions

View file

@ -145,6 +145,8 @@
"=~": true,
">": true,
">=": true,
">/": true,
"</": true,
"@": true,
"^": true,
"and": true,

View file

@ -126,6 +126,25 @@ samples. Operations involving histogram samples result in the removal of the
corresponding vector elements from the output vector, flagged by an
info-level annotation.
### Histogram trim operators
The following binary histogram trim operators exist in Prometheus:
* `</` (trim upper): removes all observations above a threshold value
* `>/` (trim lower): removes all observations below a threshold value
Histogram trim operators are defined between vector/scalar and vector/vector value pairs,
where the left hand side is a native histogram (either exponential or NHCB),
and the right hand side is a float threshold value.
In case the threshold value is not aligned to one of the bucket boundaries of the histogram,
either linear (for NHCB and zero buckets of exponential histogram) or exponential (for non zero
bucket of exponential histogram) interpolation is applied to compute the estimated count
of observations that remain in the bucket containing the threshold.
In case when some observations get trimmed, the new sum of observation values is recomputed
(approximately) based on the remaining observations.
### Comparison binary operators
The following binary comparison operators exist in Prometheus:

View file

@ -3184,6 +3184,337 @@ func scalarBinop(op parser.ItemType, lhs, rhs float64) float64 {
panic(fmt.Errorf("operator %q not allowed for Scalar operations", op))
}
func handleInfinityBuckets(isUpperTrim bool, b histogram.Bucket[float64], rhs float64) (underCount, bucketMidpoint float64) {
zeroIfInf := func(x float64) float64 {
if math.IsInf(x, 0) {
return 0
}
return x
}
// Case 1: Bucket with lower bound -Inf.
if math.IsInf(b.Lower, -1) {
// TRIM_UPPER (</) - remove values greater than rhs
if isUpperTrim {
if rhs >= b.Upper {
// As the rhs is greater than the upper bound, we keep the entire current bucket.
return b.Count, 0
}
if rhs > 0 && b.Upper > 0 && !math.IsInf(b.Upper, 1) {
// If upper is finite and positive, we treat lower as 0 (despite it de facto being -Inf).
// This is only possible with NHCB, so we can always use linear interpolation.
return b.Count * rhs / b.Upper, rhs / 2
}
if b.Upper <= 0 {
return b.Count, rhs
}
// Otherwise, we are targeting a valid trim, but as we don't know the exact distribution of values that belongs to an infinite bucket, we need to remove the entire bucket.
return 0, zeroIfInf(b.Upper)
}
// TRIM_LOWER (>/) - remove values less than rhs
if rhs <= b.Lower {
// Impossible to happen because the lower bound is -Inf. Returning the entire current bucket.
return b.Count, 0
}
if rhs >= 0 && b.Upper > rhs && !math.IsInf(b.Upper, 1) {
// If upper is finite and positive, we treat lower as 0 (despite it de facto being -Inf).
// This is only possible with NHCB, so we can always use linear interpolation.
return b.Count * (1 - rhs/b.Upper), (rhs + b.Upper) / 2
}
// Otherwise, we are targeting a valid trim, but as we don't know the exact distribution of values that belongs to an infinite bucket, we need to remove the entire bucket.
return 0, zeroIfInf(b.Upper)
}
// Case 2: Bucket with upper bound +Inf.
if math.IsInf(b.Upper, 1) {
if isUpperTrim {
// TRIM_UPPER (</) - remove values greater than rhs.
// We don't care about lower here, because:
// when rhs >= lower and the bucket extends to +Inf, some values in this bucket could be > rhs, so we conservatively remove the entire bucket;
// when rhs < lower, all values in this bucket are >= lower > rhs, so all values should be removed.
return 0, zeroIfInf(b.Lower)
}
// TRIM_LOWER (>/) - remove values less than rhs.
if rhs >= b.Lower {
return b.Count, rhs
}
// lower < rhs: we are inside the infinity bucket, but as we don't know the exact distribution of values, we conservatively remove the entire bucket.
return 0, zeroIfInf(b.Lower)
}
panic(fmt.Errorf("one of the bounds must be infinite for handleInfinityBuckets, got %v", b))
}
// computeSplit calculates the portion of the bucket's count <= rhs (trim point).
func computeSplit(b histogram.Bucket[float64], rhs float64, isPositive, isLinear bool) float64 {
if rhs <= b.Lower {
return 0
}
if rhs >= b.Upper {
return b.Count
}
var fraction float64
switch {
case isLinear:
fraction = (rhs - b.Lower) / (b.Upper - b.Lower)
default:
// Exponential interpolation.
logLower := math.Log2(math.Abs(b.Lower))
logUpper := math.Log2(math.Abs(b.Upper))
logV := math.Log2(math.Abs(rhs))
if isPositive {
fraction = (logV - logLower) / (logUpper - logLower)
} else {
fraction = 1 - ((logV - logUpper) / (logLower - logUpper))
}
}
return b.Count * fraction
}
func computeZeroBucketTrim(zeroBucket histogram.Bucket[float64], rhs float64, hasNegative, hasPositive, isUpperTrim bool) (float64, float64) {
var (
lower = zeroBucket.Lower
upper = zeroBucket.Upper
)
if hasNegative && !hasPositive {
upper = 0
}
if hasPositive && !hasNegative {
lower = 0
}
var fraction, midpoint float64
if isUpperTrim {
if rhs <= lower {
return 0, 0
}
if rhs >= upper {
return zeroBucket.Count, (lower + upper) / 2
}
fraction = (rhs - lower) / (upper - lower)
midpoint = (lower + rhs) / 2
} else { // lower trim
if rhs <= lower {
return zeroBucket.Count, (lower + upper) / 2
}
if rhs >= upper {
return 0, 0
}
fraction = (upper - rhs) / (upper - lower)
midpoint = (rhs + upper) / 2
}
return zeroBucket.Count * fraction, midpoint
}
func computeBucketTrim(b histogram.Bucket[float64], rhs float64, isUpperTrim, isPositive, isCustomBucket bool) (float64, float64) {
if math.IsInf(b.Lower, -1) || math.IsInf(b.Upper, 1) {
return handleInfinityBuckets(isUpperTrim, b, rhs)
}
underCount := computeSplit(b, rhs, isPositive, isCustomBucket)
if isUpperTrim {
return underCount, computeMidpoint(b.Lower, rhs, isPositive, isCustomBucket)
}
return b.Count - underCount, computeMidpoint(rhs, b.Upper, isPositive, isCustomBucket)
}
// Helper function to trim native histogram buckets.
// TODO: move trimHistogram to model/histogram/float_histogram.go (making it a method of FloatHistogram).
func trimHistogram(trimmedHist *histogram.FloatHistogram, rhs float64, isUpperTrim bool) {
var (
updatedCount, updatedSum float64
trimmedBuckets bool
isCustomBucket = trimmedHist.UsesCustomBuckets()
hasPositive, hasNegative bool
)
if isUpperTrim {
// Calculate the fraction to keep for buckets that contain the trim value.
// For TRIM_UPPER, we keep observations below the trim point (rhs).
// Example: histogram </ float.
for i, iter := 0, trimmedHist.PositiveBucketIterator(); iter.Next(); i++ {
bucket := iter.At()
if bucket.Count == 0 {
continue
}
hasPositive = true
switch {
case bucket.Upper <= rhs:
// Bucket is entirely below the trim point - keep all.
updatedCount += bucket.Count
bucketMidpoint := computeMidpoint(bucket.Lower, bucket.Upper, true, isCustomBucket)
updatedSum += bucketMidpoint * bucket.Count
case bucket.Lower < rhs:
// Bucket contains the trim point - interpolate.
keepCount, bucketMidpoint := computeBucketTrim(bucket, rhs, isUpperTrim, true, isCustomBucket)
updatedCount += keepCount
updatedSum += bucketMidpoint * keepCount
if trimmedHist.PositiveBuckets[i] != keepCount {
trimmedHist.PositiveBuckets[i] = keepCount
trimmedBuckets = true
}
default:
// Bucket is entirely above the trim point - discard.
trimmedHist.PositiveBuckets[i] = 0
trimmedBuckets = true
}
}
for i, iter := 0, trimmedHist.NegativeBucketIterator(); iter.Next(); i++ {
bucket := iter.At()
if bucket.Count == 0 {
continue
}
hasNegative = true
switch {
case bucket.Upper <= rhs:
// Bucket is entirely below the trim point - keep all.
updatedCount += bucket.Count
bucketMidpoint := computeMidpoint(bucket.Lower, bucket.Upper, false, isCustomBucket)
updatedSum += bucketMidpoint * bucket.Count
case bucket.Lower < rhs:
// Bucket contains the trim point - interpolate.
keepCount, bucketMidpoint := computeBucketTrim(bucket, rhs, isUpperTrim, false, isCustomBucket)
updatedCount += keepCount
updatedSum += bucketMidpoint * keepCount
if trimmedHist.NegativeBuckets[i] != keepCount {
trimmedHist.NegativeBuckets[i] = keepCount
trimmedBuckets = true
}
default:
trimmedHist.NegativeBuckets[i] = 0
trimmedBuckets = true
}
}
} else { // !isUpperTrim
// For TRIM_LOWER, we keep observations above the trim point (rhs).
// Example: histogram >/ float.
for i, iter := 0, trimmedHist.PositiveBucketIterator(); iter.Next(); i++ {
bucket := iter.At()
if bucket.Count == 0 {
continue
}
hasPositive = true
switch {
case bucket.Lower >= rhs:
// Bucket is entirely below the trim point - keep all.
updatedCount += bucket.Count
bucketMidpoint := computeMidpoint(bucket.Lower, bucket.Upper, true, isCustomBucket)
updatedSum += bucketMidpoint * bucket.Count
case bucket.Upper > rhs:
// Bucket contains the trim point - interpolate.
keepCount, bucketMidpoint := computeBucketTrim(bucket, rhs, isUpperTrim, true, isCustomBucket)
updatedCount += keepCount
updatedSum += bucketMidpoint * keepCount
if trimmedHist.PositiveBuckets[i] != keepCount {
trimmedHist.PositiveBuckets[i] = keepCount
trimmedBuckets = true
}
default:
trimmedHist.PositiveBuckets[i] = 0
trimmedBuckets = true
}
}
for i, iter := 0, trimmedHist.NegativeBucketIterator(); iter.Next(); i++ {
bucket := iter.At()
if bucket.Count == 0 {
continue
}
hasNegative = true
switch {
case bucket.Lower >= rhs:
// Bucket is entirely below the trim point - keep all.
updatedCount += bucket.Count
bucketMidpoint := computeMidpoint(bucket.Lower, bucket.Upper, false, isCustomBucket)
updatedSum += bucketMidpoint * bucket.Count
case bucket.Upper > rhs:
// Bucket contains the trim point - interpolate.
keepCount, bucketMidpoint := computeBucketTrim(bucket, rhs, isUpperTrim, false, isCustomBucket)
updatedCount += keepCount
updatedSum += bucketMidpoint * keepCount
if trimmedHist.NegativeBuckets[i] != keepCount {
trimmedHist.NegativeBuckets[i] = keepCount
trimmedBuckets = true
}
default:
trimmedHist.NegativeBuckets[i] = 0
trimmedBuckets = true
}
}
}
// Handle the zero count bucket.
if trimmedHist.ZeroCount > 0 {
keepCount, bucketMidpoint := computeZeroBucketTrim(trimmedHist.ZeroBucket(), rhs, hasNegative, hasPositive, isUpperTrim)
if trimmedHist.ZeroCount != keepCount {
trimmedHist.ZeroCount = keepCount
trimmedBuckets = true
}
updatedSum += bucketMidpoint * keepCount
updatedCount += keepCount
}
if trimmedBuckets {
// Only update the totals in case some bucket(s) were fully (or partially) trimmed.
trimmedHist.Count = updatedCount
trimmedHist.Sum = updatedSum
trimmedHist.Compact(0)
}
}
func computeMidpoint(survivingIntervalLowerBound, survivingIntervalUpperBound float64, isPositive, isLinear bool) float64 {
if math.IsInf(survivingIntervalLowerBound, 0) {
if math.IsInf(survivingIntervalUpperBound, 0) {
return 0
}
if survivingIntervalUpperBound > 0 {
return survivingIntervalUpperBound / 2
}
return survivingIntervalUpperBound
} else if math.IsInf(survivingIntervalUpperBound, 0) {
return survivingIntervalLowerBound
}
if isLinear {
return (survivingIntervalLowerBound + survivingIntervalUpperBound) / 2
}
geoMean := math.Sqrt(math.Abs(survivingIntervalLowerBound * survivingIntervalUpperBound))
if isPositive {
return geoMean
}
return -geoMean
}
// 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 {
@ -3216,6 +3547,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:
@ -3223,7 +3556,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)
}
}
@ -3234,6 +3567,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, true)
return 0, trimmedHist, true, nil, nil
case parser.TRIM_LOWER:
trimmedHist := hlhs.Copy()
trimHistogram(trimmedHist, rhs, false)
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)
}
@ -3274,7 +3615,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

@ -2020,3 +2020,356 @@ 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]}}
eval instant at 1m h_test >/ -Inf
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]}}
eval instant at 1m h_test </ +Inf
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]}}
eval instant at 1m h_test >/ +Inf
h_test {{schema:0 z_bucket_w:0.001}}
eval instant at 1m h_test </ -Inf
h_test {{schema:0 z_bucket_w:0.001}}
eval instant at 1m h_test >/ 0
h_test {{schema:0 sum:120.20840280171308 count:30.5 z_bucket:0.5 z_bucket_w:0.001 buckets:[2 4 8 16]}}
eval instant at 1m h_test </ 0
h_test {{schema:0 sum:-3.53578390593273768 count:3.5 z_bucket:0.5 z_bucket_w:0.001 n_buckets:[1 2]}}
# Exponential buckets: trim uses exponential interpolation if cutoff is inside a bucket
# Trim at sqrt(2) yields half the area between 1 and 2 boundaries.
eval instant at 1m h_test </ 1.4142135624
h_test {{count:8 sum:0.2570938865989847 z_bucket:1 z_bucket_w:0.001 buckets:[2 2] n_buckets:[1 2]}}
eval instant at 1m h_test >/ 1.4142135624
h_test {{count:26 sum:116.50067065070982 z_bucket_w:0.001 buckets:[0 2 8 16]}}
load 1m
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]}}
eval instant at 1m h_test_2 </ 1.13
h_test_2 {{schema:2 count:13.410582181123704 sum:-9.385798726068233 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
h_test_2 {{schema:2 count:14.589417818876296 sum:22.168126492693734 z_bucket_w:0.001 offset:1 buckets:[0.589417818876296 4 7 3]}}
eval instant at 1m h_test_2 >/ -1.3
h_test_2 {{schema:2 count:25.54213947904476 sum:16.29588491217537 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
h_test_2 {{schema:2 count:2.45786052095524 sum:-3.5189307983595066 z_bucket_w:0.001 n_offset:2 n_buckets:[1.45786052095524 1]}}
# Exponential buckets: trim on bucket boundary uses no interpolation.
eval instant at 1m h_test </ 2
h_test{} {{count:10 sum:3.5355339059327373 z_bucket:1 z_bucket_w:0.001 buckets:[2 4] n_buckets:[1 2]}}
eval instant at 1m h_test >/ 2
h_test{} {{count:24 sum:113.13708498984761 z_bucket_w:0.001 offset:2 buckets:[8 16]}}
eval instant at 1m h_test >/ -1
h_test{} {{count:32 sum:119.50104602052653 z_bucket:1 z_bucket_w:0.001 buckets:[2 4 8 16] n_buckets:[1]}}
eval instant at 1m h_test </ -1
h_test{} {{count:2 sum:-2.8284271247461903 z_bucket_w:0.001 n_offset:1 n_buckets:[2]}}
# Exponential buckets: trim zero bucket that is positive-biased (because of the presence of positive buckets).
load 1m
h_positive_buckets {{schema:0 sum:8.0210678118654755 count:12 z_bucket:2 z_bucket_w:0.5 buckets:[10]}}
eval instant at 1m h_positive_buckets >/ 0.5
h_positive_buckets {{schema:0 count:10 sum:7.0710678118654755 z_bucket:0 z_bucket_w:0.5 buckets:[10]}}
eval instant at 1m h_positive_buckets >/ 0.1
h_positive_buckets {{schema:0 count:11.6 sum:7.551067811865476 z_bucket:1.6 z_bucket_w:0.5 buckets:[10]}}
eval instant at 1m h_positive_buckets >/ 0
h_positive_buckets {{schema:0 sum:8.0210678118654755 count:12 z_bucket:2 z_bucket_w:0.5 buckets:[10]}}
eval instant at 1m h_positive_buckets </ 0.5
h_positive_buckets {{schema:0 count:2 sum:0.5 z_bucket:2 z_bucket_w:0.5}}
eval instant at 1m h_positive_buckets </ 0.1
h_positive_buckets {{schema:0 count:0.4 sum:0.02 z_bucket:0.4 z_bucket_w:0.5}}
eval instant at 1m h_positive_buckets </ 0
h_positive_buckets {{schema:0 z_bucket_w:0.5}}
# Exponential buckets: trim zero bucket that is negative-biased (because of the presence of negative buckets).
load 1m
h_negative_buckets {{schema:0 sum:-8.0210678118654755 count:12 z_bucket:2 z_bucket_w:0.5 n_buckets:[10]}}
eval instant at 1m h_negative_buckets </ -0.5
h_negative_buckets {{schema:0 count:10 sum:-7.0710678118654755 z_bucket:0 z_bucket_w:0.5 n_buckets:[10]}}
eval instant at 1m h_negative_buckets </ -0.1
h_negative_buckets {{schema:0 count:11.6 sum:-7.551067811865476 z_bucket:1.6 z_bucket_w:0.5 n_buckets:[10]}}
eval instant at 1m h_negative_buckets </ 0
h_negative_buckets {{schema:0 sum:-8.0210678118654755 count:12 z_bucket:2 z_bucket_w:0.5 n_buckets:[10]}}
eval instant at 1m h_negative_buckets >/ -0.5
h_negative_buckets {{schema:0 count:2 sum:-0.5 z_bucket:2 z_bucket_w:0.5}}
eval instant at 1m h_negative_buckets >/ -0.1
h_negative_buckets {{schema:0 count:0.4 sum:-0.02 z_bucket:0.4 z_bucket_w:0.5}}
eval instant at 1m h_negative_buckets >/ 0
h_negative_buckets {{schema:0 z_bucket_w:0.5}}
# Exponential buckets: trim zero bucket when there are no other buckets.
load 1m
zero_bucket_only {{schema:0 count:5 sum:0 z_bucket:5 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only >/ 0.1
zero_bucket_only {{schema:0 count:0 sum:0 z_bucket:0 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only </ 0.1
zero_bucket_only {{schema:0 count:5 sum:0 z_bucket:5 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only >/ 0.05
zero_bucket_only {{schema:0 count:1.25 sum:0.09375 z_bucket:1.25 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only </ 0.05
zero_bucket_only {{schema:0 count:3.75 sum:-0.09375 z_bucket:3.75 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only >/ 0
zero_bucket_only {{schema:0 count:2.5 sum:0.125 z_bucket:2.5 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only </ 0
zero_bucket_only {{schema:0 count:2.5 sum:-0.125 z_bucket:2.5 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only >/ -0.05
zero_bucket_only {{schema:0 count:3.75 sum:0.09375 z_bucket:3.75 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only </ -0.05
zero_bucket_only {{schema:0 count:1.25 sum:-0.09375 z_bucket:1.25 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only </ -0.1
zero_bucket_only {{schema:0 count:0 sum:0 z_bucket:0 z_bucket_w:0.1 }}
eval instant at 1m zero_bucket_only >/ -0.1
zero_bucket_only {{schema:0 count:5 sum:0 z_bucket:5 z_bucket_w:0.1 }}
load 1m
cbh {{schema:-53 sum:172.5 count:15 custom_values:[5 10 15 20] buckets:[1 6 4 3 1]}}
# Custom buckets: trim on bucket boundary without interpolation
eval instant at 1m cbh </ 15
cbh{} {{schema:-53 count:11 sum:97.5 custom_values:[5 10 15 20] buckets:[1 6 4]}}
eval instant at 1m cbh >/ 15
cbh{} {{schema:-53 count:4 sum:72.5 custom_values:[5 10 15 20] offset:3 buckets:[3 1]}}
# Custom buckets: trim uses linear interpolation if cutoff is inside a bucket
eval instant at 1m cbh </ 13
cbh{} {{schema:-53 count:9.4 sum:75.1 custom_values:[5 10 15 20] buckets:[1 6 2.4]}}
eval instant at 1m cbh >/ 13
cbh{} {{schema:-53 count:5.6 sum:94.9 custom_values:[5 10 15 20] offset:2 buckets:[1.6 3 1]}}
eval instant at 1m cbh </ 7.5
cbh{} {{schema:-53 count:4 sum:21.25 custom_values:[5 10 15 20] buckets:[1 3]}}
# Custom buckets: trim drops +Inf bucket entirely even if cutoff is above its lower bound
eval instant at 1m cbh </ 50
cbh{} {{schema:-53 count:14 sum:150.0 custom_values:[5 10 15 20] buckets:[1 6 4 3]}}
eval instant at 1m cbh </ -Inf
cbh{} {{schema:-53 custom_values:[5 10 15 20]}}
eval instant at 1m cbh >/ +Inf
cbh{} {{schema:-53 custom_values:[5 10 15 20]}}
eval instant at 1m cbh </ +Inf
cbh {{schema:-53 sum:172.5 count:15 custom_values:[5 10 15 20] buckets:[1 6 4 3 1]}}
eval instant at 1m cbh >/ -Inf
cbh {{schema:-53 sum:172.5 count:15 custom_values:[5 10 15 20] buckets:[1 6 4 3 1]}}
# Noop
eval instant at 1m cbh >/ 0
cbh {{schema:-53 sum:172.5 count:15 custom_values:[5 10 15 20] buckets:[1 6 4 3 1]}}
eval instant at 1m cbh </ 0
cbh {{schema:-53 custom_values:[5 10 15 20]}}
# Custom buckets: negative values
load 1m
cbh_has_neg {{schema:-53 sum:172.5 count:15 custom_values:[-10 5 10 15 20] buckets:[2 1 6 4 3 1]}}
eval instant at 1m cbh_has_neg </ 2
cbh_has_neg{} {{schema:-53 count:2.8 sum:-23.2 custom_values:[-10 5 10 15 20] buckets:[2 0.8]}}
eval instant at 1m cbh_has_neg </ -4
cbh_has_neg{} {{schema:-53 count:2.4 sum:-22.8 custom_values:[-10 5 10 15 20] buckets:[2 0.4]}}
eval instant at 1m cbh_has_neg </ -15
cbh_has_neg{} {{schema:-53 count:2 sum:-30 custom_values:[-10 5 10 15 20] buckets:[2]}}
load 1m
zero_bucket {{schema:0 sum:-6.75 z_bucket:5 z_bucket_w:0.01 buckets:[2 3] n_buckets:[1 2 3]}}
# Zero Bucket Edge Case: Interpolation Around Zero
eval instant at 1m zero_bucket </ -0.005
zero_bucket{} {{count:7.25 sum:-12.03019028017131 z_bucket:1.25 z_bucket_w:0.01 n_buckets:[1 2 3]}}
eval instant at 1m zero_bucket >/ 0
zero_bucket{} {{count:7.5 sum:5.669354249492381 z_bucket:2.5 z_bucket_w:0.01 buckets:[2 3]}}
load 1m
cbh_one_bucket {{schema:-53 sum:100.0 count:100 buckets:[100]}}
# Skip [-Inf; +Inf] bucket (100).
eval instant at 1m cbh_one_bucket </ 10.0
cbh_one_bucket {{schema:-53 sum:0.0 count:0 buckets:[0]}}
# Skip [-Inf; +Inf] bucket (100).
eval instant at 1m cbh_one_bucket >/ 10.0
cbh_one_bucket {{schema:-53 sum:0.0 count:0 buckets:[0]}}
# Keep [-Inf; +Inf] bucket (100).
eval instant at 1m cbh_one_bucket </ +Inf
cbh_one_bucket {{schema:-53 sum:100 count:100 buckets:[100]}}
# Skip [-Inf; +Inf] bucket (100).
eval instant at 1m cbh_one_bucket >/ +Inf
cbh_one_bucket {{schema:-53 sum:0 count:0 buckets:[0]}}
# Keep [-Inf; +Inf] bucket (100).
eval instant at 1m cbh_one_bucket >/ -Inf
cbh_one_bucket {{schema:-53 sum:100 count:100 buckets:[100]}}
# Skip [-Inf; +Inf] bucket (100).
eval instant at 1m cbh_one_bucket </ -Inf
cbh_one_bucket {{schema:-53 sum:0 count:0 buckets:[0]}}
load 1m
cbh_two_buckets_split_at_zero {{schema:-53 sum:33.0 count:100 custom_values:[0] buckets:[1 100]}}
# Skip (0; +Inf] bucket (100).
eval instant at 1m cbh_two_buckets_split_at_zero </ 10.0
cbh_two_buckets_split_at_zero {{schema:-53 sum:0.0 count:1 custom_values:[0] buckets:[1 0]}}
# Skip (0; +Inf] bucket (100).
eval instant at 1m cbh_two_buckets_split_at_zero </ 0.0
cbh_two_buckets_split_at_zero {{schema:-53 sum:0.0 count:1 custom_values:[0] buckets:[1 0]}}
# Skip both buckets (1, 100).
eval instant at 1m cbh_two_buckets_split_at_zero </ -10.0
cbh_two_buckets_split_at_zero {{schema:-53 sum:-10.0 count:1 custom_values:[0] buckets:[1 0]}}
# Skip [-Inf, 0] bucket (1).
eval instant at 1m cbh_two_buckets_split_at_zero >/ -10.0
cbh_two_buckets_split_at_zero {{schema:-53 sum:0.0 count:100 custom_values:[0] buckets:[0 100]}}
# Skip [-Inf, 0] bucket (1).
eval instant at 1m cbh_two_buckets_split_at_zero >/ 0.0
cbh_two_buckets_split_at_zero {{schema:-53 sum:0.0 count:100 custom_values:[0] buckets:[0 100]}}
# Skip first bucket.
eval instant at 1m cbh_two_buckets_split_at_zero >/ 10.0
cbh_two_buckets_split_at_zero {{schema:-53 sum:1000.0 count:100 custom_values:[0] buckets:[0 100]}}
load 1m
cbh_two_buckets_split_at_positive {{schema:-53 sum:33 count:101 custom_values:[5] buckets:[1 100]}}
# Skip (5, +Inf] bucket (100).
eval instant at 1m cbh_two_buckets_split_at_positive </ 10.0
cbh_two_buckets_split_at_positive {{schema:-53 sum:2.5 count:1 custom_values:[5] buckets:[1 0]}}
# Skip (5, +Inf] bucket (100) and 3/5 of [0, 5] bucket (0.6 * 3.5).
eval instant at 1m cbh_two_buckets_split_at_positive </ 2.0
cbh_two_buckets_split_at_positive {{schema:-53 sum:0.4 count:0.4 custom_values:[5] buckets:[0.4 0]}}
# Skip both buckets (1 and 100).
eval instant at 1m cbh_two_buckets_split_at_positive </ 0.0
cbh_two_buckets_split_at_positive {{schema:-53 custom_values:[5]}}
# Skip both buckets (1 and 100).
eval instant at 1m cbh_two_buckets_split_at_positive </ -10.0
cbh_two_buckets_split_at_positive {{schema:-53 sum:0.0 count:0 custom_values:[5] buckets:[0 0]}}
# Skip [0, 5] bucket (1).
eval instant at 1m cbh_two_buckets_split_at_positive >/ -10.0
cbh_two_buckets_split_at_positive {{schema:-53 sum:500.0 count:100 custom_values:[5] buckets:[0 100]}}
# Noop.
eval instant at 1m cbh_two_buckets_split_at_positive >/ 0.0
cbh_two_buckets_split_at_positive {{schema:-53 sum:33.0 count:101 custom_values:[5] buckets:[1 100]}}
# Keep (5, +Inf] bucket (100) and 3/5 of [0, 5] bucket (0.6 * 3.5).
eval instant at 1m cbh_two_buckets_split_at_positive >/ 2.0
cbh_two_buckets_split_at_positive {{schema:-53 sum:502.1 count:100.6 custom_values:[5] buckets:[0.6 100]}}
# Skip first bucket.
eval instant at 1m cbh_two_buckets_split_at_positive >/ 10.0
cbh_two_buckets_split_at_positive {{schema:-53 sum:1000.0 count:100 custom_values:[5] buckets:[0 100]}}
load 1m
cbh_two_buckets_split_at_negative {{schema:-53 sum:33 count:101 custom_values:[-5] buckets:[1 100]}}
# Skip (-5, +Inf] bucket (100).
eval instant at 1m cbh_two_buckets_split_at_negative </ 10.0
cbh_two_buckets_split_at_negative {{schema:-53 sum:-5.0 count:1 custom_values:[-5] buckets:[1 0]}}
# Skip (-5, +Inf] bucket (100).
eval instant at 1m cbh_two_buckets_split_at_negative </ 0.0
cbh_two_buckets_split_at_negative {{schema:-53 sum:-5 count:1 custom_values:[-5] buckets:[1 0]}}
# Skip (-5; +Inf] bucket (100).
eval instant at 1m cbh_two_buckets_split_at_negative </ -2.0
cbh_two_buckets_split_at_negative {{schema:-53 sum:-5.0 count:1 custom_values:[-5] buckets:[1 0]}}
# Skip (-5, +Inf] bucket (100).
eval instant at 1m cbh_two_buckets_split_at_negative </ -10.0
cbh_two_buckets_split_at_negative {{schema:-53 sum:-10.0 count:1 custom_values:[-5] buckets:[1 0]}}
# Skip [-Inf, -5] bucket (1).
eval instant at 1m cbh_two_buckets_split_at_negative >/ -10.0
cbh_two_buckets_split_at_negative {{schema:-53 sum:-500 count:100 custom_values:[-5] buckets:[0 100]}}
# Skip [-Inf, -5] bucket (1).
eval instant at 1m cbh_two_buckets_split_at_negative >/ -2.0
cbh_two_buckets_split_at_negative {{schema:-53 sum:-200 count:100 custom_values:[-5] buckets:[0 100]}}
# Skip [-Inf, -5] bucket (1).
eval instant at 1m cbh_two_buckets_split_at_negative >/ 0.0
cbh_two_buckets_split_at_negative {{schema:-53 sum:0.0 count:100 custom_values:[-5] buckets:[0 100]}}
# Skip [-Inf, -5] bucket (1).
eval instant at 1m cbh_two_buckets_split_at_negative >/ 10.0
cbh_two_buckets_split_at_negative {{schema:-53 sum:1000.0 count:100 custom_values:[-5] buckets:[0 100]}}
load 1m
cbh_for_join{label="a"} {{schema:-53 sum:33 count:101 custom_values:[5] buckets:[1 100]}}
cbh_for_join{label="b"} {{schema:-53 sum:66 count:202 custom_values:[5] buckets:[2 200]}}
cbh_for_join{label="c"} {{schema:-53 sum:99 count:303 custom_values:[5] buckets:[3 300]}}
float_for_join{label="a"} 1
float_for_join{label="b"} 4
eval instant at 1m cbh_for_join >/ on (label) float_for_join
{label="a"} {{schema:-53 count:100.8 sum:502.4 custom_values:[5] buckets:[0.8 100]}}
{label="b"} {{schema:-53 count:200.4 sum:1001.8 custom_values:[5] buckets:[0.4 200]}}
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';
@ -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';
@ -215,6 +217,8 @@ export class Parser {
const rt = this.checkAST(rExpr);
const boolModifierUsed = node.getChild(BoolModifier);
const isComparisonOperator = containsAtLeastOneChild(node, Eql, Neq, Lte, Lss, Gte, Gtr);
const isTrimLowerOperator = containsChild(node, TrimLower);
const isTrimUpperOperator = containsChild(node, TrimUpper);
const isSetOperator = containsAtLeastOneChild(node, And, Or, Unless);
// BOOL modifier check
@ -223,8 +227,14 @@ export class Parser {
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 (lt === ValueType.scalar && rt === ValueType.scalar) {
if (isComparisonOperator) {
this.addDiagnostic(node, 'comparisons between scalars must use BOOL modifier');
} else if (isTrimLowerOperator) {
this.addDiagnostic(node, 'operator ">/" not allowed for Scalar operations');
} else if (isTrimUpperOperator) {
this.addDiagnostic(node, 'operator "</" not allowed for Scalar operations');
}
}
}

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 |
@ -338,6 +340,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)
)
)