Add step(), min(a,b) and max(a,b) in promql duration expressions

step() is a new keyword introduced to represent the query step width in duration expressions.

min(a,b) and max(a,b) return the min and max from two duration expressions.

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
This commit is contained in:
Julien Pivotto 2025-06-25 17:39:27 +02:00
parent bfbae39931
commit ee7d5158a7
13 changed files with 1187 additions and 490 deletions

View File

@ -181,6 +181,8 @@ This state is periodically ([`max_stale`][d2c]) cleared of inactive series.
Enabling this _can_ have negative impact on performance, because the in-memory Enabling this _can_ have negative impact on performance, because the in-memory
state is mutex guarded. Cumulative-only OTLP requests are not affected. state is mutex guarded. Cumulative-only OTLP requests are not affected.
[d2c]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/deltatocumulativeprocessor
## PromQL arithmetic expressions in time durations ## PromQL arithmetic expressions in time durations
`--enable-feature=promql-duration-expr` `--enable-feature=promql-duration-expr`
@ -203,6 +205,12 @@ When using offset with duration expressions, you must wrap the expression in
parentheses. Without parentheses, only the first duration value will be used in parentheses. Without parentheses, only the first duration value will be used in
the offset calculation. the offset calculation.
`step()` can be used in duration expressions.
For a **range query**, it resolves to the step width of the range query.
For an **instant query**, it resolves to `0s`.
`min(<duration>, <duration>)` and `max(<duration>, <duration>)` functions can be used to find the minimum or maximum of two duration expressions.
**Note**: Duration expressions are not supported in the @ timestamp operator. **Note**: Duration expressions are not supported in the @ timestamp operator.
The following operators are supported: The following operators are supported:
@ -222,8 +230,10 @@ Examples of equivalent durations:
* `1h / 2` is the equivalent to `30m` or `1800s` * `1h / 2` is the equivalent to `30m` or `1800s`
* `4h % 3h` is the equivalent to `1h` or `3600s` * `4h % 3h` is the equivalent to `1h` or `3600s`
* `(2 ^ 3) * 1m` is the equivalent to `8m` or `480s` * `(2 ^ 3) * 1m` is the equivalent to `8m` or `480s`
* `step() + 1` is the equivalent to the query step width increased by 1s.
* `max(step(), 5s)` is the equivalent to the maximum value between the query step width and `5s`.
* `min(2 * step() + 5s, 5m)` is the equivalent to the minimum value between the query step increased by `5s` and `5m`.
[d2c]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/deltatocumulativeprocessor
## OTLP Native Delta Support ## OTLP Native Delta Support

View File

@ -22,13 +22,15 @@ import (
) )
// durationVisitor is a visitor that visits a duration expression and calculates the duration. // durationVisitor is a visitor that visits a duration expression and calculates the duration.
type durationVisitor struct{} type durationVisitor struct {
step time.Duration
}
func (v *durationVisitor) Visit(node parser.Node, _ []parser.Node) (parser.Visitor, error) { func (v *durationVisitor) Visit(node parser.Node, _ []parser.Node) (parser.Visitor, error) {
switch n := node.(type) { switch n := node.(type) {
case *parser.VectorSelector: case *parser.VectorSelector:
if n.OriginalOffsetExpr != nil { if n.OriginalOffsetExpr != nil {
duration, err := calculateDuration(n.OriginalOffsetExpr, true) duration, err := v.calculateDuration(n.OriginalOffsetExpr, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -36,7 +38,7 @@ func (v *durationVisitor) Visit(node parser.Node, _ []parser.Node) (parser.Visit
} }
case *parser.MatrixSelector: case *parser.MatrixSelector:
if n.RangeExpr != nil { if n.RangeExpr != nil {
duration, err := calculateDuration(n.RangeExpr, false) duration, err := v.calculateDuration(n.RangeExpr, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,21 +46,21 @@ func (v *durationVisitor) Visit(node parser.Node, _ []parser.Node) (parser.Visit
} }
case *parser.SubqueryExpr: case *parser.SubqueryExpr:
if n.OriginalOffsetExpr != nil { if n.OriginalOffsetExpr != nil {
duration, err := calculateDuration(n.OriginalOffsetExpr, true) duration, err := v.calculateDuration(n.OriginalOffsetExpr, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
n.OriginalOffset = duration n.OriginalOffset = duration
} }
if n.StepExpr != nil { if n.StepExpr != nil {
duration, err := calculateDuration(n.StepExpr, false) duration, err := v.calculateDuration(n.StepExpr, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
n.Step = duration n.Step = duration
} }
if n.RangeExpr != nil { if n.RangeExpr != nil {
duration, err := calculateDuration(n.RangeExpr, false) duration, err := v.calculateDuration(n.RangeExpr, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -69,8 +71,8 @@ func (v *durationVisitor) Visit(node parser.Node, _ []parser.Node) (parser.Visit
} }
// calculateDuration computes the duration from a duration expression. // calculateDuration computes the duration from a duration expression.
func calculateDuration(expr parser.Expr, allowedNegative bool) (time.Duration, error) { func (v *durationVisitor) calculateDuration(expr parser.Expr, allowedNegative bool) (time.Duration, error) {
duration, err := evaluateDurationExpr(expr) duration, err := v.evaluateDurationExpr(expr)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -84,7 +86,7 @@ func calculateDuration(expr parser.Expr, allowedNegative bool) (time.Duration, e
} }
// evaluateDurationExpr recursively evaluates a duration expression to a float64 value. // evaluateDurationExpr recursively evaluates a duration expression to a float64 value.
func evaluateDurationExpr(expr parser.Expr) (float64, error) { func (v *durationVisitor) evaluateDurationExpr(expr parser.Expr) (float64, error) {
switch n := expr.(type) { switch n := expr.(type) {
case *parser.NumberLiteral: case *parser.NumberLiteral:
return n.Val, nil return n.Val, nil
@ -93,19 +95,31 @@ func evaluateDurationExpr(expr parser.Expr) (float64, error) {
var err error var err error
if n.LHS != nil { if n.LHS != nil {
lhs, err = evaluateDurationExpr(n.LHS) lhs, err = v.evaluateDurationExpr(n.LHS)
if err != nil { if err != nil {
return 0, err return 0, err
} }
} }
rhs, err = evaluateDurationExpr(n.RHS) if n.RHS != nil {
if err != nil { rhs, err = v.evaluateDurationExpr(n.RHS)
return 0, err if err != nil {
return 0, err
}
} }
switch n.Op { switch n.Op {
case parser.STEP:
return float64(v.step.Seconds()), nil
case parser.MIN:
return math.Min(lhs, rhs), nil
case parser.MAX:
return math.Max(lhs, rhs), nil
case parser.ADD: case parser.ADD:
if n.LHS == nil {
// Unary positive duration expression.
return rhs, nil
}
return lhs + rhs, nil return lhs + rhs, nil
case parser.SUB: case parser.SUB:
if n.LHS == nil { if n.LHS == nil {

View File

@ -195,6 +195,24 @@ func TestCalculateDuration(t *testing.T) {
expected: -5 * time.Second, expected: -5 * time.Second,
allowedNegative: true, allowedNegative: true,
}, },
{
name: "step",
expr: &parser.DurationExpr{
Op: parser.STEP,
},
expected: 1 * time.Second,
},
{
name: "step multiplication",
expr: &parser.DurationExpr{
LHS: &parser.DurationExpr{
Op: parser.STEP,
},
RHS: &parser.NumberLiteral{Val: 3},
Op: parser.MUL,
},
expected: 3 * time.Second,
},
{ {
name: "division by zero", name: "division by zero",
expr: &parser.DurationExpr{ expr: &parser.DurationExpr{
@ -225,7 +243,8 @@ func TestCalculateDuration(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := calculateDuration(tt.expr, tt.allowedNegative) v := &durationVisitor{step: 1 * time.Second}
result, err := v.calculateDuration(tt.expr, tt.allowedNegative)
if tt.errorMessage != "" { if tt.errorMessage != "" {
require.Error(t, err) require.Error(t, err)
require.Contains(t, err.Error(), tt.errorMessage) require.Contains(t, err.Error(), tt.errorMessage)

View File

@ -481,7 +481,7 @@ func (ng *Engine) SetQueryLogger(l QueryLogger) {
// NewInstantQuery returns an evaluation query for the given expression at the given time. // NewInstantQuery returns an evaluation query for the given expression at the given time.
func (ng *Engine) NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error) { func (ng *Engine) NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error) {
pExpr, qry := ng.newQuery(q, qs, opts, ts, ts, 0) pExpr, qry := ng.newQuery(q, qs, opts, ts, ts, 0*time.Second)
finishQueue, err := ng.queueActive(ctx, qry) finishQueue, err := ng.queueActive(ctx, qry)
if err != nil { if err != nil {
return nil, err return nil, err
@ -494,7 +494,7 @@ func (ng *Engine) NewInstantQuery(ctx context.Context, q storage.Queryable, opts
if err := ng.validateOpts(expr); err != nil { if err := ng.validateOpts(expr); err != nil {
return nil, err return nil, err
} }
*pExpr, err = PreprocessExpr(expr, ts, ts) *pExpr, err = PreprocessExpr(expr, ts, ts, 0)
return qry, err return qry, err
} }
@ -518,7 +518,7 @@ func (ng *Engine) NewRangeQuery(ctx context.Context, q storage.Queryable, opts Q
if expr.Type() != parser.ValueTypeVector && expr.Type() != parser.ValueTypeScalar { if expr.Type() != parser.ValueTypeVector && expr.Type() != parser.ValueTypeScalar {
return nil, fmt.Errorf("invalid expression type %q for range query, must be Scalar or instant Vector", parser.DocumentedType(expr.Type())) return nil, fmt.Errorf("invalid expression type %q for range query, must be Scalar or instant Vector", parser.DocumentedType(expr.Type()))
} }
*pExpr, err = PreprocessExpr(expr, start, end) *pExpr, err = PreprocessExpr(expr, start, end, interval)
return qry, err return qry, err
} }
@ -3730,10 +3730,10 @@ func unwrapStepInvariantExpr(e parser.Expr) parser.Expr {
// PreprocessExpr wraps all possible step invariant parts of the given expression with // PreprocessExpr wraps all possible step invariant parts of the given expression with
// StepInvariantExpr. It also resolves the preprocessors and evaluates duration expressions // StepInvariantExpr. It also resolves the preprocessors and evaluates duration expressions
// into their numeric values. // into their numeric values.
func PreprocessExpr(expr parser.Expr, start, end time.Time) (parser.Expr, error) { func PreprocessExpr(expr parser.Expr, start, end time.Time, step time.Duration) (parser.Expr, error) {
detectHistogramStatsDecoding(expr) detectHistogramStatsDecoding(expr)
if err := parser.Walk(&durationVisitor{}, expr, nil); err != nil { if err := parser.Walk(&durationVisitor{step: step}, expr, nil); err != nil {
return nil, err return nil, err
} }

View File

@ -3088,7 +3088,7 @@ func TestPreprocessAndWrapWithStepInvariantExpr(t *testing.T) {
t.Run(test.input, func(t *testing.T) { t.Run(test.input, func(t *testing.T) {
expr, err := parser.ParseExpr(test.input) expr, err := parser.ParseExpr(test.input)
require.NoError(t, err) require.NoError(t, err)
expr, err = promql.PreprocessExpr(expr, startTime, endTime) expr, err = promql.PreprocessExpr(expr, startTime, endTime, 0)
require.NoError(t, err) require.NoError(t, err)
if test.outputTest { if test.outputTest {
require.Equal(t, test.input, expr.String(), "error on input '%s'", test.input) require.Equal(t, test.input, expr.String(), "error on input '%s'", test.input)

View File

@ -116,7 +116,8 @@ type DurationExpr struct {
LHS, RHS Expr // The operands on the respective sides of the operator. LHS, RHS Expr // The operands on the respective sides of the operator.
Wrapped bool // Set when the duration is wrapped in parentheses. Wrapped bool // Set when the duration is wrapped in parentheses.
StartPos posrange.Pos // For unary operations, the position of the operator. StartPos posrange.Pos // For unary operations and step(), the start position of the operator.
EndPos posrange.Pos // For step(), the end position of the operator.
} }
// Call represents a function call. // Call represents a function call.
@ -455,6 +456,18 @@ func (e *BinaryExpr) PositionRange() posrange.PositionRange {
} }
func (e *DurationExpr) PositionRange() posrange.PositionRange { func (e *DurationExpr) PositionRange() posrange.PositionRange {
if e.Op == STEP {
return posrange.PositionRange{
Start: e.StartPos,
End: e.EndPos,
}
}
if e.RHS == nil {
return posrange.PositionRange{
Start: e.StartPos,
End: e.RHS.PositionRange().End,
}
}
if e.LHS == nil { if e.LHS == nil {
return posrange.PositionRange{ return posrange.PositionRange{
Start: e.StartPos, Start: e.StartPos,

View File

@ -150,6 +150,7 @@ WITHOUT
%token <item> %token <item>
START START
END END
STEP
%token preprocessorEnd %token preprocessorEnd
// Counter reset hints. // Counter reset hints.
@ -174,7 +175,7 @@ START_METRIC_SELECTOR
// Type definitions for grammar rules. // Type definitions for grammar rules.
%type <matchers> label_match_list %type <matchers> label_match_list
%type <matcher> label_matcher %type <matcher> label_matcher
%type <item> aggregate_op grouping_label match_op maybe_label metric_identifier unary_op at_modifier_preprocessors string_identifier counter_reset_hint %type <item> aggregate_op grouping_label match_op maybe_label metric_identifier unary_op at_modifier_preprocessors string_identifier counter_reset_hint min_max
%type <labels> label_set metric %type <labels> label_set metric
%type <lblList> label_set_list %type <lblList> label_set_list
%type <label> label_set_item %type <label> label_set_item
@ -478,7 +479,7 @@ offset_expr: expr OFFSET offset_duration_expr
$$ = $1 $$ = $1
} }
| expr OFFSET error | expr OFFSET error
{ yylex.(*parser).unexpected("offset", "number or duration"); $$ = $1 } { yylex.(*parser).unexpected("offset", "number, duration, or step()"); $$ = $1 }
; ;
/* /*
@ -574,11 +575,11 @@ subquery_expr : expr LEFT_BRACKET positive_duration_expr COLON positive_durati
| expr LEFT_BRACKET positive_duration_expr COLON positive_duration_expr error | expr LEFT_BRACKET positive_duration_expr COLON positive_duration_expr error
{ yylex.(*parser).unexpected("subquery selector", "\"]\""); $$ = $1 } { yylex.(*parser).unexpected("subquery selector", "\"]\""); $$ = $1 }
| expr LEFT_BRACKET positive_duration_expr COLON error | expr LEFT_BRACKET positive_duration_expr COLON error
{ yylex.(*parser).unexpected("subquery selector", "number or duration or \"]\""); $$ = $1 } { yylex.(*parser).unexpected("subquery selector", "number, duration, or step() or \"]\""); $$ = $1 }
| expr LEFT_BRACKET positive_duration_expr error | expr LEFT_BRACKET positive_duration_expr error
{ yylex.(*parser).unexpected("subquery or range", "\":\" or \"]\""); $$ = $1 } { yylex.(*parser).unexpected("subquery or range", "\":\" or \"]\""); $$ = $1 }
| expr LEFT_BRACKET error | expr LEFT_BRACKET error
{ yylex.(*parser).unexpected("subquery selector", "number or duration"); $$ = $1 } { yylex.(*parser).unexpected("subquery or range selector", "number, duration, or step()"); $$ = $1 }
; ;
/* /*
@ -695,7 +696,7 @@ metric : metric_identifier label_set
; ;
metric_identifier: AVG | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | IDENTIFIER | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | QUANTILE | STDDEV | STDVAR | SUM | TOPK | WITHOUT | START | END | LIMITK | LIMIT_RATIO; metric_identifier: AVG | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | IDENTIFIER | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | QUANTILE | STDDEV | STDVAR | SUM | TOPK | WITHOUT | START | END | LIMITK | LIMIT_RATIO | STEP;
label_set : LEFT_BRACE label_set_list RIGHT_BRACE label_set : LEFT_BRACE label_set_list RIGHT_BRACE
{ $$ = labels.New($2...) } { $$ = labels.New($2...) }
@ -952,7 +953,7 @@ counter_reset_hint : UNKNOWN_COUNTER_RESET | COUNTER_RESET | NOT_COUNTER_RESET |
aggregate_op : AVG | BOTTOMK | COUNT | COUNT_VALUES | GROUP | MAX | MIN | QUANTILE | STDDEV | STDVAR | SUM | TOPK | LIMITK | LIMIT_RATIO; aggregate_op : AVG | BOTTOMK | COUNT | COUNT_VALUES | GROUP | MAX | MIN | QUANTILE | STDDEV | STDVAR | SUM | TOPK | LIMITK | LIMIT_RATIO;
// Inside of grouping options label names can be recognized as keywords by the lexer. This is a list of keywords that could also be a label name. // Inside of grouping options label names can be recognized as keywords by the lexer. This is a list of keywords that could also be a label name.
maybe_label : AVG | BOOL | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | GROUP_LEFT | GROUP_RIGHT | IDENTIFIER | IGNORING | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | ON | QUANTILE | STDDEV | STDVAR | SUM | TOPK | START | END | ATAN2 | LIMITK | LIMIT_RATIO; maybe_label : AVG | BOOL | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | GROUP_LEFT | GROUP_RIGHT | IDENTIFIER | IGNORING | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | ON | QUANTILE | STDDEV | STDVAR | SUM | TOPK | START | END | ATAN2 | LIMITK | LIMIT_RATIO | STEP;
unary_op : ADD | SUB; unary_op : ADD | SUB;
@ -1079,9 +1080,70 @@ offset_duration_expr : number_duration_literal
nl.PosRange.Start = $1.Pos nl.PosRange.Start = $1.Pos
$$ = nl $$ = nl
} }
| STEP LEFT_PAREN RIGHT_PAREN
{
$$ = &DurationExpr{
Op: STEP,
StartPos: $1.PositionRange().Start,
EndPos: $3.PositionRange().End,
}
}
| unary_op STEP LEFT_PAREN RIGHT_PAREN
{
$$ = &DurationExpr{
Op: $1.Typ,
RHS: &DurationExpr{
Op: STEP,
StartPos: $2.PositionRange().Start,
EndPos: $4.PositionRange().End,
},
StartPos: $1.Pos,
}
}
| min_max LEFT_PAREN duration_expr COMMA duration_expr RIGHT_PAREN
{
$$ = &DurationExpr{
Op: $1.Typ,
StartPos: $1.PositionRange().Start,
EndPos: $6.PositionRange().End,
LHS: $3.(Expr),
RHS: $5.(Expr),
}
}
| unary_op min_max LEFT_PAREN duration_expr COMMA duration_expr RIGHT_PAREN
{
$$ = &DurationExpr{
Op: $1.Typ,
StartPos: $1.Pos,
EndPos: $6.PositionRange().End,
RHS: &DurationExpr{
Op: $2.Typ,
StartPos: $2.PositionRange().Start,
EndPos: $6.PositionRange().End,
LHS: $4.(Expr),
RHS: $6.(Expr),
},
}
}
| unary_op LEFT_PAREN duration_expr RIGHT_PAREN %prec MUL
{
de := $3.(*DurationExpr)
de.Wrapped = true
if $1.Typ == SUB {
$$ = &DurationExpr{
Op: SUB,
RHS: de,
StartPos: $1.Pos,
}
break
}
$$ = $3
}
| duration_expr | duration_expr
; ;
min_max: MIN | MAX ;
duration_expr : number_duration_literal duration_expr : number_duration_literal
{ {
nl := $1.(*NumberLiteral) nl := $1.(*NumberLiteral)
@ -1164,6 +1226,24 @@ duration_expr : number_duration_literal
yylex.(*parser).experimentalDurationExpr($1.(Expr)) yylex.(*parser).experimentalDurationExpr($1.(Expr))
$$ = &DurationExpr{Op: POW, LHS: $1.(Expr), RHS: $3.(Expr)} $$ = &DurationExpr{Op: POW, LHS: $1.(Expr), RHS: $3.(Expr)}
} }
| STEP LEFT_PAREN RIGHT_PAREN
{
$$ = &DurationExpr{
Op: STEP,
StartPos: $1.PositionRange().Start,
EndPos: $3.PositionRange().Start,
}
}
| min_max LEFT_PAREN duration_expr COMMA duration_expr RIGHT_PAREN
{
$$ = &DurationExpr{
Op: $1.Typ,
StartPos: $1.PositionRange().Start,
EndPos: $6.PositionRange().Start,
LHS: $3.(Expr),
RHS: $5.(Expr),
}
}
| paren_duration_expr | paren_duration_expr
; ;

File diff suppressed because it is too large Load Diff

View File

@ -140,6 +140,7 @@ var key = map[string]ItemType{
// Preprocessors. // Preprocessors.
"start": START, "start": START,
"end": END, "end": END,
"step": STEP,
} }
var histogramDesc = map[string]ItemType{ var histogramDesc = map[string]ItemType{
@ -462,11 +463,20 @@ func lexStatements(l *Lexer) stateFn {
l.backup() l.backup()
return lexKeywordOrIdentifier return lexKeywordOrIdentifier
} }
if l.gotColon { switch r {
return l.errorf("unexpected colon %q", r) case ':':
if l.gotColon {
return l.errorf("unexpected colon %q", r)
}
l.emit(COLON)
l.gotColon = true
return lexStatements
case 's', 'S', 'm', 'M':
if l.scanDurationKeyword() {
return lexStatements
}
} }
l.emit(COLON) return l.errorf("unexpected character: %q, expected %q", r, ':')
l.gotColon = true
case r == '(': case r == '(':
l.emit(LEFT_PAREN) l.emit(LEFT_PAREN)
l.parenDepth++ l.parenDepth++
@ -889,6 +899,32 @@ func lexNumber(l *Lexer) stateFn {
return lexStatements return lexStatements
} }
func (l *Lexer) scanDurationKeyword() bool {
for {
switch r := l.next(); {
case isAlpha(r):
// absorb.
default:
l.backup()
word := l.input[l.start:l.pos]
kw := strings.ToLower(word)
switch kw {
case "step":
l.emit(STEP)
return true
case "min":
l.emit(MIN)
return true
case "max":
l.emit(MAX)
return true
default:
return false
}
}
}
}
// lexNumberOrDuration scans a number or a duration Item. // lexNumberOrDuration scans a number or a duration Item.
func lexNumberOrDuration(l *Lexer) stateFn { func lexNumberOrDuration(l *Lexer) stateFn {
if l.scanNumber() { if l.scanNumber() {
@ -1133,6 +1169,14 @@ func lexDurationExpr(l *Lexer) stateFn {
case r == '^': case r == '^':
l.emit(POW) l.emit(POW)
return lexDurationExpr return lexDurationExpr
case r == ',':
l.emit(COMMA)
return lexDurationExpr
case r == 's' || r == 'S' || r == 'm' || r == 'M':
if l.scanDurationKeyword() {
return lexDurationExpr
}
return l.errorf("unexpected character in duration expression: %q", r)
case isDigit(r) || (r == '.' && isDigit(l.peek())): case isDigit(r) || (r == '.' && isDigit(l.peek())):
l.backup() l.backup()
l.gotDuration = true l.gotDuration = true

View File

@ -614,6 +614,43 @@ var testExpr = []struct {
fail: true, fail: true,
errMsg: "1:11: parse error: unexpected <ignoring>", errMsg: "1:11: parse error: unexpected <ignoring>",
}, },
// Vector selectors.
{
input: `offset{step="1s"}[5m]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "offset",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, "step", "1s"),
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "offset"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 17,
},
},
Range: 5 * time.Minute,
EndPos: 21,
},
},
{
input: `step{offset="1s"}[5m]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "step",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, "offset", "1s"),
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "step"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 17,
},
},
Range: 5 * time.Minute,
EndPos: 21,
},
},
// Vector binary operations. // Vector binary operations.
{ {
input: "foo * bar", input: "foo * bar",
@ -2388,7 +2425,7 @@ var testExpr = []struct {
{ {
input: `foo[]`, input: `foo[]`,
fail: true, fail: true,
errMsg: "unexpected \"]\" in subquery selector, expected number or duration", errMsg: "unexpected \"]\" in subquery or range selector, expected number, duration, or step()",
}, },
{ {
input: `foo[-1]`, input: `foo[-1]`,
@ -2403,7 +2440,7 @@ var testExpr = []struct {
{ {
input: `some_metric[5m] OFFSET`, input: `some_metric[5m] OFFSET`,
fail: true, fail: true,
errMsg: "unexpected end of input in offset, expected number or duration", errMsg: "1:23: parse error: unexpected end of input in offset, expected number, duration, or step()",
}, },
{ {
input: `some_metric OFFSET 1m[5m]`, input: `some_metric OFFSET 1m[5m]`,
@ -4131,6 +4168,242 @@ var testExpr = []struct {
EndPos: 13, EndPos: 13,
}, },
}, },
{
input: `foo[step()]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
RangeExpr: &DurationExpr{
Op: STEP,
StartPos: 4,
EndPos: 9,
},
EndPos: 11,
},
},
{
input: `foo[ - step ( ) ]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
RangeExpr: &DurationExpr{
Op: SUB,
StartPos: 6,
RHS: &DurationExpr{
Op: STEP,
StartPos: 9,
EndPos: 18,
},
},
EndPos: 22,
},
},
{
input: `foo[ step ( ) ]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
RangeExpr: &DurationExpr{
Op: STEP,
StartPos: 7,
EndPos: 16,
},
EndPos: 20,
},
},
{
input: `foo[-step()]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
RangeExpr: &DurationExpr{
Op: SUB,
StartPos: 4,
RHS: &DurationExpr{Op: STEP, StartPos: 5, EndPos: 10},
},
EndPos: 12,
},
},
{
input: `foo offset step()`,
expected: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 17,
},
OriginalOffsetExpr: &DurationExpr{
Op: STEP,
StartPos: 11,
EndPos: 17,
},
},
},
{
input: `foo offset -step()`,
expected: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 18,
},
OriginalOffsetExpr: &DurationExpr{
Op: SUB,
StartPos: 11,
RHS: &DurationExpr{Op: STEP, StartPos: 12, EndPos: 18},
},
},
},
{
input: `foo[max(step(),5s)]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 3,
},
},
RangeExpr: &DurationExpr{
Op: MAX,
LHS: &DurationExpr{
Op: STEP,
StartPos: 8,
EndPos: 13,
},
RHS: &NumberLiteral{
Val: 5,
Duration: true,
PosRange: posrange.PositionRange{
Start: 15,
End: 17,
},
},
StartPos: 4,
EndPos: 17,
},
EndPos: 19,
},
},
{
input: `foo offset max(step(),5s)`,
expected: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 25,
},
OriginalOffsetExpr: &DurationExpr{
Op: MAX,
LHS: &DurationExpr{
Op: STEP,
StartPos: 15,
EndPos: 20,
},
RHS: &NumberLiteral{
Val: 5,
Duration: true,
PosRange: posrange.PositionRange{
Start: 22,
End: 24,
},
},
StartPos: 11,
EndPos: 25,
},
},
},
{
input: `foo offset -min(5s,step()+8s)`,
expected: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{
Start: 0,
End: 29,
},
OriginalOffsetExpr: &DurationExpr{
Op: SUB,
RHS: &DurationExpr{
Op: MIN,
LHS: &NumberLiteral{
Val: 5,
Duration: true,
PosRange: posrange.PositionRange{
Start: 16,
End: 18,
},
},
RHS: &DurationExpr{
Op: ADD,
LHS: &DurationExpr{
Op: STEP,
StartPos: 19,
EndPos: 24,
},
RHS: &NumberLiteral{
Val: 8,
Duration: true,
PosRange: posrange.PositionRange{
Start: 26,
End: 28,
},
},
},
StartPos: 12,
EndPos: 28,
},
StartPos: 11,
EndPos: 28,
},
},
},
{ {
input: `foo[4s+4s:1s*2] offset (5s-8)`, input: `foo[4s+4s:1s*2] offset (5s-8)`,
expected: &SubqueryExpr{ expected: &SubqueryExpr{
@ -4453,6 +4726,16 @@ var testExpr = []struct {
EndPos: 11, EndPos: 11,
}, },
}, },
{
input: `foo[step]`,
fail: true,
errMsg: `1:9: parse error: unexpected "]" in subquery or range selector, expected number, duration, or step()`,
},
{
input: `foo[step()/0d]`,
fail: true,
errMsg: `division by zero`,
},
{ {
input: `foo[5s/0d]`, input: `foo[5s/0d]`,
fail: true, fail: true,
@ -4545,6 +4828,16 @@ var testExpr = []struct {
fail: true, fail: true,
errMsg: "unclosed left parenthesis", errMsg: "unclosed left parenthesis",
}, },
{
input: "foo[5s x 5s]",
fail: true,
errMsg: "unexpected character: 'x', expected ':'",
},
{
input: "foo[5s s 5s]",
fail: true,
errMsg: "unexpected character: 's', expected ':'",
},
} }
func makeInt64Pointer(val int64) *int64 { func makeInt64Pointer(val int64) *int64 {

View File

@ -148,10 +148,17 @@ func (node *BinaryExpr) getMatchingStr() string {
func (node *DurationExpr) String() string { func (node *DurationExpr) String() string {
var expr string var expr string
if node.LHS == nil { switch {
// This is a unary negative duration expression. case node.Op == STEP:
expr = "step()"
case node.Op == MIN:
expr = fmt.Sprintf("min(%s, %s)", node.LHS, node.RHS)
case node.Op == MAX:
expr = fmt.Sprintf("max(%s, %s)", node.LHS, node.RHS)
case node.LHS == nil:
// This is a unary duration expression.
expr = fmt.Sprintf("%s%s", node.Op, node.RHS) expr = fmt.Sprintf("%s%s", node.Op, node.RHS)
} else { default:
expr = fmt.Sprintf("%s %s %s", node.LHS, node.Op, node.RHS) expr = fmt.Sprintf("%s %s %s", node.LHS, node.Op, node.RHS)
} }
if node.Wrapped { if node.Wrapped {

View File

@ -22,6 +22,10 @@ import (
) )
func TestExprString(t *testing.T) { func TestExprString(t *testing.T) {
ExperimentalDurationExpr = true
t.Cleanup(func() {
ExperimentalDurationExpr = false
})
// A list of valid expressions that are expected to be // A list of valid expressions that are expected to be
// returned as out when calling String(). If out is empty the output // returned as out when calling String(). If out is empty the output
// is expected to equal the input. // is expected to equal the input.
@ -167,6 +171,30 @@ func TestExprString(t *testing.T) {
in: "1048576", in: "1048576",
out: "1048576", out: "1048576",
}, },
{
in: "foo[step()]",
},
{
in: "foo[-step()]",
},
{
in: "foo[(step())]",
},
{
in: "foo[-(step())]",
},
{
in: "foo offset step()",
},
{
in: "foo offset -step()",
},
{
in: "foo offset (step())",
},
{
in: "foo offset -(step())",
},
} }
for _, test := range inputs { for _, test := range inputs {

View File

@ -145,4 +145,78 @@ eval instant at 1000s metric1_total offset -4
metric1_total{} 100 metric1_total{} 100
eval instant at 1000s metric1_total offset (-2 ^ 2) eval instant at 1000s metric1_total offset (-2 ^ 2)
metric1_total{} 100 metric1_total{} 100
clear
load 1s
metric1_total 0+1x100
eval range from 50s to 60s step 10s count_over_time(metric1_total[step()])
{} 10 10
eval range from 50s to 60s step 10s count_over_time(metric1_total[step()+1ms])
{} 11 11
eval range from 50s to 60s step 10s count_over_time(metric1_total[(step())+1])
{} 11 11
eval range from 50s to 60s step 10s count_over_time(metric1_total[1+(STep()-5)*2])
{} 11 11
eval range from 50s to 60s step 5s count_over_time(metric1_total[step()+1])
{} 6 6 6
eval range from 50s to 60s step 5s count_over_time(metric1_total[min(step()+1,1h)])
{} 6 6 6
eval range from 50s to 60s step 5s count_over_time(metric1_total[max(min(step()+1,1h),1ms)])
{} 6 6 6
eval range from 50s to 60s step 5s count_over_time(metric1_total[((max(min((step()+1),((1h))),1ms)))])
{} 6 6 6
eval range from 50s to 60s step 5s metric1_total offset STEP()
metric1_total{} 45 50 55
eval range from 50s to 60s step 5s metric1_total offset step()
metric1_total{} 45 50 55
eval range from 50s to 60s step 5s metric1_total offset step()*0
{} 0 0 0
eval range from 50s to 60s step 5s metric1_total offset (-step()*2)
metric1_total{} 60 65 70
eval range from 50s to 60s step 5s metric1_total offset -step()*2
{} 110 120 130
eval range from 50s to 60s step 5s metric1_total offset step()^0
{} 1 1 1
eval range from 50s to 60s step 5s metric1_total offset (STEP()/10)
metric1_total{} 49 54 59
eval range from 50s to 60s step 5s metric1_total offset (step())
metric1_total{} 45 50 55
eval range from 50s to 60s step 5s metric1_total offset min(step(), 1s)
metric1_total{} 49 54 59
eval range from 50s to 60s step 5s metric1_total offset min(step(), 1s)+8000
{} 8049 8054 8059
eval range from 50s to 60s step 5s metric1_total offset -min(step(), 1s)+8000
{} 8051 8056 8061
eval range from 50s to 60s step 5s metric1_total offset -(min(step(), 1s))+8000
{} 8051 8056 8061
eval range from 50s to 60s step 5s metric1_total offset -min(step(), 1s)^0
{} 1 1 1
eval range from 50s to 60s step 5s metric1_total offset max(3s,min(step(), 1s))+8000
{} 8047 8052 8057
eval range from 50s to 60s step 5s metric1_total offset -(min(step(), 2s)-5)+8000
{} 8047 8052 8057