PromQL: allow arithmetic in durations in PromQL parser

Updated the parser to allow calculations in PromQL durations.

This enables durations in the form of:

  rate(http_requests_total[10m+2s])

The computation of the calculations is done directly at the parse level and does not hit the PromQL Engine.
The lexer has also been updated and improved, in particular for subqueries.

Buxfix: rate(http_requests_total[0]) is no longer allowed.

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
This commit is contained in:
Julien Pivotto 2025-03-20 13:15:20 +01:00
parent 8ad21d0659
commit 7370d62acf
12 changed files with 957 additions and 436 deletions

View File

@ -249,6 +249,9 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
case "promql-experimental-functions":
parser.EnableExperimentalFunctions = true
logger.Info("Experimental PromQL functions enabled.")
case "promql-duration-expr":
parser.ExperimentalDurationExpr = true
logger.Info("Experimental duration expression parsing enabled.")
case "native-histograms":
c.tsdb.EnableNativeHistograms = true
c.scrape.EnableNativeHistogramsIngestion = true
@ -539,7 +542,7 @@ func main() {
a.Flag("scrape.discovery-reload-interval", "Interval used by scrape manager to throttle target groups updates.").
Hidden().Default("5s").SetValue(&cfg.scrape.DiscoveryReloadInterval)
a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, native-histograms, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui, otlp-deltatocumulative. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.").
a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, native-histograms, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui, otlp-deltatocumulative, promql-duration-expr. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.").
Default("").StringsVar(&cfg.featureList)
a.Flag("agent", "Run Prometheus in 'Agent mode'.").BoolVar(&agentMode)

View File

@ -61,7 +61,7 @@ The Prometheus monitoring server
| <code class="text-nowrap">--query.timeout</code> | Maximum time a query may take before being aborted. Use with server mode only. | `2m` |
| <code class="text-nowrap">--query.max-concurrency</code> | Maximum number of queries executed concurrently. Use with server mode only. | `20` |
| <code class="text-nowrap">--query.max-samples</code> | Maximum number of samples a single query can load into memory. Note that queries will fail if they try to load more samples than this into memory, so this also limits the number of samples a query can return. Use with server mode only. | `50000000` |
| <code class="text-nowrap">--enable-feature</code> <code class="text-nowrap">...<code class="text-nowrap"> | Comma separated feature names to enable. Valid options: exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, native-histograms, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui, otlp-deltatocumulative. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | |
| <code class="text-nowrap">--enable-feature</code> <code class="text-nowrap">...<code class="text-nowrap"> | Comma separated feature names to enable. Valid options: exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-per-step-stats, promql-experimental-functions, extra-scrape-metrics, auto-gomaxprocs, native-histograms, created-timestamp-zero-ingestion, concurrent-rule-eval, delayed-compaction, old-ui, otlp-deltatocumulative, promql-duration-expr. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | |
| <code class="text-nowrap">--agent</code> | Run Prometheus in 'Agent mode'. | |
| <code class="text-nowrap">--log.level</code> | Only log messages with the given severity or above. One of: [debug, info, warn, error] | `info` |
| <code class="text-nowrap">--log.format</code> | Output format of log messages. One of: [logfmt, json] | `logfmt` |

View File

@ -183,4 +183,26 @@ This state is periodically ([`max_stale`][d2c]) cleared of inactive series.
Enabling this _can_ have negative impact on performance, because the in-memory
state is mutex guarded. Cumulative-only OTLP requests are not affected.
### PromQL arithmetic expressions in time durations
`--enable-feature=promql-duration-expr`
With this flag, arithmetic expressions can also be used in time durations. The following operators are supported:
* `+` - addition
* `-` - subtraction
* `*` - multiplication
* `/` - division
* `%` - modulo
* `^` - exponentiation
Examples:
5m * 2 # Equivalent to 10m or 600s
10m - 1m # Equivalent to 9m or 540s
(5+2) * 1m # Equivalent to 7m or 420s
1h / 2 # Equivalent to 30m or 1800s
4h % 3h # Equivalent to 1h or 3600s
(2 ^ 3) * 1m # Equivalent to 8m or 480s
[d2c]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/deltatocumulativeprocessor

View File

@ -1900,15 +1900,6 @@ func TestSubquerySelector(t *testing.T) {
},
Start: time.Unix(35, 0),
},
{
Query: "metric[0:10s]",
Result: promql.Result{
nil,
promql.Matrix{},
nil,
},
Start: time.Unix(10, 0),
},
},
},
{
@ -3268,11 +3259,6 @@ func TestInstantQueryWithRangeVectorSelector(t *testing.T) {
},
},
},
"matches series but range is 0": {
expr: "some_metric[0]",
ts: baseT.Add(2 * time.Minute),
expected: promql.Matrix{},
},
}
for name, testCase := range testCases {

View File

@ -186,7 +186,7 @@ START_METRIC_SELECTOR
%type <int> int
%type <uint> uint
%type <float> number series_value signed_number signed_or_unsigned_number
%type <node> step_invariant_expr aggregate_expr aggregate_modifier bin_modifier binary_expr bool_modifier expr function_call function_call_args function_call_body group_modifiers label_matchers matrix_selector number_duration_literal offset_expr on_or_ignoring paren_expr string_literal subquery_expr unary_expr vector_selector
%type <node> step_invariant_expr aggregate_expr aggregate_modifier bin_modifier binary_expr bool_modifier expr function_call function_call_args function_call_body group_modifiers label_matchers matrix_selector number_duration_literal offset_expr on_or_ignoring paren_expr string_literal subquery_expr unary_expr vector_selector duration_expr paren_duration_expr positive_duration_expr
%start start
@ -433,14 +433,30 @@ paren_expr : LEFT_PAREN expr RIGHT_PAREN
* Offset modifiers.
*/
offset_expr: expr OFFSET number_duration_literal
positive_duration_expr : duration_expr
{
numLit, _ := $3.(*NumberLiteral)
dur := time.Duration(numLit.Val * 1000) * time.Millisecond
numLit, ok := $1.(*NumberLiteral)
if !ok {
// This should never happen but handle it gracefully.
yylex.(*parser).addParseErrf(posrange.PositionRange{}, "internal error: duration expression did not evaluate to a number")
$$ = &NumberLiteral{Val: 1} // Use 1 as fallback to prevent cascading errors.
} else if numLit.Val > 0 {
$$ = numLit
} else {
yylex.(*parser).addParseErrf(numLit.PosRange, "duration must be greater than 0")
$$ = &NumberLiteral{Val: 1, PosRange: numLit.PosRange} // Use 1 as fallback.
}
}
;
offset_expr: expr OFFSET duration_expr
{
numLit, _ := $3.(*NumberLiteral)
dur := time.Duration(numLit.Val * 1000) * time.Millisecond
yylex.(*parser).addOffset($1, dur)
$$ = $1
}
| expr OFFSET SUB number_duration_literal
| expr OFFSET SUB duration_expr
{
numLit, _ := $4.(*NumberLiteral)
dur := time.Duration(numLit.Val * 1000) * time.Millisecond
@ -450,6 +466,7 @@ offset_expr: expr OFFSET number_duration_literal
| expr OFFSET error
{ yylex.(*parser).unexpected("offset", "number or duration"); $$ = $1 }
;
/*
* @ modifiers.
*/
@ -474,7 +491,7 @@ at_modifier_preprocessors: START | END;
* Subquery and range selectors.
*/
matrix_selector : expr LEFT_BRACKET number_duration_literal RIGHT_BRACKET
matrix_selector : expr LEFT_BRACKET positive_duration_expr RIGHT_BRACKET
{
var errMsg string
vs, ok := $1.(*VectorSelector)
@ -500,7 +517,7 @@ matrix_selector : expr LEFT_BRACKET number_duration_literal RIGHT_BRACKET
}
;
subquery_expr : expr LEFT_BRACKET number_duration_literal COLON number_duration_literal RIGHT_BRACKET
subquery_expr : expr LEFT_BRACKET positive_duration_expr COLON positive_duration_expr RIGHT_BRACKET
{
numLitRange, _ := $3.(*NumberLiteral)
numLitStep, _ := $5.(*NumberLiteral)
@ -511,7 +528,7 @@ subquery_expr : expr LEFT_BRACKET number_duration_literal COLON number_duratio
EndPos: $6.Pos + 1,
}
}
| expr LEFT_BRACKET number_duration_literal COLON RIGHT_BRACKET
| expr LEFT_BRACKET positive_duration_expr COLON RIGHT_BRACKET
{
numLitRange, _ := $3.(*NumberLiteral)
$$ = &SubqueryExpr{
@ -521,11 +538,11 @@ subquery_expr : expr LEFT_BRACKET number_duration_literal COLON number_duratio
EndPos: $5.Pos + 1,
}
}
| expr LEFT_BRACKET number_duration_literal COLON number_duration_literal error
| expr LEFT_BRACKET positive_duration_expr COLON positive_duration_expr error
{ yylex.(*parser).unexpected("subquery selector", "\"]\""); $$ = $1 }
| expr LEFT_BRACKET number_duration_literal COLON error
| expr LEFT_BRACKET positive_duration_expr COLON error
{ yylex.(*parser).unexpected("subquery selector", "number or duration or \"]\""); $$ = $1 }
| expr LEFT_BRACKET number_duration_literal error
| expr LEFT_BRACKET positive_duration_expr error
{ yylex.(*parser).unexpected("subquery or range", "\":\" or \"]\""); $$ = $1 }
| expr LEFT_BRACKET error
{ yylex.(*parser).unexpected("subquery selector", "number or duration"); $$ = $1 }
@ -997,4 +1014,43 @@ maybe_grouping_labels: /* empty */ { $$ = nil }
| grouping_labels
;
/*
* Duration expressions.
*/
duration_expr : number_duration_literal
/* Gives the rule the same precedence as MUL. This aligns with mathematical conventions. */
| unary_op duration_expr %prec MUL
{
nl, ok := $2.(*NumberLiteral)
if !ok {
yylex.(*parser).addParseErrf($1.PositionRange(), "expected number literal in duration expression")
$$ = &NumberLiteral{Val: 0}
break
}
if $1.Typ == SUB {
nl.Val *= -1
}
nl.PosRange.Start = $1.Pos
$$ = nl
}
| duration_expr ADD duration_expr
{ $$ = yylex.(*parser).evalDurationExprBinOp($1, $3, $2) }
| duration_expr SUB duration_expr
{ $$ = yylex.(*parser).evalDurationExprBinOp($1, $3, $2) }
| duration_expr MUL duration_expr
{ $$ = yylex.(*parser).evalDurationExprBinOp($1, $3, $2) }
| duration_expr DIV duration_expr
{ $$ = yylex.(*parser).evalDurationExprBinOp($1, $3, $2) }
| duration_expr MOD duration_expr
{ $$ = yylex.(*parser).evalDurationExprBinOp($1, $3, $2) }
| duration_expr POW duration_expr
{ $$ = yylex.(*parser).evalDurationExprBinOp($1, $3, $2) }
| paren_duration_expr
;
paren_duration_expr : LEFT_PAREN duration_expr RIGHT_PAREN
{ $$ = $2 }
;
%%

File diff suppressed because it is too large Load Diff

View File

@ -277,6 +277,7 @@ type Lexer struct {
braceOpen bool // Whether a { is opened.
bracketOpen bool // Whether a [ is opened.
gotColon bool // Whether we got a ':' after [ was opened.
gotDuration bool // Whether we got a duration after [ was opened.
stringOpen rune // Quote rune of the string currently being read.
// series description variables for internal PromQL testing framework as well as in promtool rules unit tests.
@ -491,7 +492,7 @@ func lexStatements(l *Lexer) stateFn {
skipSpaces(l)
}
l.bracketOpen = true
return lexNumberOrDuration
return lexDurationExpr
case r == ']':
if !l.bracketOpen {
return l.errorf("unexpected right bracket %q", r)
@ -549,6 +550,8 @@ func lexHistogram(l *Lexer) stateFn {
return lexNumber
case r == '[':
l.bracketOpen = true
l.gotColon = false
l.gotDuration = false
l.emit(LEFT_BRACKET)
return lexBuckets
case r == '}' && l.peek() == '}':
@ -1077,3 +1080,64 @@ func isDigit(r rune) bool {
func isAlpha(r rune) bool {
return r == '_' || ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z')
}
// lexDurationExpr scans arithmetic expressions within brackets for duration expressions.
func lexDurationExpr(l *Lexer) stateFn {
switch r := l.next(); {
case r == eof:
return l.errorf("unexpected end of input in duration expression")
case r == ']':
l.emit(RIGHT_BRACKET)
l.bracketOpen = false
l.gotColon = false
return lexStatements
case r == ':':
l.emit(COLON)
if !l.gotDuration {
return l.errorf("unexpected colon before duration in duration expression")
}
if l.gotColon {
return l.errorf("unexpected repeated colon in duration expression")
}
l.gotColon = true
return lexDurationExpr
case r == '(':
l.emit(LEFT_PAREN)
l.parenDepth++
return lexDurationExpr
case r == ')':
l.emit(RIGHT_PAREN)
l.parenDepth--
if l.parenDepth < 0 {
return l.errorf("unexpected right parenthesis %q", r)
}
return lexDurationExpr
case isSpace(r):
skipSpaces(l)
return lexDurationExpr
case r == '+':
l.emit(ADD)
return lexDurationExpr
case r == '-':
l.emit(SUB)
return lexDurationExpr
case r == '*':
l.emit(MUL)
return lexDurationExpr
case r == '/':
l.emit(DIV)
return lexDurationExpr
case r == '%':
l.emit(MOD)
return lexDurationExpr
case r == '^':
l.emit(POW)
return lexDurationExpr
case isDigit(r) || (r == '.' && isDigit(l.peek())):
l.backup()
l.gotDuration = true
return lexNumberOrDuration
default:
return l.errorf("unexpected character in duration expression: %q", r)
}
}

View File

@ -915,6 +915,10 @@ var tests = []struct {
input: `test:name{on!~"bar"}[:4s]`,
fail: true,
},
{
input: `test:name{on!~"bar"}[1s:1s:1s]`,
fail: true,
},
},
},
}

View File

@ -39,6 +39,9 @@ var parserPool = sync.Pool{
},
}
// ExperimentalDurationExpr is a flag to enable experimental duration expression parsing.
var ExperimentalDurationExpr bool
type Parser interface {
ParseExpr() (Expr, error)
Close()
@ -881,9 +884,6 @@ func parseDuration(ds string) (time.Duration, error) {
if err != nil {
return 0, err
}
if dur == 0 {
return 0, errors.New("duration must be greater than 0")
}
return time.Duration(dur), nil
}
@ -1060,3 +1060,66 @@ func MustGetFunction(name string) *Function {
}
return f
}
// evalDurationExprBinOp evaluates binary operations for duration expressions.
// It handles type checking, performs the operation using the specified operator,
// and constructs a new NumberLiteral with the result.
func (p *parser) evalDurationExprBinOp(lhs, rhs Node, op Item) *NumberLiteral {
if !ExperimentalDurationExpr {
p.addParseErrf(op.PositionRange(), "experimental duration expression parsing is experimental and must be enabled with --enable-feature=promql-duration-expr")
return &NumberLiteral{Val: 0}
}
numLit1, ok1 := lhs.(*NumberLiteral)
numLit2, ok2 := rhs.(*NumberLiteral)
if !ok1 || !ok2 {
p.addParseErrf(posrange.PositionRange{
Start: lhs.PositionRange().Start,
End: rhs.PositionRange().End,
}, "invalid operands for %s", op.Val)
return &NumberLiteral{Val: 0}
}
var val float64
var err error
switch op.Typ {
case ADD:
val = numLit1.Val + numLit2.Val
case SUB:
val = numLit1.Val - numLit2.Val
case MUL:
val = numLit1.Val * numLit2.Val
case DIV:
if numLit2.Val == 0 {
err = errors.New("division by zero")
} else {
val = numLit1.Val / numLit2.Val
}
case MOD:
if numLit2.Val == 0 {
err = errors.New("modulo by zero")
} else {
val = math.Mod(numLit1.Val, numLit2.Val)
}
case POW:
val = math.Pow(numLit1.Val, numLit2.Val)
default:
p.addParseErrf(op.PositionRange(), "unknown operator for duration expression: %s", op.Val)
return &NumberLiteral{Val: 0}
}
if err != nil {
p.addParseErrf(numLit2.PosRange, err.Error())
return &NumberLiteral{Val: 0}
}
return &NumberLiteral{
Val: val,
PosRange: posrange.PositionRange{
Start: numLit1.PosRange.Start,
End: numLit2.PosRange.End,
},
}
}

View File

@ -2337,12 +2337,12 @@ var testExpr = []struct {
{
input: `foo[]`,
fail: true,
errMsg: "bad number or duration syntax: \"\"",
errMsg: "unexpected \"]\" in subquery selector, expected number or duration",
},
{
input: `foo[-1]`,
fail: true,
errMsg: "bad number or duration syntax: \"\"",
errMsg: "duration must be greater than 0",
},
{
input: `some_metric[5m] OFFSET 1mm`,
@ -3091,7 +3091,7 @@ var testExpr = []struct {
{
input: `foo{bar="baz"}[`,
fail: true,
errMsg: `1:16: parse error: bad number or duration syntax: ""`,
errMsg: `unexpected end of input in duration expression`,
},
{
input: `foo{bar="baz"}[10m:6s]`,
@ -3946,6 +3946,120 @@ var testExpr = []struct {
},
},
},
{
input: `foo[11s+10s-5*2^2]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
Range: 1 * time.Second, // 11s+10s-5*2^2 = 21s-20s = 1s
EndPos: 18,
},
},
{
input: `foo[-(10s-5s)+20s]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
Range: 15 * time.Second, // -(10s-5s)+20s = -5s+20s = 15s
EndPos: 18,
},
},
{
input: `foo[-10s+15s]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
Range: 5 * time.Second, // -10s+15s = 5s
EndPos: 13,
},
},
{
input: `foo[4s+4s:1s*2] offset (5s-8)`,
expected: &SubqueryExpr{
Expr: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
Range: 8 * time.Second, // 4s+4s = 8s
Step: 2 * time.Second, // 1s*2 = 2s
OriginalOffset: -3 * time.Second, // 5s-8 = -3s
EndPos: 29,
},
},
{
input: `foo offset 5s-8`,
expected: &BinaryExpr{
Op: SUB,
LHS: &VectorSelector{
Name: "foo",
OriginalOffset: 5 * time.Second,
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 13,
},
},
RHS: &NumberLiteral{
Val: 8,
PosRange: posrange.PositionRange{
Start: 14,
End: 15,
},
},
},
},
{
input: `foo[5s/0d]`,
fail: true,
errMsg: `division by zero`,
},
{
input: `foo offset (4d/0)`,
fail: true,
errMsg: `division by zero`,
},
{
input: `foo[5s%0d]`,
fail: true,
errMsg: `modulo by zero`,
},
{
input: `foo offset (5s%(2d-2d))`,
fail: true,
errMsg: `modulo by zero`,
},
}
func makeInt64Pointer(val int64) *int64 {
@ -3965,8 +4079,11 @@ func readable(s string) string {
func TestParseExpressions(t *testing.T) {
// Enable experimental functions testing.
EnableExperimentalFunctions = true
// Enable experimental duration expression parsing.
ExperimentalDurationExpr = true
t.Cleanup(func() {
EnableExperimentalFunctions = false
ExperimentalDurationExpr = false
})
for _, test := range testExpr {

View File

@ -117,8 +117,12 @@ func RunBuiltinTests(t TBRun, engine promql.QueryEngine) {
// RunBuiltinTestsWithStorage runs an acceptance test suite against the provided engine and storage.
func RunBuiltinTestsWithStorage(t TBRun, engine promql.QueryEngine, newStorage func(testutil.T) storage.Storage) {
t.Cleanup(func() { parser.EnableExperimentalFunctions = false })
t.Cleanup(func() {
parser.EnableExperimentalFunctions = false
parser.ExperimentalDurationExpr = false
})
parser.EnableExperimentalFunctions = true
parser.ExperimentalDurationExpr = true
files, err := fs.Glob(testsFs, "*/*.test")
require.NoError(t, err)

View File

@ -0,0 +1,121 @@
# Test for different duration expression formats in range selectors.
# This tests the parser's ability to handle various duration expression.
# Set up a basic counter that increases steadily.
load 5m
http_requests{path="/foo"} 1 2 3 0 1 0 0 1 2 0
http_requests{path="/bar"} 1 2 3 4 5 1 2 3 4 5
http_requests{path="/biz"} 0 0 0 0 0 1 1 1 1 1
# Test basic duration with unit: [30m]
eval instant at 50m changes(http_requests[30m])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test addition in duration: [26m+4m]
eval instant at 50m changes(http_requests[26m+4m])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test addition with 0 in duration: [30m+0s]
eval instant at 50m changes(http_requests[30m+0s])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test raw seconds: [1800]
eval instant at 50m changes(http_requests[1800])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test seconds with multiplication: [60*30]
eval instant at 50m changes(http_requests[60*30])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test minutes with multiplication: [2m*15]
eval instant at 50m changes(http_requests[2m*15])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test complex expression with parentheses: [2m*(10+5)]
eval instant at 50m changes(http_requests[2m*(10+5)])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test mixed units: [29m+60s]
eval instant at 50m changes(http_requests[29m+60s])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test nested parentheses: [24m+((1.5*2m)+2m)]
eval instant at 50m changes(http_requests[24m+((1.5*2m)+2m)])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test start with -: [-5m+35m]
eval instant at 50m changes(http_requests[-5m+35m])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test division: [1h/2]
eval instant at 50m changes(http_requests[1h/2])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test modulo: [1h30m % 1h]
eval instant at 50m changes(http_requests[1h30m % 1h])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test modulo and calculation: [30m1s-30m1s % 1m]
eval instant at 50m changes(http_requests[30m1s-30m1s % 1m])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
# Test combination of operations: [(9m30s+30s)*3]
eval instant at 50m changes(http_requests[(9m30s+30s)*3])
{path="/foo"} 3
{path="/bar"} 4
{path="/biz"} 0
clear
load 10s
metric1_total 0+1x1000
# In subquery expression.
eval instant at 1000s sum_over_time(metric1_total[29s+1s:5s+5s])
{} 297
# Test complex expressions in subquery ranges.
eval instant at 1000s sum_over_time(metric1_total[29s+1s:((((8 - 2) / 3) * 7s) % 4) + 8000ms])
{} 297
# Test complex expressions in offset ranges.
eval instant at 1200s sum_over_time(metric1_total[29s+1s:20*500ms] offset (20*(((((8 - 2) / 3) * 7s) % 4) + 8000ms)))
{} 297
# Test complex expressions in offset ranges with negative offset.
eval instant at 800s sum_over_time(metric1_total[29s+1s:20*500ms] offset -(20*(((((8 - 2) / 3) * 7s) % 4) + 8000ms)))
{} 297
# Test offset precedence with parentheses: offset (100 + 2)
eval instant at 1000s metric1_total offset (100 + 2)
{__name__="metric1_total"} 89
# Test offset precedence without parentheses: offset 100 + 2
eval instant at 1000s metric1_total offset 100 + 2
{} 92