mirror of
https://github.com/prometheus/prometheus.git
synced 2026-05-05 04:16:15 +02:00
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:
parent
8ad21d0659
commit
7370d62acf
@ -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)
|
||||
|
||||
@ -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` |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
121
promql/promqltest/testdata/duration_expression.test
vendored
Normal file
121
promql/promqltest/testdata/duration_expression.test
vendored
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user