mirror of
https://github.com/prometheus/prometheus.git
synced 2025-08-06 06:07:11 +02:00
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:
parent
bfbae39931
commit
ee7d5158a7
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user