Repository: prometheus/prometheus (language: go)

Issue:
Allow PromTool to check rules with experimental promql functions
### Proposal

We would like to enable the experimental feature flag for our prometheus install  `--enable-feature=promql-experimental-functions`, however doing so would break our CI for ensuring new rules are valid as we use `promtool check rules` which errors when it encounters experimental functions.

Maybe something like allowing PromTool using the  `--enable-feature=promql-experimental-functions` to configure the Parser to allow experimental functions.


Discussion / hints:

Fix patch:
diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go
index 5fa4de44edc..0304fa2d576 100644
--- a/cmd/promtool/main.go
+++ b/cmd/promtool/main.go
@@ -61,6 +61,8 @@ import (
 	"github.com/prometheus/prometheus/util/documentcli"
 )
 
+var promqlEnableDelayedNameRemoval = false
+
 func init() {
 	// This can be removed when the legacy global mode is fully deprecated.
 	//nolint:staticcheck
@@ -304,7 +306,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. Currently unused.").Default("").Strings()
+	featureList := app.Flag("enable-feature", "Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal. 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()
 
@@ -338,10 +340,14 @@ func main() {
 		opts := strings.Split(f, ",")
 		for _, o := range opts {
 			switch o {
+			case "promql-experimental-functions":
+				parser.EnableExperimentalFunctions = true
+			case "promql-delayed-name-removal":
+				promqlEnableDelayedNameRemoval = true
 			case "":
 				continue
 			default:
-				fmt.Printf("  WARNING: --enable-feature is currently a no-op")
+				fmt.Printf("  WARNING: Unknown feature passed to --enable-feature: %s", o)
 			}
 		}
 	}
@@ -399,8 +405,9 @@ func main() {
 		}
 		os.Exit(RulesUnitTestResult(results,
 			promqltest.LazyLoaderOpts{
-				EnableAtModifier:     true,
-				EnableNegativeOffset: true,
+				EnableAtModifier:         true,
+				EnableNegativeOffset:     true,
+				EnableDelayedNameRemoval: promqlEnableDelayedNameRemoval,
 			},
 			*testRulesRun,
 			*testRulesDiff,
diff --git a/docs/command-line/promtool.md b/docs/command-line/promtool.md
index ab675e6345f..1c4c0a18f97 100644
--- a/docs/command-line/promtool.md
+++ b/docs/command-line/promtool.md
@@ -15,7 +15,7 @@ Tooling for the Prometheus monitoring system.
 | <code class="text-nowrap">-h</code>, <code class="text-nowrap">--help</code> | Show context-sensitive help (also try --help-long and --help-man). |
 | <code class="text-nowrap">--version</code> | Show application version. |
 | <code class="text-nowrap">--experimental</code> | Enable experimental commands. |
-| <code class="text-nowrap">--enable-feature</code> <code class="text-nowrap">...<code class="text-nowrap"> | Comma separated feature names to enable. Currently unused. |
+| <code class="text-nowrap">--enable-feature</code> <code class="text-nowrap">...<code class="text-nowrap"> | Comma separated feature names to enable. Valid options: promql-experimental-functions, promql-delayed-name-removal. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details |
 
 
 


Test patch:
diff --git a/cmd/promtool/main_test.go b/cmd/promtool/main_test.go
index d3804da5e24..d1390f0d676 100644
--- a/cmd/promtool/main_test.go
+++ b/cmd/promtool/main_test.go
@@ -558,6 +558,16 @@ func TestCheckRules(t *testing.T) {
 	})
 }
 
+func TestCheckRulesWithFeatureFlag(t *testing.T) {
+	// As opposed to TestCheckRules calling CheckRules directly we run promtool
+	// so the feature flag parsing can be tested.
+
+	args := []string{"-test.main", "--enable-feature=promql-experimental-functions", "check", "rules", "testdata/features.yml"}
+	tool := exec.Command(promtoolPath, args...)
+	err := tool.Run()
+	require.NoError(t, err)
+}
+
 func TestCheckRulesWithRuleFiles(t *testing.T) {
 	t.Run("rules-good", func(t *testing.T) {
 		t.Parallel()
diff --git a/cmd/promtool/testdata/features.yml b/cmd/promtool/testdata/features.yml
new file mode 100644
index 00000000000..769f8362bf3
--- /dev/null
+++ b/cmd/promtool/testdata/features.yml
@@ -0,0 +1,6 @@
+groups:
+  - name: features
+    rules:
+      - record: x
+        # We don't expect anything from this, just want to check the function parses.
+        expr: sort_by_label(up, "instance")
diff --git a/promql/promqltest/test.go b/promql/promqltest/test.go
index 84ca16e8ab6..cbdf14fcd9a 100644
--- a/promql/promqltest/test.go
+++ b/promql/promqltest/test.go
@@ -1501,6 +1501,9 @@ type LazyLoaderOpts struct {
 	// Prometheus v2.33). They can still be disabled here for legacy and
 	// other uses.
 	EnableAtModifier, EnableNegativeOffset bool
+	// Currently defaults to false, matches the "promql-delayed-name-removal"
+	// feature flag.
+	EnableDelayedNameRemoval bool
 }
 
 // NewLazyLoader returns an initialized empty LazyLoader.
@@ -1563,7 +1566,7 @@ func (ll *LazyLoader) clear() error {
 		NoStepSubqueryIntervalFn: func(int64) int64 { return durationMilliseconds(ll.SubqueryInterval) },
 		EnableAtModifier:         ll.opts.EnableAtModifier,
 		EnableNegativeOffset:     ll.opts.EnableNegativeOffset,
-		EnableDelayedNameRemoval: true,
+		EnableDelayedNameRemoval: ll.opts.EnableDelayedNameRemoval,
 	}
 
 	ll.queryEngine = promql.NewEngine(opts)