diff --git a/docs/feature_flags.md b/docs/feature_flags.md index 08981ee931..e2a64915a6 100644 --- a/docs/feature_flags.md +++ b/docs/feature_flags.md @@ -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(, )` and `max(, )` 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 diff --git a/promql/durations.go b/promql/durations.go index 8431fa5bd4..20fa095d53 100644 --- a/promql/durations.go +++ b/promql/durations.go @@ -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 { diff --git a/promql/durations_test.go b/promql/durations_test.go index 0cdfb7597a..18592a0d0a 100644 --- a/promql/durations_test.go +++ b/promql/durations_test.go @@ -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) diff --git a/promql/engine.go b/promql/engine.go index adf49ed96e..275fc34a40 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -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 } diff --git a/promql/engine_test.go b/promql/engine_test.go index f352d5999c..ce5ef6efd7 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -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) diff --git a/promql/parser/ast.go b/promql/parser/ast.go index 9eebaed9ab..dc3e36b5b5 100644 --- a/promql/parser/ast.go +++ b/promql/parser/ast.go @@ -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, diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y index fc901374e2..982f94aae1 100644 --- a/promql/parser/generated_parser.y +++ b/promql/parser/generated_parser.y @@ -150,6 +150,7 @@ WITHOUT %token START END +STEP %token preprocessorEnd // Counter reset hints. @@ -174,7 +175,7 @@ START_METRIC_SELECTOR // Type definitions for grammar rules. %type label_match_list %type label_matcher -%type aggregate_op grouping_label match_op maybe_label metric_identifier unary_op at_modifier_preprocessors string_identifier counter_reset_hint +%type aggregate_op grouping_label match_op maybe_label metric_identifier unary_op at_modifier_preprocessors string_identifier counter_reset_hint min_max %type label_set metric %type label_set_list %type