From 7b29b912e3712cb06e93d3c2db7f77cc2b4fee96 Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Wed, 13 May 2026 09:29:32 +0200 Subject: [PATCH 1/3] Revert "PromQL: Promote duration expressions as stable" This reverts commit 1463a5bb5a6f837e56f83d83f9f0726804a62e56. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- cmd/prometheus/main.go | 5 +- cmd/prometheus/testdata/features.json | 2 +- cmd/promtool/main.go | 4 +- docs/command-line/prometheus.md | 2 +- docs/command-line/promtool.md | 2 +- docs/feature_flags.md | 57 +++++++++++++++ docs/querying/basics.md | 69 +++---------------- docs/querying/functions.md | 3 +- promql/durations_test.go | 2 +- promql/parser/features.go | 4 +- promql/parser/generated_parser.y | 7 ++ promql/parser/generated_parser.y.go | 7 ++ promql/parser/parse.go | 7 ++ promql/parser/parse_test.go | 2 + promql/parser/prettier_test.go | 2 +- promql/parser/printer_test.go | 1 + promql/promqltest/test.go | 1 + util/fuzzing/fuzz_test.go | 1 + web/ui/mantine-ui/src/promql/functionDocs.tsx | 48 ++++++++----- 19 files changed, 138 insertions(+), 88 deletions(-) diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index fcb655fb1e..6066a5be1d 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -260,7 +260,8 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error { c.parserOpts.EnableExperimentalFunctions = true logger.Info("Experimental PromQL functions enabled.") case "promql-duration-expr": - logger.Warn("This option for --enable-feature is now permanently enabled and therefore a no-op.", "option", o) + c.parserOpts.ExperimentalDurationExpr = true + logger.Info("Experimental duration expression parsing enabled.") case "native-histograms": logger.Warn("This option for --enable-feature is a no-op. To scrape native histograms, set the scrape_native_histograms scrape config setting to true.", "option", o) case "ooo-native-histograms": @@ -619,7 +620,7 @@ func main() { a.Flag("scrape.discovery-reload-interval", "Interval used by scrape manager to throttle target groups updates."). Hidden().Default("5s").SetValue(&cfg.scrape.DiscoveryReloadInterval) - a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: concurrent-rule-eval, created-timestamp-zero-ingestion, delayed-compaction, exemplar-storage, extra-scrape-metrics, memory-snapshot-on-shutdown, metadata-wal-records, old-ui, otlp-deltatocumulative, otlp-native-delta-ingestion, promql-binop-fill-modifiers, promql-delayed-name-removal, promql-experimental-functions, promql-extended-range-selectors, promql-per-step-stats, st-storage, type-and-unit-labels, use-start-timestamps, use-uncached-io, xor2-encoding. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details."). + a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: concurrent-rule-eval, created-timestamp-zero-ingestion, delayed-compaction, exemplar-storage, extra-scrape-metrics, memory-snapshot-on-shutdown, metadata-wal-records, old-ui, otlp-deltatocumulative, otlp-native-delta-ingestion, promql-binop-fill-modifiers, promql-delayed-name-removal, promql-duration-expr, promql-experimental-functions, promql-extended-range-selectors, promql-per-step-stats, st-storage, type-and-unit-labels, use-start-timestamps, use-uncached-io, xor2-encoding. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details."). StringsVar(&cfg.featureList) a.Flag("agent", "Run Prometheus in 'Agent mode'.").BoolVar(&agentMode) diff --git a/cmd/prometheus/testdata/features.json b/cmd/prometheus/testdata/features.json index 90bf375781..78c6b5bdc8 100644 --- a/cmd/prometheus/testdata/features.json +++ b/cmd/prometheus/testdata/features.json @@ -29,7 +29,7 @@ "bool": true, "by": true, "delayed_name_removal": false, - "duration_expr": true, + "duration_expr": false, "fill": false, "fill_left": false, "fill_right": false, diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go index 88ee3be3f7..fbea5eda6a 100644 --- a/cmd/promtool/main.go +++ b/cmd/promtool/main.go @@ -320,7 +320,7 @@ func main() { promQLLabelsDeleteQuery := promQLLabelsDeleteCmd.Arg("query", "PromQL query.").Required().String() promQLLabelsDeleteName := promQLLabelsDeleteCmd.Arg("name", "Name of the label to delete.").Required().String() - featureList := app.Flag("enable-feature", "Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal, promql-extended-range-selectors. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details").Default("").Strings() + featureList := app.Flag("enable-feature", "Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal, promql-duration-expr, promql-extended-range-selectors. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details").Default("").Strings() documentationCmd := app.Command("write-documentation", "Generate command line documentation. Internal use.").Hidden() @@ -358,7 +358,7 @@ func main() { case "promql-delayed-name-removal": promqlEnableDelayedNameRemoval = true case "promql-duration-expr": - fmt.Printf(" WARNING: promql-duration-expr is now permanently enabled and therefore a no-op") + promtoolParserOpts.ExperimentalDurationExpr = true case "promql-extended-range-selectors": promtoolParserOpts.EnableExtendedRangeSelectors = true case "": diff --git a/docs/command-line/prometheus.md b/docs/command-line/prometheus.md index e21c4c2a67..289ded3842 100644 --- a/docs/command-line/prometheus.md +++ b/docs/command-line/prometheus.md @@ -62,7 +62,7 @@ The Prometheus monitoring server | --query.timeout | Maximum time a query may take before being aborted. Use with server mode only. | `2m` | | --query.max-concurrency | Maximum number of queries executed concurrently. Use with server mode only. | `20` | | --query.max-samples | Maximum number of samples a single query can load into memory. Note that queries will fail if they try to load more samples than this into memory, so this also limits the number of samples a query can return. Use with server mode only. | `50000000` | -| --enable-feature ... | Comma separated feature names to enable. Valid options: concurrent-rule-eval, created-timestamp-zero-ingestion, delayed-compaction, exemplar-storage, extra-scrape-metrics, memory-snapshot-on-shutdown, metadata-wal-records, old-ui, otlp-deltatocumulative, otlp-native-delta-ingestion, promql-binop-fill-modifiers, promql-delayed-name-removal, promql-experimental-functions, promql-extended-range-selectors, promql-per-step-stats, st-storage, type-and-unit-labels, use-start-timestamps, use-uncached-io, xor2-encoding. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | | +| --enable-feature ... | Comma separated feature names to enable. Valid options: concurrent-rule-eval, created-timestamp-zero-ingestion, delayed-compaction, exemplar-storage, extra-scrape-metrics, memory-snapshot-on-shutdown, metadata-wal-records, old-ui, otlp-deltatocumulative, otlp-native-delta-ingestion, promql-binop-fill-modifiers, promql-delayed-name-removal, promql-duration-expr, promql-experimental-functions, promql-extended-range-selectors, promql-per-step-stats, st-storage, type-and-unit-labels, use-start-timestamps, use-uncached-io, xor2-encoding. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details. | | | --agent | Run Prometheus in 'Agent mode'. | | | --log.level | Only log messages with the given severity or above. One of: [debug, info, warn, error] | `info` | | --log.format | Output format of log messages. One of: [logfmt, json] | `logfmt` | diff --git a/docs/command-line/promtool.md b/docs/command-line/promtool.md index aa606435f8..28a0d99696 100644 --- a/docs/command-line/promtool.md +++ b/docs/command-line/promtool.md @@ -12,7 +12,7 @@ Tooling for the Prometheus monitoring system. | -h, --help | Show context-sensitive help (also try --help-long and --help-man). | | --version | Show application version. | | --experimental | Enable experimental commands. | -| --enable-feature ... | Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal, promql-extended-range-selectors. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details | +| --enable-feature ... | Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal, promql-duration-expr, promql-extended-range-selectors. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details | diff --git a/docs/feature_flags.md b/docs/feature_flags.md index fddb4dcf6b..2806e4baa7 100644 --- a/docs/feature_flags.md +++ b/docs/feature_flags.md @@ -192,6 +192,63 @@ 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` + +With this flag, arithmetic expressions can be used in time durations in range queries and offset durations. + +In range queries: +``` +rate(http_requests_total[5m * 2]) # 10 minute range +rate(http_requests_total[(5+2) * 1m]) # 7 minute range +``` + +In offset durations: +``` +http_requests_total offset (1h / 2) # 30 minute offset +http_requests_total offset ((2 ^ 3) * 1m) # 8 minute offset +``` + +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`. + +`range()` can be used in duration expressions. +For a **range query**, it resolves to the full range of the query (end time - start time). +For an **instant query**, it resolves to `0s`. +This is particularly useful in combination with `@end()` to look back over the entire query range, e.g., `max_over_time(metric[range()] @ end())`. + +`min(, )` and `max(, )` 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: + +* `+` - addition +* `-` - subtraction +* `*` - multiplication +* `/` - division +* `%` - modulo +* `^` - exponentiation + +Examples of equivalent durations: + +* `5m * 2` is equivalent to `10m` or `600s` +* `10m - 1m` is equivalent to `9m` or `540s` +* `(5+2) * 1m` is equivalent to `7m` or `420s` +* `1h / 2` is equivalent to `30m` or `1800s` +* `4h % 3h` is equivalent to `1h` or `3600s` +* `(2 ^ 3) * 1m` is equivalent to `8m` or `480s` +* `step() + 1` is equivalent to the query step width increased by 1s. +* `max(step(), 5s)` is equivalent to the larger of the query step width and `5s`. +* `min(2 * step() + 5s, 5m)` is equivalent to the smaller of twice the query step increased by `5s` and `5m`. + + ## OTLP Native Delta Support `--enable-feature=otlp-native-delta-ingestion` diff --git a/docs/querying/basics.md b/docs/querying/basics.md index c3fcfee432..e7f1173af4 100644 --- a/docs/querying/basics.md +++ b/docs/querying/basics.md @@ -174,57 +174,6 @@ Examples: 12h34m56s # Equivalent to 45296s and thus 45296. 54s321ms # Equivalent to 54.321. -#### Duration expressions - -Duration expressions can be used in range selectors, subquery range and -resolution fields, and offset durations. - -Examples: - - rate(http_requests_total[5m * 2]) # 10 minute range. - rate(http_requests_total[(5 + 2) * 1m]) # 7 minute range. - http_requests_total offset (1h / 2) # 30 minute offset. - http_requests_total offset ((2 ^ 3) * 1m) # 8 minute offset. - -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`. - -`range()` can be used in duration expressions. For a range query, it resolves to -the full range of the query (end time minus start time). For an instant query, -it resolves to `0s`. This is particularly useful in combination with `@ end()` -to look back over the entire query range, e.g., -`max_over_time(metric[range()] @ end())`. - -`min(, )` and `max(, )` can be used to -find the minimum or maximum of two duration expressions. - -Duration expressions are not supported in the @ timestamp operator. - -The following operators are supported: - -* `+` - addition. -* `-` - subtraction. -* `*` - multiplication. -* `/` - division. -* `%` - modulo. -* `^` - exponentiation. - -Examples of equivalent durations: - -* `5m * 2` is equivalent to `10m` or `600s`. -* `10m - 1m` is equivalent to `9m` or `540s`. -* `(5 + 2) * 1m` is equivalent to `7m` or `420s`. -* `1h / 2` is equivalent to `30m` or `1800s`. -* `4h % 3h` is equivalent to `1h` or `3600s`. -* `(2 ^ 3) * 1m` is equivalent to `8m` or `480s`. -* `step() + 1` is equivalent to the query step width increased by 1s. -* `max(step(), 5s)` is equivalent to the larger of the query step width and `5s`. -* `min(2 * step() + 5s, 5m)` is equivalent to the smaller of twice the query step increased by `5s` and `5m`. - ## Time series selectors These are the basic building-blocks that instruct PromQL what data to fetch. @@ -333,13 +282,14 @@ A workaround for this restriction is to use the `__name__` label: Range vector literals work like instant vector literals, except that they select a range of samples back from the current instant. Syntactically, a -[duration](#float-literals-and-time-durations) is appended in square brackets -(`[]`) at the end of a vector selector to specify how far back in time values -should be fetched for each resulting range vector element. Commonly, this uses -one or more time units, e.g. `[5m]`. The range is a left-open and right-closed -interval, i.e. samples with timestamps coinciding with the left boundary of the -range are excluded from the selection, while samples coinciding with the right -boundary of the range are included in the selection. +[float literal](#float-literals-and-time-durations) is appended in square +brackets (`[]`) at the end of a vector selector to specify for how many seconds +back in time values should be fetched for each resulting range vector element. +Commonly, the float literal uses the syntax with one or more time units, e.g. +`[5m]`. The range is a left-open and right-closed interval, i.e. samples with +timestamps coinciding with the left boundary of the range are excluded from the +selection, while samples coinciding with the right boundary of the range are +included in the selection. In this example, we select all the values recorded less than 5m ago for all time series that have the metric name `http_requests_total` and a `job` label @@ -429,9 +379,8 @@ Note that the `@` modifier allows a query to look ahead of its evaluation time. Subquery allows you to run an instant query for a given range and resolution. The result of a subquery is a range vector. -Syntax: ` '[' ':' [] ']' [ @ ] [ offset ]` +Syntax: ` '[' ':' [] ']' [ @ ] [ offset ]` -* ``, ``, and `offset` support duration expressions. * `` is optional. Default is the global evaluation interval. ## Operators diff --git a/docs/querying/functions.md b/docs/querying/functions.md index 6418c2955f..a175064b81 100644 --- a/docs/querying/functions.md +++ b/docs/querying/functions.md @@ -961,7 +961,8 @@ These functions act on histograms in the following way: select the first sample of `m` _within_ the 1m range, where `m offset 1m` will select the most recent sample within the lookback interval _outside and prior to_ the 1m offset. This is particularly useful with `first_over_time(m[step()])` -in range queries to ensure that the sample selected is within the range step. +in range queries (available when `--enable-feature=promql-duration-expr` is set) +to ensure that the sample selected is within the range step. ## Trigonometric Functions diff --git a/promql/durations_test.go b/promql/durations_test.go index 3b31e92dd6..b8225ca8fc 100644 --- a/promql/durations_test.go +++ b/promql/durations_test.go @@ -23,7 +23,7 @@ import ( ) func TestDurationVisitor(t *testing.T) { - p := parser.NewParser(parser.Options{}) + p := parser.NewParser(parser.Options{ExperimentalDurationExpr: true}) complexExpr := `sum_over_time( rate(metric[5m] offset 1h)[10m:30s] offset 2h ) + diff --git a/promql/parser/features.go b/promql/parser/features.go index d21c7470e6..3bd3c493f5 100644 --- a/promql/parser/features.go +++ b/promql/parser/features.go @@ -53,6 +53,6 @@ func (pql *promQLParser) RegisterFeatures(r features.Collector) { r.Set(features.PromQLFunctions, f, !fc.Experimental || pql.options.EnableExperimentalFunctions) } - // Register parser features. - r.Enable(features.PromQL, "duration_expr") + // Register experimental parser features. + r.Set(features.PromQL, "duration_expr", pql.options.ExperimentalDurationExpr) } diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y index c1524f5669..39dfa1f49f 100644 --- a/promql/parser/generated_parser.y +++ b/promql/parser/generated_parser.y @@ -1327,18 +1327,22 @@ duration_expr : number_duration_literal } | duration_expr ADD duration_expr { + yylex.(*parser).experimentalDurationExpr($1.(Expr)) $$ = &DurationExpr{Op: ADD, LHS: $1.(Expr), RHS: $3.(Expr)} } | duration_expr SUB duration_expr { + yylex.(*parser).experimentalDurationExpr($1.(Expr)) $$ = &DurationExpr{Op: SUB, LHS: $1.(Expr), RHS: $3.(Expr)} } | duration_expr MUL duration_expr { + yylex.(*parser).experimentalDurationExpr($1.(Expr)) $$ = &DurationExpr{Op: MUL, LHS: $1.(Expr), RHS: $3.(Expr)} } | duration_expr DIV duration_expr { + yylex.(*parser).experimentalDurationExpr($1.(Expr)) if nl, ok := $3.(*NumberLiteral); ok && nl.Val == 0 { yylex.(*parser).addParseErrf($2.PositionRange(), "division by zero") $$ = &NumberLiteral{Val: 0} @@ -1348,6 +1352,7 @@ duration_expr : number_duration_literal } | duration_expr MOD duration_expr { + yylex.(*parser).experimentalDurationExpr($1.(Expr)) if nl, ok := $3.(*NumberLiteral); ok && nl.Val == 0 { yylex.(*parser).addParseErrf($2.PositionRange(), "modulo by zero") $$ = &NumberLiteral{Val: 0} @@ -1357,6 +1362,7 @@ duration_expr : number_duration_literal } | duration_expr POW duration_expr { + yylex.(*parser).experimentalDurationExpr($1.(Expr)) $$ = &DurationExpr{Op: POW, LHS: $1.(Expr), RHS: $3.(Expr)} } | STEP LEFT_PAREN RIGHT_PAREN @@ -1390,6 +1396,7 @@ duration_expr : number_duration_literal paren_duration_expr : LEFT_PAREN duration_expr RIGHT_PAREN { + yylex.(*parser).experimentalDurationExpr($2.(Expr)) if durationExpr, ok := $2.(*DurationExpr); ok { durationExpr.Wrapped = true $$ = durationExpr diff --git a/promql/parser/generated_parser.y.go b/promql/parser/generated_parser.y.go index db7c239b55..bf96f73f5a 100644 --- a/promql/parser/generated_parser.y.go +++ b/promql/parser/generated_parser.y.go @@ -2446,21 +2446,25 @@ yydefault: case 294: yyDollar = yyS[yypt-3 : yypt+1] { + yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr)) yyVAL.node = &DurationExpr{Op: ADD, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)} } case 295: yyDollar = yyS[yypt-3 : yypt+1] { + yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr)) yyVAL.node = &DurationExpr{Op: SUB, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)} } case 296: yyDollar = yyS[yypt-3 : yypt+1] { + yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr)) yyVAL.node = &DurationExpr{Op: MUL, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)} } case 297: yyDollar = yyS[yypt-3 : yypt+1] { + yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr)) if nl, ok := yyDollar[3].node.(*NumberLiteral); ok && nl.Val == 0 { yylex.(*parser).addParseErrf(yyDollar[2].item.PositionRange(), "division by zero") yyVAL.node = &NumberLiteral{Val: 0} @@ -2471,6 +2475,7 @@ yydefault: case 298: yyDollar = yyS[yypt-3 : yypt+1] { + yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr)) if nl, ok := yyDollar[3].node.(*NumberLiteral); ok && nl.Val == 0 { yylex.(*parser).addParseErrf(yyDollar[2].item.PositionRange(), "modulo by zero") yyVAL.node = &NumberLiteral{Val: 0} @@ -2481,6 +2486,7 @@ yydefault: case 299: yyDollar = yyS[yypt-3 : yypt+1] { + yylex.(*parser).experimentalDurationExpr(yyDollar[1].node.(Expr)) yyVAL.node = &DurationExpr{Op: POW, LHS: yyDollar[1].node.(Expr), RHS: yyDollar[3].node.(Expr)} } case 300: @@ -2515,6 +2521,7 @@ yydefault: case 304: yyDollar = yyS[yypt-3 : yypt+1] { + yylex.(*parser).experimentalDurationExpr(yyDollar[2].node.(Expr)) if durationExpr, ok := yyDollar[2].node.(*DurationExpr); ok { durationExpr.Wrapped = true yyVAL.node = durationExpr diff --git a/promql/parser/parse.go b/promql/parser/parse.go index 8d4491ea64..ec3e1001d9 100644 --- a/promql/parser/parse.go +++ b/promql/parser/parse.go @@ -43,6 +43,7 @@ var parserPool = sync.Pool{ // Options holds the configuration for the PromQL parser. type Options struct { EnableExperimentalFunctions bool + ExperimentalDurationExpr bool EnableExtendedRangeSelectors bool EnableBinopFillModifiers bool } @@ -1192,6 +1193,12 @@ func (p *parser) getAtModifierVars(e Node) (**int64, *ItemType, *posrange.Pos, b return timestampp, preprocp, endPosp, true } +func (p *parser) experimentalDurationExpr(e Expr) { + if !p.options.ExperimentalDurationExpr { + p.addParseErrf(e.PositionRange(), "experimental duration expression is not enabled") + } +} + func MustLabelMatcher(mt labels.MatchType, name, val string) *labels.Matcher { m, err := labels.NewMatcher(mt, name, val) if err != nil { diff --git a/promql/parser/parse_test.go b/promql/parser/parse_test.go index b3ea1bb0f4..6a6c50ee23 100644 --- a/promql/parser/parse_test.go +++ b/promql/parser/parse_test.go @@ -5327,6 +5327,7 @@ func readable(s string) string { func TestParseExpressions(t *testing.T) { optsParser := NewParser(Options{ EnableExperimentalFunctions: true, + ExperimentalDurationExpr: true, }) for _, test := range testExpr { @@ -6083,6 +6084,7 @@ func TestParseCustomFunctions(t *testing.T) { func TestNewParser(t *testing.T) { p := NewParser(Options{ EnableExperimentalFunctions: true, + ExperimentalDurationExpr: true, }) // ParseExpr should work. diff --git a/promql/parser/prettier_test.go b/promql/parser/prettier_test.go index 47e5407e30..d00bc283ec 100644 --- a/promql/parser/prettier_test.go +++ b/promql/parser/prettier_test.go @@ -670,7 +670,7 @@ func TestUnaryPretty(t *testing.T) { } func TestDurationExprPretty(t *testing.T) { - optsParser := NewParser(Options{}) + optsParser := NewParser(Options{ExperimentalDurationExpr: true}) maxCharactersPerLine = 10 inputs := []struct { in, out string diff --git a/promql/parser/printer_test.go b/promql/parser/printer_test.go index 774ff77d2f..eae91d4f88 100644 --- a/promql/parser/printer_test.go +++ b/promql/parser/printer_test.go @@ -23,6 +23,7 @@ import ( func TestExprString(t *testing.T) { optsParser := NewParser(Options{ + ExperimentalDurationExpr: true, EnableExtendedRangeSelectors: true, EnableBinopFillModifiers: true, }) diff --git a/promql/promqltest/test.go b/promql/promqltest/test.go index 0be88829bf..85c0c4f88a 100644 --- a/promql/promqltest/test.go +++ b/promql/promqltest/test.go @@ -90,6 +90,7 @@ func LoadedStorage(t testing.TB, input string) *teststorage.TestStorage { // TestParserOpts are the parser options used for all built-in test engines. var TestParserOpts = parser.Options{ EnableExperimentalFunctions: true, + ExperimentalDurationExpr: true, EnableExtendedRangeSelectors: true, EnableBinopFillModifiers: true, } diff --git a/util/fuzzing/fuzz_test.go b/util/fuzzing/fuzz_test.go index 32353c6afc..747eab1f1a 100644 --- a/util/fuzzing/fuzz_test.go +++ b/util/fuzzing/fuzz_test.go @@ -363,6 +363,7 @@ func FuzzParseExpr(f *testing.F) { p := parser.NewParser(parser.Options{ EnableExperimentalFunctions: true, + ExperimentalDurationExpr: true, EnableExtendedRangeSelectors: true, EnableBinopFillModifiers: true, }) diff --git a/web/ui/mantine-ui/src/promql/functionDocs.tsx b/web/ui/mantine-ui/src/promql/functionDocs.tsx index 375c4155d4..7ea47c02bb 100644 --- a/web/ui/mantine-ui/src/promql/functionDocs.tsx +++ b/web/ui/mantine-ui/src/promql/functionDocs.tsx @@ -589,7 +589,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -908,7 +909,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -1238,7 +1240,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -2069,7 +2072,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -2219,7 +2223,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -2328,7 +2333,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -2437,7 +2443,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -2652,7 +2659,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -2761,7 +2769,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -3274,7 +3283,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -3383,7 +3393,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -3508,7 +3519,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -3773,7 +3785,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -3882,7 +3895,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -3991,7 +4005,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), @@ -4100,7 +4115,8 @@ const funcDocs: Record = { first sample of m within the 1m range, where m offset 1m will select the most recent sample within the lookback interval outside and prior to the 1m offset. This is particularly useful with first_over_time(m[step()]) - in range queries to ensure that the sample selected is within the range step. + in range queries (available when --enable-feature=promql-duration-expr is set) to ensure that the + sample selected is within the range step.

), From 502e31a82fe42ec56de76f658c303b1ab7a91a00 Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Wed, 13 May 2026 09:56:54 +0200 Subject: [PATCH 2/3] promql: protect min(), max(), step(), and range() duration exprs with feature flag min(), max(), step(), and range() in duration expression context were not guarded by the ExperimentalDurationExpr feature flag, unlike the binary operators (+, -, *, /, %, ^). Add the missing experimentalDurationExpr() calls in both offset_duration_expr and duration_expr grammar rules. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- promql/parser/generated_parser.y | 44 ++++++++++++++++++++--------- promql/parser/generated_parser.y.go | 36 +++++++++++++++++------ 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y index 39dfa1f49f..34ce028c1f 100644 --- a/promql/parser/generated_parser.y +++ b/promql/parser/generated_parser.y @@ -1201,23 +1201,27 @@ offset_duration_expr : number_duration_literal } | STEP LEFT_PAREN RIGHT_PAREN { - $$ = &DurationExpr{ - Op: STEP, + de := &DurationExpr{ + Op: STEP, StartPos: $1.PositionRange().Start, - EndPos: $3.PositionRange().End, + EndPos: $3.PositionRange().End, } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | RANGE LEFT_PAREN RIGHT_PAREN { - $$ = &DurationExpr{ - Op: RANGE, + de := &DurationExpr{ + Op: RANGE, StartPos: $1.PositionRange().Start, - EndPos: $3.PositionRange().End, + EndPos: $3.PositionRange().End, } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | unary_op STEP LEFT_PAREN RIGHT_PAREN { - $$ = &DurationExpr{ + de := &DurationExpr{ Op: $1.Typ, RHS: &DurationExpr{ Op: STEP, @@ -1226,10 +1230,12 @@ offset_duration_expr : number_duration_literal }, StartPos: $1.Pos, } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | unary_op RANGE LEFT_PAREN RIGHT_PAREN { - $$ = &DurationExpr{ + de := &DurationExpr{ Op: $1.Typ, RHS: &DurationExpr{ Op: RANGE, @@ -1238,20 +1244,24 @@ offset_duration_expr : number_duration_literal }, StartPos: $1.Pos, } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | min_max LEFT_PAREN duration_expr COMMA duration_expr RIGHT_PAREN { - $$ = &DurationExpr{ + de := &DurationExpr{ Op: $1.Typ, StartPos: $1.PositionRange().Start, EndPos: $6.PositionRange().End, LHS: $3.(Expr), RHS: $5.(Expr), } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | unary_op min_max LEFT_PAREN duration_expr COMMA duration_expr RIGHT_PAREN { - $$ = &DurationExpr{ + de := &DurationExpr{ Op: $1.Typ, StartPos: $1.Pos, EndPos: $6.PositionRange().End, @@ -1263,6 +1273,8 @@ offset_duration_expr : number_duration_literal RHS: $6.(Expr), }, } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | unary_op LEFT_PAREN duration_expr RIGHT_PAREN %prec MUL { @@ -1367,29 +1379,35 @@ duration_expr : number_duration_literal } | STEP LEFT_PAREN RIGHT_PAREN { - $$ = &DurationExpr{ + de := &DurationExpr{ Op: STEP, StartPos: $1.PositionRange().Start, EndPos: $3.PositionRange().End, } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | RANGE LEFT_PAREN RIGHT_PAREN { - $$ = &DurationExpr{ + de := &DurationExpr{ Op: RANGE, StartPos: $1.PositionRange().Start, EndPos: $3.PositionRange().End, } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | min_max LEFT_PAREN duration_expr COMMA duration_expr RIGHT_PAREN { - $$ = &DurationExpr{ + de := &DurationExpr{ Op: $1.Typ, StartPos: $1.PositionRange().Start, EndPos: $6.PositionRange().End, LHS: $3.(Expr), RHS: $5.(Expr), } + yylex.(*parser).experimentalDurationExpr(de) + $$ = de } | paren_duration_expr ; diff --git a/promql/parser/generated_parser.y.go b/promql/parser/generated_parser.y.go index bf96f73f5a..9f9f017dfa 100644 --- a/promql/parser/generated_parser.y.go +++ b/promql/parser/generated_parser.y.go @@ -2316,25 +2316,29 @@ yydefault: case 282: yyDollar = yyS[yypt-3 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: STEP, StartPos: yyDollar[1].item.PositionRange().Start, EndPos: yyDollar[3].item.PositionRange().End, } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 283: yyDollar = yyS[yypt-3 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: RANGE, StartPos: yyDollar[1].item.PositionRange().Start, EndPos: yyDollar[3].item.PositionRange().End, } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 284: yyDollar = yyS[yypt-4 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: yyDollar[1].item.Typ, RHS: &DurationExpr{ Op: STEP, @@ -2343,11 +2347,13 @@ yydefault: }, StartPos: yyDollar[1].item.Pos, } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 285: yyDollar = yyS[yypt-4 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: yyDollar[1].item.Typ, RHS: &DurationExpr{ Op: RANGE, @@ -2356,22 +2362,26 @@ yydefault: }, StartPos: yyDollar[1].item.Pos, } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 286: yyDollar = yyS[yypt-6 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: yyDollar[1].item.Typ, StartPos: yyDollar[1].item.PositionRange().Start, EndPos: yyDollar[6].item.PositionRange().End, LHS: yyDollar[3].node.(Expr), RHS: yyDollar[5].node.(Expr), } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 287: yyDollar = yyS[yypt-7 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: yyDollar[1].item.Typ, StartPos: yyDollar[1].item.Pos, EndPos: yyDollar[6].node.PositionRange().End, @@ -2383,6 +2393,8 @@ yydefault: RHS: yyDollar[6].node.(Expr), }, } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 288: yyDollar = yyS[yypt-4 : yypt+1] @@ -2492,31 +2504,37 @@ yydefault: case 300: yyDollar = yyS[yypt-3 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: STEP, StartPos: yyDollar[1].item.PositionRange().Start, EndPos: yyDollar[3].item.PositionRange().End, } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 301: yyDollar = yyS[yypt-3 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: RANGE, StartPos: yyDollar[1].item.PositionRange().Start, EndPos: yyDollar[3].item.PositionRange().End, } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 302: yyDollar = yyS[yypt-6 : yypt+1] { - yyVAL.node = &DurationExpr{ + de := &DurationExpr{ Op: yyDollar[1].item.Typ, StartPos: yyDollar[1].item.PositionRange().Start, EndPos: yyDollar[6].item.PositionRange().End, LHS: yyDollar[3].node.(Expr), RHS: yyDollar[5].node.(Expr), } + yylex.(*parser).experimentalDurationExpr(de) + yyVAL.node = de } case 304: yyDollar = yyS[yypt-3 : yypt+1] From dd406b3df5002cf67a298b73928ec7435024102f Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Wed, 13 May 2026 10:52:59 +0200 Subject: [PATCH 3/3] web/api/v1: enable ExperimentalDurationExpr in translate AST tests Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- web/api/v1/translate_ast_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/api/v1/translate_ast_test.go b/web/api/v1/translate_ast_test.go index 50befb1962..84c4019363 100644 --- a/web/api/v1/translate_ast_test.go +++ b/web/api/v1/translate_ast_test.go @@ -22,7 +22,7 @@ import ( ) func TestTranslateASTDurationExpressions(t *testing.T) { - p := parser.NewParser(parser.Options{}) + p := parser.NewParser(parser.Options{ExperimentalDurationExpr: true}) type tc struct { name string