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
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
`--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
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.
The following operators are supported:
@ -222,8 +230,10 @@ Examples of equivalent durations:
* `1h / 2` is the equivalent to `30m` or `1800s`
* `4h % 3h` is the equivalent to `1h` or `3600s`
* `(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

View File

@ -22,13 +22,15 @@ import (
)
// 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) {
switch n := node.(type) {
case *parser.VectorSelector:
if n.OriginalOffsetExpr != nil {
duration, err := calculateDuration(n.OriginalOffsetExpr, true)
duration, err := v.calculateDuration(n.OriginalOffsetExpr, true)
if err != nil {
return nil, err
}
@ -36,7 +38,7 @@ func (v *durationVisitor) Visit(node parser.Node, _ []parser.Node) (parser.Visit
}
case *parser.MatrixSelector:
if n.RangeExpr != nil {
duration, err := calculateDuration(n.RangeExpr, false)
duration, err := v.calculateDuration(n.RangeExpr, false)
if err != nil {
return nil, err
}
@ -44,21 +46,21 @@ func (v *durationVisitor) Visit(node parser.Node, _ []parser.Node) (parser.Visit
}
case *parser.SubqueryExpr:
if n.OriginalOffsetExpr != nil {
duration, err := calculateDuration(n.OriginalOffsetExpr, true)
duration, err := v.calculateDuration(n.OriginalOffsetExpr, true)
if err != nil {
return nil, err
}
n.OriginalOffset = duration
}
if n.StepExpr != nil {
duration, err := calculateDuration(n.StepExpr, false)
duration, err := v.calculateDuration(n.StepExpr, false)
if err != nil {
return nil, err
}
n.Step = duration
}
if n.RangeExpr != nil {
duration, err := calculateDuration(n.RangeExpr, false)
duration, err := v.calculateDuration(n.RangeExpr, false)
if err != nil {
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.
func calculateDuration(expr parser.Expr, allowedNegative bool) (time.Duration, error) {
duration, err := evaluateDurationExpr(expr)
func (v *durationVisitor) calculateDuration(expr parser.Expr, allowedNegative bool) (time.Duration, error) {
duration, err := v.evaluateDurationExpr(expr)
if err != nil {
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.
func evaluateDurationExpr(expr parser.Expr) (float64, error) {
func (v *durationVisitor) evaluateDurationExpr(expr parser.Expr) (float64, error) {
switch n := expr.(type) {
case *parser.NumberLiteral:
return n.Val, nil
@ -93,19 +95,31 @@ func evaluateDurationExpr(expr parser.Expr) (float64, error) {
var err error
if n.LHS != nil {
lhs, err = evaluateDurationExpr(n.LHS)
lhs, err = v.evaluateDurationExpr(n.LHS)
if err != nil {
return 0, err
}
}
rhs, err = evaluateDurationExpr(n.RHS)
if err != nil {
return 0, err
if n.RHS != nil {
rhs, err = v.evaluateDurationExpr(n.RHS)
if err != nil {
return 0, err
}
}
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:
if n.LHS == nil {
// Unary positive duration expression.
return rhs, nil
}
return lhs + rhs, nil
case parser.SUB:
if n.LHS == nil {

View File

@ -195,6 +195,24 @@ func TestCalculateDuration(t *testing.T) {
expected: -5 * time.Second,
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",
expr: &parser.DurationExpr{
@ -225,7 +243,8 @@ func TestCalculateDuration(t *testing.T) {
for _, tt := range tests {
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 != "" {
require.Error(t, err)
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.
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)
if err != nil {
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 {
return nil, err
}
*pExpr, err = PreprocessExpr(expr, ts, ts)
*pExpr, err = PreprocessExpr(expr, ts, ts, 0)
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 {
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
}
@ -3730,10 +3730,10 @@ func unwrapStepInvariantExpr(e parser.Expr) parser.Expr {
// PreprocessExpr wraps all possible step invariant parts of the given expression with
// StepInvariantExpr. It also resolves the preprocessors and evaluates duration expressions
// 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)
if err := parser.Walk(&durationVisitor{}, expr, nil); err != nil {
if err := parser.Walk(&durationVisitor{step: step}, expr, nil); err != nil {
return nil, err
}

View File

@ -3088,7 +3088,7 @@ func TestPreprocessAndWrapWithStepInvariantExpr(t *testing.T) {
t.Run(test.input, func(t *testing.T) {
expr, err := parser.ParseExpr(test.input)
require.NoError(t, err)
expr, err = promql.PreprocessExpr(expr, startTime, endTime)
expr, err = promql.PreprocessExpr(expr, startTime, endTime, 0)
require.NoError(t, err)
if test.outputTest {
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.
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.
@ -455,6 +456,18 @@ func (e *BinaryExpr) 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 {
return posrange.PositionRange{
Start: e.StartPos,

View File

@ -150,6 +150,7 @@ WITHOUT
%token <item>
START
END
STEP
%token preprocessorEnd
// Counter reset hints.
@ -174,7 +175,7 @@ START_METRIC_SELECTOR
// Type definitions for grammar rules.
%type <matchers> label_match_list
%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 <lblList> label_set_list
%type <label> label_set_item
@ -478,7 +479,7 @@ offset_expr: expr OFFSET offset_duration_expr
$$ = $1
}
| 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
{ yylex.(*parser).unexpected("subquery selector", "\"]\""); $$ = $1 }
| 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
{ yylex.(*parser).unexpected("subquery or range", "\":\" or \"]\""); $$ = $1 }
| 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
{ $$ = 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;
// 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;
@ -1079,9 +1080,70 @@ offset_duration_expr : number_duration_literal
nl.PosRange.Start = $1.Pos
$$ = 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
;
min_max: MIN | MAX ;
duration_expr : number_duration_literal
{
nl := $1.(*NumberLiteral)
@ -1164,6 +1226,24 @@ duration_expr : number_duration_literal
yylex.(*parser).experimentalDurationExpr($1.(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
;

File diff suppressed because it is too large Load Diff

View File

@ -140,6 +140,7 @@ var key = map[string]ItemType{
// Preprocessors.
"start": START,
"end": END,
"step": STEP,
}
var histogramDesc = map[string]ItemType{
@ -462,11 +463,20 @@ func lexStatements(l *Lexer) stateFn {
l.backup()
return lexKeywordOrIdentifier
}
if l.gotColon {
return l.errorf("unexpected colon %q", r)
switch 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)
l.gotColon = true
return l.errorf("unexpected character: %q, expected %q", r, ':')
case r == '(':
l.emit(LEFT_PAREN)
l.parenDepth++
@ -889,6 +899,32 @@ func lexNumber(l *Lexer) stateFn {
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.
func lexNumberOrDuration(l *Lexer) stateFn {
if l.scanNumber() {
@ -1133,6 +1169,14 @@ func lexDurationExpr(l *Lexer) stateFn {
case r == '^':
l.emit(POW)
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())):
l.backup()
l.gotDuration = true

View File

@ -614,6 +614,43 @@ var testExpr = []struct {
fail: true,
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.
{
input: "foo * bar",
@ -2388,7 +2425,7 @@ var testExpr = []struct {
{
input: `foo[]`,
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]`,
@ -2403,7 +2440,7 @@ var testExpr = []struct {
{
input: `some_metric[5m] OFFSET`,
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]`,
@ -4131,6 +4168,242 @@ var testExpr = []struct {
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)`,
expected: &SubqueryExpr{
@ -4453,6 +4726,16 @@ var testExpr = []struct {
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]`,
fail: true,
@ -4545,6 +4828,16 @@ var testExpr = []struct {
fail: true,
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 {

View File

@ -148,10 +148,17 @@ func (node *BinaryExpr) getMatchingStr() string {
func (node *DurationExpr) String() string {
var expr string
if node.LHS == nil {
// This is a unary negative duration expression.
switch {
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)
} else {
default:
expr = fmt.Sprintf("%s %s %s", node.LHS, node.Op, node.RHS)
}
if node.Wrapped {

View File

@ -22,6 +22,10 @@ import (
)
func TestExprString(t *testing.T) {
ExperimentalDurationExpr = true
t.Cleanup(func() {
ExperimentalDurationExpr = false
})
// A list of valid expressions that are expected to be
// returned as out when calling String(). If out is empty the output
// is expected to equal the input.
@ -167,6 +171,30 @@ func TestExprString(t *testing.T) {
in: "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 {

View File

@ -145,4 +145,78 @@ eval instant at 1000s metric1_total offset -4
metric1_total{} 100
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