From e1ace8d00e09e3a791b2814b635d917116e27914 Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Wed, 12 Jul 2023 21:34:55 +0200 Subject: [PATCH 1/3] Add PromQL format and label matcher set/delete commands to promtool Signed-off-by: Rob Skillington Signed-off-by: Julien Pivotto --- cmd/promtool/main.go | 103 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go index f94be8b27..2b5ee9aee 100644 --- a/cmd/promtool/main.go +++ b/cmd/promtool/main.go @@ -58,6 +58,7 @@ import ( "github.com/prometheus/prometheus/notifier" _ "github.com/prometheus/prometheus/plugins" // Register plugins. "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/scrape" "github.com/prometheus/prometheus/util/documentcli" ) @@ -245,6 +246,22 @@ func main() { "A list of one or more files containing recording rules to be backfilled. All recording rules listed in the files will be backfilled. Alerting rules are not evaluated.", ).Required().ExistingFiles() + promQLCmd := app.Command("promql", "PromQL formatting and editing.") + + promQLFormatCmd := promQLCmd.Command("format", "Format PromQL query to pretty printed form.") + promQLFormatQuery := promQLFormatCmd.Arg("query", "PromQL query.").Required().String() + + promQLLabelsCmd := promQLCmd.Command("label-matchers", "Edit label matchers contained within an existing PromQL query.") + promQLLabelsSetCmd := promQLLabelsCmd.Command("set", "Set a label matcher in the query.") + promQLLabelsSetType := promQLLabelsSetCmd.Flag("type", "Type of the label matcher to set.").Short('t').Default("=").Enum("=", "!=", "=~", "!~") + promQLLabelsSetQuery := promQLLabelsSetCmd.Arg("query", "PromQL query.").Required().String() + promQLLabelsSetName := promQLLabelsSetCmd.Arg("name", "Name of the label matcher to set.").Required().String() + promQLLabelsSetValue := promQLLabelsSetCmd.Arg("value", "Value of the label matcher to set.").Required().String() + + promQLLabelsDeleteCmd := promQLLabelsCmd.Command("delete", "Delete a label from the query.") + 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 (only PromQL related and no-default-scrape-port). See https://prometheus.io/docs/prometheus/latest/feature_flags/ for the options and more details.").Default("").Strings() documentationCmd := app.Command("write-documentation", "Generate command line documentation. Internal use.").Hidden() @@ -364,8 +381,18 @@ func main() { case importRulesCmd.FullCommand(): os.Exit(checkErr(importRules(serverURL, httpRoundTripper, *importRulesStart, *importRulesEnd, *importRulesOutputDir, *importRulesEvalInterval, *maxBlockDuration, *importRulesFiles...))) + case documentationCmd.FullCommand(): os.Exit(checkErr(documentcli.GenerateMarkdown(app.Model(), os.Stdout))) + + case promQLFormatCmd.FullCommand(): + os.Exit(checkErr(formatPromQL(*promQLFormatQuery))) + + case promQLLabelsSetCmd.FullCommand(): + os.Exit(checkErr(labelsSetPromQL(*promQLLabelsSetQuery, *promQLLabelsSetType, *promQLLabelsSetName, *promQLLabelsSetValue))) + + case promQLLabelsDeleteCmd.FullCommand(): + os.Exit(checkErr(labelsDeletePromQL(*promQLLabelsDeleteQuery, *promQLLabelsDeleteName))) } } @@ -1375,3 +1402,79 @@ func checkTargetGroupsForScrapeConfig(targetGroups []*targetgroup.Group, scfg *c return nil } + +func formatPromQL(query string) error { + expr, err := parser.ParseExpr(query) + if err != nil { + return err + } + + fmt.Println(expr.Pretty(0)) + return nil +} + +func labelsSetPromQL(query, labelMatchType, name, value string) error { + expr, err := parser.ParseExpr(query) + if err != nil { + return err + } + + var matchType labels.MatchType + switch labelMatchType { + case parser.ItemType(parser.EQL).String(): + matchType = labels.MatchEqual + case parser.ItemType(parser.NEQ).String(): + matchType = labels.MatchNotEqual + case parser.ItemType(parser.EQL_REGEX).String(): + matchType = labels.MatchRegexp + case parser.ItemType(parser.NEQ_REGEX).String(): + matchType = labels.MatchNotRegexp + default: + return fmt.Errorf("invalid label match type: %s", labelMatchType) + } + + parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { + if n, ok := node.(*parser.VectorSelector); ok { + var found bool + for i, l := range n.LabelMatchers { + if l.Name == name { + n.LabelMatchers[i].Type = matchType + n.LabelMatchers[i].Value = value + found = true + } + } + if !found { + n.LabelMatchers = append(n.LabelMatchers, &labels.Matcher{ + Type: matchType, + Name: name, + Value: value, + }) + } + } + return nil + }) + + fmt.Println(expr.Pretty(0)) + return nil +} + +func labelsDeletePromQL(query, name string) error { + expr, err := parser.ParseExpr(query) + if err != nil { + return err + } + + parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { + if n, ok := node.(*parser.VectorSelector); ok { + for i, l := range n.LabelMatchers { + if l.Name == name { + n.LabelMatchers = append(n.LabelMatchers[:i], n.LabelMatchers[i+1:]...) + } + } + } + return nil + }) + + fmt.Println(expr.Pretty(0)) + return nil +} From b3b669fd9a00d980cb853efa1b8a4912ddeba3bf Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Wed, 12 Jul 2023 22:30:20 +0200 Subject: [PATCH 2/3] Add experimental flag and docs Signed-off-by: Julien Pivotto --- cmd/promtool/main.go | 14 ++++++- docs/command-line/promtool.md | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go index 2b5ee9aee..da4b8dc79 100644 --- a/cmd/promtool/main.go +++ b/cmd/promtool/main.go @@ -92,6 +92,8 @@ func main() { checkCmd := app.Command("check", "Check the resources for validity.") + experimental := app.Flag("experimental", "Enable experimental commands.").Bool() + sdCheckCmd := checkCmd.Command("service-discovery", "Perform service discovery for the given job name and report the results, including relabeling.") sdConfigFile := sdCheckCmd.Arg("config-file", "The prometheus config file.").Required().ExistingFile() sdJobName := sdCheckCmd.Arg("job", "The job to run service discovery for.").Required().String() @@ -246,7 +248,7 @@ func main() { "A list of one or more files containing recording rules to be backfilled. All recording rules listed in the files will be backfilled. Alerting rules are not evaluated.", ).Required().ExistingFiles() - promQLCmd := app.Command("promql", "PromQL formatting and editing.") + promQLCmd := app.Command("promql", "PromQL formatting and editing. Requires the --experimental flag.") promQLFormatCmd := promQLCmd.Command("format", "Format PromQL query to pretty printed form.") promQLFormatQuery := promQLFormatCmd.Arg("query", "PromQL query.").Required().String() @@ -386,16 +388,26 @@ func main() { os.Exit(checkErr(documentcli.GenerateMarkdown(app.Model(), os.Stdout))) case promQLFormatCmd.FullCommand(): + checkExperimental(*experimental) os.Exit(checkErr(formatPromQL(*promQLFormatQuery))) case promQLLabelsSetCmd.FullCommand(): + checkExperimental(*experimental) os.Exit(checkErr(labelsSetPromQL(*promQLLabelsSetQuery, *promQLLabelsSetType, *promQLLabelsSetName, *promQLLabelsSetValue))) case promQLLabelsDeleteCmd.FullCommand(): + checkExperimental(*experimental) os.Exit(checkErr(labelsDeletePromQL(*promQLLabelsDeleteQuery, *promQLLabelsDeleteName))) } } +func checkExperimental(f bool) { + if !f { + fmt.Fprintln(os.Stderr, "This command is experimental and requires the --experimental flag to be set.") + os.Exit(1) + } +} + // nolint:revive var lintError = fmt.Errorf("lint error") diff --git a/docs/command-line/promtool.md b/docs/command-line/promtool.md index 673e8c048..587286e10 100644 --- a/docs/command-line/promtool.md +++ b/docs/command-line/promtool.md @@ -14,6 +14,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 (only PromQL related and no-default-scrape-port). See https://prometheus.io/docs/prometheus/latest/feature_flags/ for the options and more details. | @@ -30,6 +31,7 @@ Tooling for the Prometheus monitoring system. | push | Push to a Prometheus server. | | test | Unit testing. | | tsdb | Run tsdb commands. | +| promql | PromQL formatting and editing. Requires the --experimental flag. | @@ -609,3 +611,72 @@ Create blocks of data for new recording rules. | rule-files | A list of one or more files containing recording rules to be backfilled. All recording rules listed in the files will be backfilled. Alerting rules are not evaluated. | Yes | + + +### `promtool promql` + +PromQL formatting and editing. Requires the --experimental flag. + + + +##### `promtool promql format` + +Format PromQL query to pretty printed form. + + + +###### Arguments + +| Argument | Description | Required | +| --- | --- | --- | +| query | PromQL query. | Yes | + + + + +##### `promtool promql label-matchers` + +Edit label matchers contained within an existing PromQL query. + + + +##### `promtool promql label-matchers set` + +Set a label matcher in the query. + + + +###### Flags + +| Flag | Description | Default | +| --- | --- | --- | +| -t, --type | Type of the label matcher to set. | `=` | + + + + +###### Arguments + +| Argument | Description | Required | +| --- | --- | --- | +| query | PromQL query. | Yes | +| name | Name of the label matcher to set. | Yes | +| value | Value of the label matcher to set. | Yes | + + + + +##### `promtool promql label-matchers delete` + +Delete a label from the query. + + + +###### Arguments + +| Argument | Description | Required | +| --- | --- | --- | +| query | PromQL query. | Yes | +| name | Name of the label to delete. | Yes | + + From fd5b01afdcb1fefad227b9f7667125e02a1395f1 Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Thu, 13 Jul 2023 22:26:49 +0200 Subject: [PATCH 3/3] promtool docs: write flags between backtits in help Signed-off-by: Julien Pivotto --- docs/command-line/promtool.md | 2 +- util/documentcli/documentcli.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/command-line/promtool.md b/docs/command-line/promtool.md index 587286e10..546b200e2 100644 --- a/docs/command-line/promtool.md +++ b/docs/command-line/promtool.md @@ -615,7 +615,7 @@ Create blocks of data for new recording rules. ### `promtool promql` -PromQL formatting and editing. Requires the --experimental flag. +PromQL formatting and editing. Requires the `--experimental` flag. diff --git a/util/documentcli/documentcli.go b/util/documentcli/documentcli.go index c199d8d9b..720a7c9c7 100644 --- a/util/documentcli/documentcli.go +++ b/util/documentcli/documentcli.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/alecthomas/kingpin/v2" + "github.com/grafana/regexp" ) // GenerateMarkdown generates the markdown documentation for an application from @@ -230,6 +231,7 @@ func writeSubcommands(writer io.Writer, level int, modelName string, commands [] if cmd.HelpLong != "" { help = cmd.HelpLong } + help = formatHyphenatedWords(help) if _, err := writer.Write([]byte(fmt.Sprintf("\n\n%s `%s %s`\n\n%s\n\n", strings.Repeat("#", level+1), modelName, cmd.FullCommand, help))); err != nil { return err } @@ -250,3 +252,11 @@ func writeSubcommands(writer io.Writer, level int, modelName string, commands [] } return nil } + +func formatHyphenatedWords(input string) string { + hyphenRegex := regexp.MustCompile(`\B--\w+\b`) + replacer := func(s string) string { + return fmt.Sprintf("`%s`", s) + } + return hyphenRegex.ReplaceAllStringFunc(input, replacer) +}