From bd487546c531cbe7bbc9b21d76eb27236d642760 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:33:29 +0200 Subject: [PATCH 01/26] :sparkles: required matchers can be provided via args + dynamic check template if matchers use vars --- lint/results.go | 16 ++- lint/rule_target_job_instance.go | 47 ------ lint/rule_target_job_instance_test.go | 98 ------------- lint/rule_target_required_matchers.go | 82 +++++++++++ lint/rule_target_required_matchers_test.go | 136 ++++++++++++++++++ lint/rule_template_instance.go | 19 --- lint/rule_template_instance_test.go | 83 ----------- lint/rule_template_variable_matchers.go | 29 ++++ ...> rule_template_variable_matchers_test.go} | 10 +- lint/rules.go | 10 +- ...rule_template_job.go => template_utils.go} | 18 --- main.go | 27 +++- 12 files changed, 301 insertions(+), 274 deletions(-) delete mode 100644 lint/rule_target_job_instance.go delete mode 100644 lint/rule_target_job_instance_test.go create mode 100644 lint/rule_target_required_matchers.go create mode 100644 lint/rule_target_required_matchers_test.go delete mode 100644 lint/rule_template_instance.go delete mode 100644 lint/rule_template_instance_test.go create mode 100644 lint/rule_template_variable_matchers.go rename lint/{rule_template_job_test.go => rule_template_variable_matchers_test.go} (95%) rename lint/{rule_template_job.go => template_utils.go} (79%) diff --git a/lint/results.go b/lint/results.go index 7cf9be0..2841f8f 100644 --- a/lint/results.go +++ b/lint/results.go @@ -34,12 +34,26 @@ type TargetRuleResults struct { Results []TargetResult } +func TargetMessage(d Dashboard, p Panel, t Target, message string) string { + return fmt.Sprintf("Dashboard '%s', panel '%s', target idx '%d' %s", d.Title, p.Title, t.Idx, message) +} + func (r *TargetRuleResults) AddError(d Dashboard, p Panel, t Target, message string) { r.Results = append(r.Results, TargetResult{ Result: Result{ Severity: Error, - Message: fmt.Sprintf("Dashboard '%s', panel '%s', target idx '%d' %s", d.Title, p.Title, t.Idx, message), + Message: TargetMessage(d, p, t, message), + }, + }) +} + +func (r *TargetRuleResults) AddFixableError(d Dashboard, p Panel, t Target, message string, fix func(Dashboard, Panel, *Target)) { + r.Results = append(r.Results, TargetResult{ + Result: Result{ + Severity: Error, + Message: TargetMessage(d, p, t, message), }, + Fix: fix, }) } diff --git a/lint/rule_target_job_instance.go b/lint/rule_target_job_instance.go deleted file mode 100644 index 968f679..0000000 --- a/lint/rule_target_job_instance.go +++ /dev/null @@ -1,47 +0,0 @@ -package lint - -import ( - "fmt" - - "github.com/prometheus/prometheus/model/labels" - "github.com/prometheus/prometheus/promql/parser" -) - -func newTargetRequiredMatcherRule(matcher string) *TargetRuleFunc { - return &TargetRuleFunc{ - name: fmt.Sprintf("target-%s-rule", matcher), - description: fmt.Sprintf("Checks that every PromQL query has a %s matcher.", matcher), - fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { - r := TargetRuleResults{} - // TODO: The RuleSet should be responsible for routing rule checks based on their query type (prometheus, loki, mysql, etc) - // and for ensuring that the datasource is set. - if t := getTemplateDatasource(d); t == nil || t.Query != Prometheus { - // Missing template datasource is a separate rule. - // Non prometheus datasources don't have rules yet - return r - } - - node, err := parsePromQL(t.Expr, d.Templating.List) - if err != nil { - // Invalid PromQL is another rule - return r - } - - for _, selector := range parser.ExtractSelectors(node) { - if err := checkForMatcher(selector, matcher, labels.MatchRegexp, fmt.Sprintf("$%s", matcher)); err != nil { - r.AddError(d, p, t, fmt.Sprintf("invalid PromQL query '%s': %v", t.Expr, err)) - } - } - - return r - }, - } -} - -func NewTargetJobRule() *TargetRuleFunc { - return newTargetRequiredMatcherRule("job") -} - -func NewTargetInstanceRule() *TargetRuleFunc { - return newTargetRequiredMatcherRule("instance") -} diff --git a/lint/rule_target_job_instance_test.go b/lint/rule_target_job_instance_test.go deleted file mode 100644 index 3b36277..0000000 --- a/lint/rule_target_job_instance_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package lint - -import ( - "fmt" - "testing" -) - -func testTargetRequiredMatcherRule(t *testing.T, matcher string) { - var linter *TargetRuleFunc - - switch matcher { - case "job": - linter = NewTargetJobRule() - case "instance": - linter = NewTargetInstanceRule() - default: - t.Errorf("No concrete target required matcher rule for '%s", matcher) - return - } - - for _, tc := range []struct { - result Result - target Target - }{ - // Happy path - { - result: ResultSuccess, - target: Target{ - Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, matcher, matcher), - }, - }, - // Also happy when the promql is invalid - { - result: ResultSuccess, - target: Target{ - Expr: `foo(bar.baz))`, - }, - }, - // Missing matcher - { - result: Result{ - Severity: Error, - Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo[5m]))': %s selector not found", matcher), - }, - target: Target{ - Expr: `sum(rate(foo[5m]))`, - }, - }, - // Not a regex matcher - { - result: Result{ - Severity: Error, - Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo{%s=\"$%s\"}[5m]))': %s selector is =, not =~", matcher, matcher, matcher), - }, - target: Target{ - Expr: fmt.Sprintf(`sum(rate(foo{%s="$%s"}[5m]))`, matcher, matcher), - }, - }, - // Wrong template variable - { - result: Result{ - Severity: Error, - Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo{%s=~\"$foo\"}[5m]))': %s selector is $foo, not $%s", matcher, matcher, matcher), - }, - target: Target{ - Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$foo"}[5m]))`, matcher), - }, - }, - } { - dashboard := Dashboard{ - Title: "dashboard", - Templating: struct { - List []Template `json:"list"` - }{ - List: []Template{ - { - Type: "datasource", - Query: "prometheus", - }, - }, - }, - Panels: []Panel{ - { - Title: "panel", - Type: "singlestat", - Targets: []Target{tc.target}, - }, - }, - } - - testRule(t, linter, dashboard, tc.result) - } -} - -func TestTargetJobInstanceRule(t *testing.T) { - testTargetRequiredMatcherRule(t, "job") - testTargetRequiredMatcherRule(t, "instance") -} diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go new file mode 100644 index 0000000..40f6533 --- /dev/null +++ b/lint/rule_target_required_matchers.go @@ -0,0 +1,82 @@ +package lint + +import ( + "fmt" + + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +func newTargetRequiredMatchersRule(matchers []*labels.Matcher) *TargetRuleFunc { + return &TargetRuleFunc{ + name: "target-required-matchers", + description: "Checks that target expr has the required matchers", + fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { + r := TargetRuleResults{} + // TODO: The RuleSet should be responsible for routing rule checks based on their query type (prometheus, loki, mysql, etc) + // and for ensuring that the datasource is set. + if t := getTemplateDatasource(d); t == nil || t.Query != Prometheus { + // Missing template datasource is a separate rule. + // Non prometheus datasources don't have rules yet + return r + } + + expr, err := parsePromQL(t.Expr, d.Templating.List) + if err != nil { + // Invalid PromQL is another rule + return r + } + for _, m := range matchers { + for _, selector := range parser.ExtractSelectors(expr) { + if err := checkForMatcher(selector, m.Name, m.Type, m.Value); err != nil { + r.AddFixableError(d, p, t, fmt.Sprintf("invalid PromQL query '%s': %v", t.Expr, err), fixTargetRequiredMatcherRule(m.Name, m.Type, m.Value)) + } + } + } + return r + }, + } +} + +func fixTargetRequiredMatcherRule(name string, ty labels.MatchType, value string) func(Dashboard, Panel, *Target) { + return func(d Dashboard, p Panel, t *Target) { + // using t.Expr to ensure matchers added earlier in the loop are not lost + // no need to check for errors here, as the expression was already parsed and validated + expr, _ := parsePromQL(t.Expr, d.Templating.List) + // Walk the expression tree and add the matcher to all vector selectors + parser.Walk(addMatchers(name, ty, value), expr, nil) + t.Expr = expr.String() + } +} + +type matcherAdder func(node parser.Node) error + +func (f matcherAdder) Visit(node parser.Node, path []parser.Node) (w parser.Visitor, err error) { + err = f(node) + return f, err +} + +func addMatchers(name string, ty labels.MatchType, value string) matcherAdder { + return func(node parser.Node) error { + if n, ok := node.(*parser.VectorSelector); ok { + matcherfixed := false + for _, m := range n.LabelMatchers { + if m.Name == name { + if m.Type != ty || m.Value != value { + m.Type = ty + m.Value = value + } + matcherfixed = true + } + } + if !matcherfixed { + n.LabelMatchers = append(n.LabelMatchers, &labels.Matcher{ + Name: name, + Type: ty, + Value: value, + }) + } + } + return nil + } +} diff --git a/lint/rule_target_required_matchers_test.go b/lint/rule_target_required_matchers_test.go new file mode 100644 index 0000000..d17556f --- /dev/null +++ b/lint/rule_target_required_matchers_test.go @@ -0,0 +1,136 @@ +package lint + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/prometheus/prometheus/model/labels" + "github.com/stretchr/testify/require" +) + +func TestTargetRequiredMatcherRule(t *testing.T) { + linter := newTargetRequiredMatchersRule([]*labels.Matcher{ + { + Type: labels.MatchRegexp, + Name: "instance", + Value: "$instance", + }, + }) + + for _, tc := range []struct { + name string + result Result + target Target + fixed *Target + }{ + // Happy path + { + name: "OK", + result: ResultSuccess, + target: Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + // Also happy when the promql is invalid + { + name: "OK-invalid-promql", + result: ResultSuccess, + target: Target{ + Expr: `foo(bar.baz))`, + }, + }, + // Missing matcher + { + name: "autofix-missing-matcher", + result: Result{ + Severity: Fixed, + Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo[5m]))': %s selector not found", "instance"), + }, + target: Target{ + Expr: `sum(rate(foo[5m]))`, + }, + fixed: &Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + // Not a regex matcher + { + name: "autofix-not-regex-matcher", + result: Result{ + Severity: Fixed, + Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo{%s=\"$%s\"}[5m]))': %s selector is =, not =~", "instance", "instance", "instance"), + }, + target: Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s="$%s"}[5m]))`, "instance", "instance"), + }, + fixed: &Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + // Wrong template variable + { + name: "autofix-wrong-template-variable", + result: Result{ + Severity: Fixed, + Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo{%s=~\"$foo\"}[5m]))': %s selector is $foo, not $%s", "instance", "instance", "instance"), + }, + target: Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$foo"}[5m]))`, "instance"), + }, + fixed: &Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + } { + dashboard := Dashboard{ + Title: "dashboard", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + }, + }, + Panels: []Panel{ + { + Title: "panel", + Type: "singlestat", + Targets: []Target{tc.target}, + }, + }, + } + t.Run(tc.name, func(t *testing.T) { + autofix := tc.fixed != nil + testRuleWithAutofix(t, linter, &dashboard, []Result{tc.result}, autofix) + if autofix { + fixedDashboard := Dashboard{ + Title: "dashboard", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + }, + }, + Panels: []Panel{ + { + Title: "panel", + Type: "singlestat", + Targets: []Target{*tc.fixed}, + }, + }, + } + expected, _ := json.Marshal(fixedDashboard) + actual, _ := json.Marshal(dashboard) + require.Equal(t, string(expected), string(actual)) + } + }) + } +} diff --git a/lint/rule_template_instance.go b/lint/rule_template_instance.go deleted file mode 100644 index 01142e4..0000000 --- a/lint/rule_template_instance.go +++ /dev/null @@ -1,19 +0,0 @@ -package lint - -func NewTemplateInstanceRule() *DashboardRuleFunc { - return &DashboardRuleFunc{ - name: "template-instance-rule", - description: "Checks that the dashboard has a templated instance.", - fn: func(d Dashboard) DashboardRuleResults { - r := DashboardRuleResults{} - - template := getTemplateDatasource(d) - if template == nil || template.Query != Prometheus { - return r - } - - checkTemplate(d, "instance", &r) - return r - }, - } -} diff --git a/lint/rule_template_instance_test.go b/lint/rule_template_instance_test.go deleted file mode 100644 index 6c395e4..0000000 --- a/lint/rule_template_instance_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package lint - -import "testing" - -func TestInstanceTemplate(t *testing.T) { - linter := NewTemplateInstanceRule() - - for _, tc := range []struct { - result Result - dashboard Dashboard - }{ - // Non-promtheus dashboards shouldn't fail. - { - result: ResultSuccess, - dashboard: Dashboard{ - Title: "test", - }, - }, - // Missing instance templates. - { - result: Result{ - Severity: Error, - Message: "Dashboard 'test' is missing the instance template", - }, - dashboard: Dashboard{ - Title: "test", - Templating: struct { - List []Template `json:"list"` - }{ - List: []Template{ - { - Type: "datasource", - Query: "prometheus", - }, - { - Name: "job", - Datasource: "$datasource", - Type: "query", - Label: "Job", - Multi: true, - AllValue: ".+", - }, - }, - }, - }, - }, - // What success looks like. - { - result: ResultSuccess, - dashboard: Dashboard{ - Title: "test", - Templating: struct { - List []Template `json:"list"` - }{ - List: []Template{ - { - Type: "datasource", - Query: "prometheus", - }, - { - Name: "job", - Datasource: "$datasource", - Type: "query", - Label: "Job", - Multi: true, - AllValue: ".+", - }, - { - Name: "instance", - Datasource: "${datasource}", - Type: "query", - Label: "Instance", - Multi: true, - AllValue: ".+", - }, - }, - }, - }, - }, - } { - testRule(t, linter, tc.dashboard, tc.result) - } -} diff --git a/lint/rule_template_variable_matchers.go b/lint/rule_template_variable_matchers.go new file mode 100644 index 0000000..e19ebe0 --- /dev/null +++ b/lint/rule_template_variable_matchers.go @@ -0,0 +1,29 @@ +package lint + +import ( + "strings" + + "github.com/prometheus/prometheus/model/labels" +) + +func NewTemplateVariableMatchersRule(matchers []*labels.Matcher) *DashboardRuleFunc { + return &DashboardRuleFunc{ + name: "template-variable-matchers-rule", + description: "Checks that the dashboard has a template variable for required matchers that use variables", + fn: func(d Dashboard) DashboardRuleResults { + r := DashboardRuleResults{} + + template := getTemplateDatasource(d) + if template == nil || template.Query != Prometheus { + return r + } + + for _, m := range matchers { + if strings.HasPrefix(m.Value, "$") { + checkTemplate(d, m.Value[1:], &r) + } + } + return r + }, + } +} diff --git a/lint/rule_template_job_test.go b/lint/rule_template_variable_matchers_test.go similarity index 95% rename from lint/rule_template_job_test.go rename to lint/rule_template_variable_matchers_test.go index 1f7d5e4..4413e51 100644 --- a/lint/rule_template_job_test.go +++ b/lint/rule_template_variable_matchers_test.go @@ -2,10 +2,18 @@ package lint import ( "testing" + + "github.com/prometheus/prometheus/model/labels" ) func TestJobTemplate(t *testing.T) { - linter := NewTemplateJobRule() + linter := NewTemplateVariableMatchersRule([]*labels.Matcher{ + { + Type: labels.MatchRegexp, + Name: "job", + Value: "$job", + }, + }) for _, tc := range []struct { name string diff --git a/lint/rules.go b/lint/rules.go index cb031fc..5bdcb4f 100644 --- a/lint/rules.go +++ b/lint/rules.go @@ -1,5 +1,7 @@ package lint +import "github.com/prometheus/prometheus/model/labels" + type Rule interface { Description() string Name() string @@ -169,12 +171,11 @@ type RuleSet struct { rules []Rule } -func NewRuleSet() RuleSet { +func NewRuleSet(matchers []*labels.Matcher) RuleSet { return RuleSet{ rules: []Rule{ NewTemplateDatasourceRule(), - NewTemplateJobRule(), - NewTemplateInstanceRule(), + NewTemplateVariableMatchersRule(matchers), NewTemplateLabelPromQLRule(), NewTemplateOnTimeRangeReloadRule(), NewPanelDatasourceRule(), @@ -183,8 +184,7 @@ func NewRuleSet() RuleSet { NewPanelNoTargetsRule(), NewTargetPromQLRule(), NewTargetRateIntervalRule(), - NewTargetJobRule(), - NewTargetInstanceRule(), + newTargetRequiredMatchersRule(matchers), NewTargetCounterAggRule(), }, } diff --git a/lint/rule_template_job.go b/lint/template_utils.go similarity index 79% rename from lint/rule_template_job.go rename to lint/template_utils.go index 0b7b674..2a4abc5 100644 --- a/lint/rule_template_job.go +++ b/lint/template_utils.go @@ -7,24 +7,6 @@ import ( "golang.org/x/text/language" ) -func NewTemplateJobRule() *DashboardRuleFunc { - return &DashboardRuleFunc{ - name: "template-job-rule", - description: "Checks that the dashboard has a templated job.", - fn: func(d Dashboard) DashboardRuleResults { - r := DashboardRuleResults{} - - template := getTemplateDatasource(d) - if template == nil || template.Query != Prometheus { - return r - } - - checkTemplate(d, "job", &r) - return r - }, - } -} - func checkTemplate(d Dashboard, name string, r *DashboardRuleResults) { t := getTemplate(d, name) if t == nil { diff --git a/main.go b/main.go index d85e7a3..c538119 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( "path" "strings" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zeitlinger/conflate" @@ -19,6 +21,7 @@ var lintVerboseFlag bool var lintAutofixFlag bool var lintReadFromStdIn bool var lintConfigFlag string +var lintExprMatchers []string // lintCmd represents the lint command var lintCmd = &cobra.Command{ @@ -34,6 +37,19 @@ var lintCmd = &cobra.Command{ var buf []byte var err error var filename string + // the matchers that need to be present in all selectors + var exprMatchers []*labels.Matcher + + // check the provided matchers are valid prometheus matchers + if len(lintExprMatchers) > 0 { + for _, m := range lintExprMatchers { + matcher, err := parser.ParseMetricSelector(fmt.Sprintf("{%s}", m)) + if err != nil { + return fmt.Errorf("failed to parse provided matcher {%s}: %v", m, err) + } + exprMatchers = append(exprMatchers, matcher[0]) + } + } if lintReadFromStdIn { if lintAutofixFlag { @@ -69,7 +85,7 @@ var lintCmd = &cobra.Command{ config.Verbose = lintVerboseFlag config.Autofix = lintAutofixFlag - rules := lint.NewRuleSet() + rules := lint.NewRuleSet(exprMatchers) results, err := rules.Lint([]lint.Dashboard{dashboard}) if err != nil { return fmt.Errorf("failed to lint dashboard: %v", err) @@ -119,7 +135,7 @@ var rulesCmd = &cobra.Command{ Short: "Print documentation about each lint rule.", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - rules := lint.NewRuleSet() + rules := lint.NewRuleSet(nil) for _, rule := range rules.Rules() { fmt.Fprintf(os.Stdout, "* `%s` - %s\n", rule.Name(), rule.Description()) } @@ -161,6 +177,13 @@ func init() { false, "read from stdin", ) + lintCmd.Flags().StringArrayVarP( + &lintExprMatchers, + "matcher", + "m", + []string{"instance=~\"$instance\"", "job=~\"$job\""}, + "matcher required to be present in all selectors, e.g. 'instance=~\"$instance\"' or 'cluster=\"$cluster\"', can be specified multiple times", + ) } var rootCmd = &cobra.Command{ From ff625b550236d9f59e3f39497e12885885ac10f8 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:41:35 +0200 Subject: [PATCH 02/26] Change var naming --- main.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index c538119..b2b39aa 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ var lintVerboseFlag bool var lintAutofixFlag bool var lintReadFromStdIn bool var lintConfigFlag string -var lintExprMatchers []string +var lintRequiredMatchers []string // lintCmd represents the lint command var lintCmd = &cobra.Command{ @@ -38,16 +38,16 @@ var lintCmd = &cobra.Command{ var err error var filename string // the matchers that need to be present in all selectors - var exprMatchers []*labels.Matcher + var requiredMatchers []*labels.Matcher // check the provided matchers are valid prometheus matchers - if len(lintExprMatchers) > 0 { - for _, m := range lintExprMatchers { + if len(lintRequiredMatchers) > 0 { + for _, m := range lintRequiredMatchers { matcher, err := parser.ParseMetricSelector(fmt.Sprintf("{%s}", m)) if err != nil { return fmt.Errorf("failed to parse provided matcher {%s}: %v", m, err) } - exprMatchers = append(exprMatchers, matcher[0]) + requiredMatchers = append(requiredMatchers, matcher[0]) } } @@ -85,7 +85,7 @@ var lintCmd = &cobra.Command{ config.Verbose = lintVerboseFlag config.Autofix = lintAutofixFlag - rules := lint.NewRuleSet(exprMatchers) + rules := lint.NewRuleSet(requiredMatchers) results, err := rules.Lint([]lint.Dashboard{dashboard}) if err != nil { return fmt.Errorf("failed to lint dashboard: %v", err) @@ -178,7 +178,7 @@ func init() { "read from stdin", ) lintCmd.Flags().StringArrayVarP( - &lintExprMatchers, + &lintRequiredMatchers, "matcher", "m", []string{"instance=~\"$instance\"", "job=~\"$job\""}, From 9527c055b127f9f3dc227dffbaccca146a3ccf0d Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:04:57 +0200 Subject: [PATCH 03/26] update docs --- docs/index.md | 36 +++++++++++++--------------- docs/rules/target-instance-rule.md | 2 -- docs/rules/target-job-rule.md | 3 --- docs/rules/template-instance-rule.md | 14 ----------- docs/rules/template-job-rule.md | 14 ----------- 5 files changed, 17 insertions(+), 52 deletions(-) delete mode 100644 docs/rules/target-instance-rule.md delete mode 100644 docs/rules/target-job-rule.md delete mode 100644 docs/rules/template-instance-rule.md delete mode 100644 docs/rules/template-job-rule.md diff --git a/docs/index.md b/docs/index.md index 53f9a62..740843c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,11 +51,13 @@ Usage: dashboard-linter lint [dashboard.json] [flags] Flags: - -c, --config string path to a configuration file - --fix automatically fix problems if possible - -h, --help help for lint - --strict fail upon linting error or warning - --verbose show more information about linting + -c, --config string path to a configuration file + --fix automatically fix problems if possible + -h, --help help for lint + -m, --matcher stringArray matcher required to be present in all selectors, e.g. 'instance=~"$instance"' or 'cluster="$cluster"', can be specified multiple times (default ["instance=~""$instance""","job=~""$job"""]) + --stdin read from stdin + --strict fail upon linting error or warning + --verbose show more information about linting ``` # Rules @@ -63,8 +65,7 @@ Flags: The linter implements the following rules: * [template-datasource-rule](./rules/template-datasource-rule.md) - Checks that the dashboard has a templated datasource. -* [template-job-rule](./rules/template-job-rule.md) - Checks that the dashboard has a templated job. -* [template-instance-rule](./rules/template-instance-rule.md) - Checks that the dashboard has a templated instance. +* [template-variable-matchers-rule](./rules/template-variable-matchers-rule.md) - Checks that the dashboard has a template variable for required matchers that use variables. * [template-label-promql-rule](./rules/template-label-promql-rule.md) - Checks that the dashboard templated labels have proper PromQL expressions. * [template-on-time-change-reload-rule](./rules/template-on-time-change-reload-rule.md) - Checks that the dashboard template variables are configured to reload on time change. * [panel-datasource-rule](./rules/panel-datasource-rule.md) - Checks that each panel uses the templated datasource. @@ -73,22 +74,19 @@ The linter implements the following rules: * `panel-no-targets-rule` - Checks that each panel has at least one target. * [target-promql-rule](./rules/target-promql-rule.md) - Checks that each target uses a valid PromQL query. * [target-rate-interval-rule](./rules/target-rate-interval-rule.md) - Checks that each target uses $__rate_interval. -* [target-job-rule](./rules/target-job-rule.md) - Checks that every PromQL query has a job matcher. -* [target-instance-rule](./rules/target-instance-rule.md) - Checks that every PromQL query has a instance matcher. +* [target-required-matchers](./rules/target-required-matchers.md) - Checks that target expr has the required matchers. * `target-counter-agg-rule` - Checks that any counter metric (ending in _total) is aggregated with rate, irate, or increase. ## Related Rules There are groups of rules that are intended to drive certain outcomes, but may be implemented separately to allow more granular [exceptions](#exclusions-and-warnings), and to keep the rules terse. -### Job and Instance Template Variables +### Required Matchers And Template Variables -The following rules work together to ensure that every dashboard has template variables for `Job` and `Instance`, that they are properly configured, and used in every promql query. +The following rules work together to ensure that every dashboard has template variables for required matchers that use variables, and that they are properly configured, and used in every promql query. -* [template-job-rule](./rules/template-job-rule.md) -* [template-instance-rule](./rules/template-instance-rule.md) -* [target-job-rule](./rules/target-job-rule.md) -* [target-instance-rule](./rules/target-instance-rule.md) +* [template-variable-matchers-rule](./rules/template-variable-matchers-rule.md) +* [target-required-matchers](./rules/target-required-matchers.md) These rules enforce a best practice for dashboards with a single Prometheus or Loki data source. Metrics and logs scraped by Prometheus and Loki have automatically generated [job and instance labels](https://prometheus.io/docs/concepts/jobs_instances/) on them. For this reason, having the ability to filter by these assured always-present labels is logical and a useful additional feature. @@ -109,9 +107,9 @@ Where the rules above don't make sense, you can add a `.lint` file in the same d Example: ```yaml exclusions: - template-job-rule: + template-variable-matchers-rule: warnings: - template-instance-rule: + template-variable-matchers-rule: ``` ## Reasons @@ -121,7 +119,7 @@ Whenever you exclude or warn for a rule, it's recommended that you provide a rea Example: ```yaml exclusions: - template-job-rule: + template-variable-matchers-rule: reason: A job matcher is hardcoded into the recording rule used for all queries on these dashboards. ``` @@ -139,7 +137,7 @@ exclusions: panel: Top 10 Duration Rate - dashboard: Apollo Server panel: Top 10 Slowest Fields Resolution - target-instance-rule: + template-variable-matchers-rule: reason: Totals are intended to be across all instances entries: - panel: Requests Per Second diff --git a/docs/rules/target-instance-rule.md b/docs/rules/target-instance-rule.md deleted file mode 100644 index 9adb88e..0000000 --- a/docs/rules/target-instance-rule.md +++ /dev/null @@ -1,2 +0,0 @@ -# target-instance-rule -Checks that each PromQL query has an instance matcher. See [Job and Instance Template Variables](../index.md#job-and-instance-template-variables) for more information about rules relating to this one. \ No newline at end of file diff --git a/docs/rules/target-job-rule.md b/docs/rules/target-job-rule.md deleted file mode 100644 index dac98ec..0000000 --- a/docs/rules/target-job-rule.md +++ /dev/null @@ -1,3 +0,0 @@ -# target-job-rule -Checks that each PromQL query has a job matcher. See [Job and Instance Template Variables](../index.md#job-and-instance-template-variables) for more information about rules relating to this one. - diff --git a/docs/rules/template-instance-rule.md b/docs/rules/template-instance-rule.md deleted file mode 100644 index b3b9422..0000000 --- a/docs/rules/template-instance-rule.md +++ /dev/null @@ -1,14 +0,0 @@ -# template-instance-rule -Checks that each dashboard has a templated instance. See [Job and Instance Template Variables](../index.md#job-and-instance-template-variables) for more information about rules relating to this one. - -# Best Practice -The rule ensures all of the following conditions. - -* The dashboard template exists. -* The dashboard template is named `instance`. -* The dashboard template is labeled `instance`. -* The dashboard template uses a templated datasource, specifically named `$datasource`. -* The dashboard template uses a Prometheus query to find available matching instances. -* The dashboard template is multi select -* The dashboard template has an allValue of `.+` - diff --git a/docs/rules/template-job-rule.md b/docs/rules/template-job-rule.md deleted file mode 100644 index 1d9ab2a..0000000 --- a/docs/rules/template-job-rule.md +++ /dev/null @@ -1,14 +0,0 @@ -# template-job-rule -Checks that each dashboard has a templated job. See [Job and Instance Template Variables](../index.md#job-and-instance-template-variables) for more information about rules relating to this one. - -# Best Practice -The rule ensures all of the following conditions. - -* The dashboard template exists. -* The dashboard template is named `job`. -* The dashboard template is labeled `job`. -* The dashboard template uses a templated datasource, specifically named `$datasource`. -* The dashboard template uses a Prometheus query to find available matching jobs. -* The dashboard template is multi select -* The dashboard template has an allValue of `.+` - From 5d25007a7d61efbb7667e6f6be31cf88d865a7f7 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:05:11 +0200 Subject: [PATCH 04/26] update docs add new --- docs/rules/target-required-matchers.md | 2 ++ docs/rules/template-variable-matchers-rule.md | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 docs/rules/target-required-matchers.md create mode 100644 docs/rules/template-variable-matchers-rule.md diff --git a/docs/rules/target-required-matchers.md b/docs/rules/target-required-matchers.md new file mode 100644 index 0000000..6cfe55c --- /dev/null +++ b/docs/rules/target-required-matchers.md @@ -0,0 +1,2 @@ +# target-required-matchers +Checks that each PromQL query has the required matchers. See [Required Matchers And Template Variables](../index.md#required-matchers-and-template-variables) for more information about rules relating to this one. \ No newline at end of file diff --git a/docs/rules/template-variable-matchers-rule.md b/docs/rules/template-variable-matchers-rule.md new file mode 100644 index 0000000..9673ed6 --- /dev/null +++ b/docs/rules/template-variable-matchers-rule.md @@ -0,0 +1,14 @@ +# template-variable-matchers-rule +Checks that each dashboard has a templated variable corresponding to a required matcher variable. See [Required Matchers And Template Variables](../index.md#required-matchers-and-template-variables) for more information about rules relating to this one. + +# Best Practice +The rule ensures all of the following conditions. + +* The dashboard template exists. +* The dashboard template is named after the required variable by matcher. +* The dashboard template is labeled after the required variable by matcher. +* The dashboard template uses a templated datasource, specifically named `$datasource`. +* The dashboard template uses a Prometheus query to find available matching instances. +* The dashboard template is multi select +* The dashboard template has an allValue of `.+` + From a3f79d8b77dd14d1523213df4d66eedce1ebc758 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Fri, 12 Jul 2024 06:24:22 +0200 Subject: [PATCH 05/26] :rewind: :hankey: - Fix chmod... --- .github/dependabot.yml | 0 .github/workflows/build.yml | 0 .github/workflows/golangci-lint.yml | 0 .github/workflows/test.yml | 0 .gitignore | 0 LICENSE | 0 Makefile | 0 README.md | 0 docs/index.md | 0 docs/rules/panel-datasource-rule.md | 0 docs/rules/panel-title-description-rule.md | 0 docs/rules/panel-units-rule.md | 0 docs/rules/target-instance-rule.md | 0 docs/rules/target-job-rule.md | 0 docs/rules/target-promql-rule.md | 0 docs/rules/target-rate-interval-rule.md | 0 docs/rules/template-datasource-rule.md | 0 docs/rules/template-instance-rule.md | 0 docs/rules/template-job-rule.md | 0 docs/rules/template-label-promql-rule.md | 0 docs/rules/template-on-time-change-reload-rule.md | 0 docs/rules/template-uneditable-rule.md | 0 go.mod | 0 go.sum | 0 lint/configuration.go | 0 lint/constants.go | 0 lint/lint.go | 0 lint/lint_test.go | 0 lint/model_test.go | 0 lint/results.go | 0 lint/rule_panel_datasource.go | 0 lint/rule_panel_datasource_test.go | 0 lint/rule_panel_no_targets.go | 0 lint/rule_panel_no_targets_test.go | 0 lint/rule_panel_title_description.go | 0 lint/rule_panel_title_description_test.go | 0 lint/rule_panel_units.go | 0 lint/rule_panel_units_test.go | 0 lint/rule_target_counter_agg.go | 0 lint/rule_target_counter_agg_test.go | 0 lint/rule_target_job_instance.go | 0 lint/rule_target_job_instance_test.go | 0 lint/rule_target_promql.go | 0 lint/rule_target_promql_test.go | 0 lint/rule_target_rate_interval.go | 0 lint/rule_target_rate_interval_test.go | 0 lint/rule_target_required_matchers.go | 0 lint/rule_target_required_matchers_test.go | 0 lint/rule_template_datasource.go | 0 lint/rule_template_datasource_test.go | 0 lint/rule_template_instance.go | 0 lint/rule_template_instance_test.go | 0 lint/rule_template_job.go | 0 lint/rule_template_job_test.go | 0 lint/rule_template_label_promql.go | 0 lint/rule_template_label_promql_test.go | 0 lint/rule_template_on_time_change_reload.go | 0 lint/rule_template_on_time_change_reload_test.go | 0 lint/rule_template_required_variables.go | 0 lint/rule_template_required_variables_test.go | 0 lint/rule_uneditable.go | 0 lint/rule_uneditable_test.go | 0 lint/rules.go | 0 lint/rules_test.go | 0 lint/target_utils.go | 0 lint/template_utils.go | 0 lint/testdata/dashboard.json | 0 lint/variables.go | 0 lint/variables_test.go | 0 main.go | 0 70 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 .github/dependabot.yml mode change 100755 => 100644 .github/workflows/build.yml mode change 100755 => 100644 .github/workflows/golangci-lint.yml mode change 100755 => 100644 .github/workflows/test.yml mode change 100755 => 100644 .gitignore mode change 100755 => 100644 LICENSE mode change 100755 => 100644 Makefile mode change 100755 => 100644 README.md mode change 100755 => 100644 docs/index.md mode change 100755 => 100644 docs/rules/panel-datasource-rule.md mode change 100755 => 100644 docs/rules/panel-title-description-rule.md mode change 100755 => 100644 docs/rules/panel-units-rule.md mode change 100755 => 100644 docs/rules/target-instance-rule.md mode change 100755 => 100644 docs/rules/target-job-rule.md mode change 100755 => 100644 docs/rules/target-promql-rule.md mode change 100755 => 100644 docs/rules/target-rate-interval-rule.md mode change 100755 => 100644 docs/rules/template-datasource-rule.md mode change 100755 => 100644 docs/rules/template-instance-rule.md mode change 100755 => 100644 docs/rules/template-job-rule.md mode change 100755 => 100644 docs/rules/template-label-promql-rule.md mode change 100755 => 100644 docs/rules/template-on-time-change-reload-rule.md mode change 100755 => 100644 docs/rules/template-uneditable-rule.md mode change 100755 => 100644 go.mod mode change 100755 => 100644 go.sum mode change 100755 => 100644 lint/configuration.go mode change 100755 => 100644 lint/constants.go mode change 100755 => 100644 lint/lint.go mode change 100755 => 100644 lint/lint_test.go mode change 100755 => 100644 lint/model_test.go mode change 100755 => 100644 lint/results.go mode change 100755 => 100644 lint/rule_panel_datasource.go mode change 100755 => 100644 lint/rule_panel_datasource_test.go mode change 100755 => 100644 lint/rule_panel_no_targets.go mode change 100755 => 100644 lint/rule_panel_no_targets_test.go mode change 100755 => 100644 lint/rule_panel_title_description.go mode change 100755 => 100644 lint/rule_panel_title_description_test.go mode change 100755 => 100644 lint/rule_panel_units.go mode change 100755 => 100644 lint/rule_panel_units_test.go mode change 100755 => 100644 lint/rule_target_counter_agg.go mode change 100755 => 100644 lint/rule_target_counter_agg_test.go mode change 100755 => 100644 lint/rule_target_job_instance.go mode change 100755 => 100644 lint/rule_target_job_instance_test.go mode change 100755 => 100644 lint/rule_target_promql.go mode change 100755 => 100644 lint/rule_target_promql_test.go mode change 100755 => 100644 lint/rule_target_rate_interval.go mode change 100755 => 100644 lint/rule_target_rate_interval_test.go mode change 100755 => 100644 lint/rule_target_required_matchers.go mode change 100755 => 100644 lint/rule_target_required_matchers_test.go mode change 100755 => 100644 lint/rule_template_datasource.go mode change 100755 => 100644 lint/rule_template_datasource_test.go mode change 100755 => 100644 lint/rule_template_instance.go mode change 100755 => 100644 lint/rule_template_instance_test.go mode change 100755 => 100644 lint/rule_template_job.go mode change 100755 => 100644 lint/rule_template_job_test.go mode change 100755 => 100644 lint/rule_template_label_promql.go mode change 100755 => 100644 lint/rule_template_label_promql_test.go mode change 100755 => 100644 lint/rule_template_on_time_change_reload.go mode change 100755 => 100644 lint/rule_template_on_time_change_reload_test.go mode change 100755 => 100644 lint/rule_template_required_variables.go mode change 100755 => 100644 lint/rule_template_required_variables_test.go mode change 100755 => 100644 lint/rule_uneditable.go mode change 100755 => 100644 lint/rule_uneditable_test.go mode change 100755 => 100644 lint/rules.go mode change 100755 => 100644 lint/rules_test.go mode change 100755 => 100644 lint/target_utils.go mode change 100755 => 100644 lint/template_utils.go mode change 100755 => 100644 lint/testdata/dashboard.json mode change 100755 => 100644 lint/variables.go mode change 100755 => 100644 lint/variables_test.go mode change 100755 => 100644 main.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml old mode 100755 new mode 100644 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml old mode 100755 new mode 100644 diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml old mode 100755 new mode 100644 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml old mode 100755 new mode 100644 diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 diff --git a/Makefile b/Makefile old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/docs/index.md b/docs/index.md old mode 100755 new mode 100644 diff --git a/docs/rules/panel-datasource-rule.md b/docs/rules/panel-datasource-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/panel-title-description-rule.md b/docs/rules/panel-title-description-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/panel-units-rule.md b/docs/rules/panel-units-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/target-instance-rule.md b/docs/rules/target-instance-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/target-job-rule.md b/docs/rules/target-job-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/target-promql-rule.md b/docs/rules/target-promql-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/target-rate-interval-rule.md b/docs/rules/target-rate-interval-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/template-datasource-rule.md b/docs/rules/template-datasource-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/template-instance-rule.md b/docs/rules/template-instance-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/template-job-rule.md b/docs/rules/template-job-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/template-label-promql-rule.md b/docs/rules/template-label-promql-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/template-on-time-change-reload-rule.md b/docs/rules/template-on-time-change-reload-rule.md old mode 100755 new mode 100644 diff --git a/docs/rules/template-uneditable-rule.md b/docs/rules/template-uneditable-rule.md old mode 100755 new mode 100644 diff --git a/go.mod b/go.mod old mode 100755 new mode 100644 diff --git a/go.sum b/go.sum old mode 100755 new mode 100644 diff --git a/lint/configuration.go b/lint/configuration.go old mode 100755 new mode 100644 diff --git a/lint/constants.go b/lint/constants.go old mode 100755 new mode 100644 diff --git a/lint/lint.go b/lint/lint.go old mode 100755 new mode 100644 diff --git a/lint/lint_test.go b/lint/lint_test.go old mode 100755 new mode 100644 diff --git a/lint/model_test.go b/lint/model_test.go old mode 100755 new mode 100644 diff --git a/lint/results.go b/lint/results.go old mode 100755 new mode 100644 diff --git a/lint/rule_panel_datasource.go b/lint/rule_panel_datasource.go old mode 100755 new mode 100644 diff --git a/lint/rule_panel_datasource_test.go b/lint/rule_panel_datasource_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_panel_no_targets.go b/lint/rule_panel_no_targets.go old mode 100755 new mode 100644 diff --git a/lint/rule_panel_no_targets_test.go b/lint/rule_panel_no_targets_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_panel_title_description.go b/lint/rule_panel_title_description.go old mode 100755 new mode 100644 diff --git a/lint/rule_panel_title_description_test.go b/lint/rule_panel_title_description_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_panel_units.go b/lint/rule_panel_units.go old mode 100755 new mode 100644 diff --git a/lint/rule_panel_units_test.go b/lint/rule_panel_units_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_counter_agg.go b/lint/rule_target_counter_agg.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_counter_agg_test.go b/lint/rule_target_counter_agg_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_job_instance.go b/lint/rule_target_job_instance.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_job_instance_test.go b/lint/rule_target_job_instance_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_promql.go b/lint/rule_target_promql.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_promql_test.go b/lint/rule_target_promql_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_rate_interval.go b/lint/rule_target_rate_interval.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_rate_interval_test.go b/lint/rule_target_rate_interval_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go old mode 100755 new mode 100644 diff --git a/lint/rule_target_required_matchers_test.go b/lint/rule_target_required_matchers_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_datasource.go b/lint/rule_template_datasource.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_datasource_test.go b/lint/rule_template_datasource_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_instance.go b/lint/rule_template_instance.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_instance_test.go b/lint/rule_template_instance_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_job.go b/lint/rule_template_job.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_job_test.go b/lint/rule_template_job_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_label_promql.go b/lint/rule_template_label_promql.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_label_promql_test.go b/lint/rule_template_label_promql_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_on_time_change_reload.go b/lint/rule_template_on_time_change_reload.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_on_time_change_reload_test.go b/lint/rule_template_on_time_change_reload_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_required_variables.go b/lint/rule_template_required_variables.go old mode 100755 new mode 100644 diff --git a/lint/rule_template_required_variables_test.go b/lint/rule_template_required_variables_test.go old mode 100755 new mode 100644 diff --git a/lint/rule_uneditable.go b/lint/rule_uneditable.go old mode 100755 new mode 100644 diff --git a/lint/rule_uneditable_test.go b/lint/rule_uneditable_test.go old mode 100755 new mode 100644 diff --git a/lint/rules.go b/lint/rules.go old mode 100755 new mode 100644 diff --git a/lint/rules_test.go b/lint/rules_test.go old mode 100755 new mode 100644 diff --git a/lint/target_utils.go b/lint/target_utils.go old mode 100755 new mode 100644 diff --git a/lint/template_utils.go b/lint/template_utils.go old mode 100755 new mode 100644 diff --git a/lint/testdata/dashboard.json b/lint/testdata/dashboard.json old mode 100755 new mode 100644 diff --git a/lint/variables.go b/lint/variables.go old mode 100755 new mode 100644 diff --git a/lint/variables_test.go b/lint/variables_test.go old mode 100755 new mode 100644 diff --git a/main.go b/main.go old mode 100755 new mode 100644 From dcfc870b9b0ba1d7a921b52b79616f46ba1ba41b Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Fri, 12 Jul 2024 07:19:06 +0200 Subject: [PATCH 06/26] :lipstick: - Add way of indicating a rules stability + Change printing of rules description --- lint/rule_panel_datasource.go | 1 + lint/rule_panel_no_targets.go | 1 + lint/rule_panel_title_description.go | 1 + lint/rule_panel_units.go | 1 + lint/rule_target_counter_agg.go | 1 + lint/rule_target_job_instance.go | 1 + lint/rule_target_promql.go | 1 + lint/rule_target_rate_interval.go | 1 + lint/rule_target_required_matchers.go | 1 + lint/rule_template_datasource.go | 1 + lint/rule_template_instance.go | 1 + lint/rule_template_job.go | 1 + lint/rule_template_label_promql.go | 1 + lint/rule_template_on_time_change_reload.go | 1 + lint/rule_template_required_variables.go | 1 + lint/rule_uneditable.go | 1 + lint/rules.go | 28 ++++++++++--------- lint/rules_test.go | 6 ++--- main.go | 30 +++++++++++++-------- 19 files changed, 54 insertions(+), 26 deletions(-) diff --git a/lint/rule_panel_datasource.go b/lint/rule_panel_datasource.go index ac0a9d2..401cc52 100644 --- a/lint/rule_panel_datasource.go +++ b/lint/rule_panel_datasource.go @@ -8,6 +8,7 @@ func NewPanelDatasourceRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-datasource-rule", description: "Checks that each panel uses the templated datasource.", + stability: "stable", fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} diff --git a/lint/rule_panel_no_targets.go b/lint/rule_panel_no_targets.go index 79a249a..29ab6ed 100644 --- a/lint/rule_panel_no_targets.go +++ b/lint/rule_panel_no_targets.go @@ -4,6 +4,7 @@ func NewPanelNoTargetsRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-no-targets-rule", description: "Checks that each panel has at least one target.", + stability: "stable", fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_panel_title_description.go b/lint/rule_panel_title_description.go index eeb7506..993af64 100644 --- a/lint/rule_panel_title_description.go +++ b/lint/rule_panel_title_description.go @@ -6,6 +6,7 @@ func NewPanelTitleDescriptionRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-title-description-rule", description: "Checks that each panel has a title and description.", + stability: "stable", fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_panel_units.go b/lint/rule_panel_units.go index ace2c3f..5de00b5 100644 --- a/lint/rule_panel_units.go +++ b/lint/rule_panel_units.go @@ -66,6 +66,7 @@ func NewPanelUnitsRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-units-rule", description: "Checks that each panel uses has valid units defined.", + stability: "stable", fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_target_counter_agg.go b/lint/rule_target_counter_agg.go index bb87d06..87dfe61 100644 --- a/lint/rule_target_counter_agg.go +++ b/lint/rule_target_counter_agg.go @@ -11,6 +11,7 @@ func NewTargetCounterAggRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-counter-agg-rule", description: "Checks that any counter metric (ending in _total) is aggregated with rate, irate, or increase.", + stability: "stable", fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} expr, err := parsePromQL(t.Expr, d.Templating.List) diff --git a/lint/rule_target_job_instance.go b/lint/rule_target_job_instance.go index 968f679..1f2f322 100644 --- a/lint/rule_target_job_instance.go +++ b/lint/rule_target_job_instance.go @@ -11,6 +11,7 @@ func newTargetRequiredMatcherRule(matcher string) *TargetRuleFunc { return &TargetRuleFunc{ name: fmt.Sprintf("target-%s-rule", matcher), description: fmt.Sprintf("Checks that every PromQL query has a %s matcher.", matcher), + stability: "stable", fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} // TODO: The RuleSet should be responsible for routing rule checks based on their query type (prometheus, loki, mysql, etc) diff --git a/lint/rule_target_promql.go b/lint/rule_target_promql.go index 2159b80..af00ff5 100644 --- a/lint/rule_target_promql.go +++ b/lint/rule_target_promql.go @@ -39,6 +39,7 @@ func NewTargetPromQLRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-promql-rule", description: "Checks that each target uses a valid PromQL query.", + stability: "stable", fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} diff --git a/lint/rule_target_rate_interval.go b/lint/rule_target_rate_interval.go index 55fbc6f..cf65238 100644 --- a/lint/rule_target_rate_interval.go +++ b/lint/rule_target_rate_interval.go @@ -27,6 +27,7 @@ func NewTargetRateIntervalRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-rate-interval-rule", description: "Checks that each target uses $__rate_interval.", + stability: "stable", fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} if t := getTemplateDatasource(d); t == nil || t.Query != Prometheus { diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go index c8a3956..9901513 100644 --- a/lint/rule_target_required_matchers.go +++ b/lint/rule_target_required_matchers.go @@ -16,6 +16,7 @@ func NewTargetRequiredMatchersRule(config *TargetRequiredMatchersRuleSettings) * return &TargetRuleFunc{ name: "target-required-matchers", description: "Checks that target expr has the required matchers", + stability: "experimental", fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} // TODO: The RuleSet should be responsible for routing rule checks based on their query type (prometheus, loki, mysql, etc) diff --git a/lint/rule_template_datasource.go b/lint/rule_template_datasource.go index 37bffab..00f50ed 100644 --- a/lint/rule_template_datasource.go +++ b/lint/rule_template_datasource.go @@ -12,6 +12,7 @@ func NewTemplateDatasourceRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-datasource-rule", description: "Checks that the dashboard has a templated datasource.", + stability: "stable", fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_instance.go b/lint/rule_template_instance.go index 01142e4..a8f1701 100644 --- a/lint/rule_template_instance.go +++ b/lint/rule_template_instance.go @@ -4,6 +4,7 @@ func NewTemplateInstanceRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-instance-rule", description: "Checks that the dashboard has a templated instance.", + stability: "stable", fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_job.go b/lint/rule_template_job.go index cd8a15f..74be18d 100644 --- a/lint/rule_template_job.go +++ b/lint/rule_template_job.go @@ -4,6 +4,7 @@ func NewTemplateJobRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-job-rule", description: "Checks that the dashboard has a templated job.", + stability: "stable", fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_label_promql.go b/lint/rule_template_label_promql.go index 62c3357..4576b1b 100644 --- a/lint/rule_template_label_promql.go +++ b/lint/rule_template_label_promql.go @@ -43,6 +43,7 @@ func NewTemplateLabelPromQLRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-label-promql-rule", description: "Checks that the dashboard templated labels have proper PromQL expressions.", + stability: "stable", fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_on_time_change_reload.go b/lint/rule_template_on_time_change_reload.go index cef025a..6bf5772 100644 --- a/lint/rule_template_on_time_change_reload.go +++ b/lint/rule_template_on_time_change_reload.go @@ -8,6 +8,7 @@ func NewTemplateOnTimeRangeReloadRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-on-time-change-reload-rule", description: "Checks that the dashboard template variables are configured to reload on time change.", + stability: "stable", fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_required_variables.go b/lint/rule_template_required_variables.go index 60266a7..25b40fd 100644 --- a/lint/rule_template_required_variables.go +++ b/lint/rule_template_required_variables.go @@ -13,6 +13,7 @@ func NewTemplateRequiredVariablesRule(config *TemplateRequiredVariablesRuleSetti return &DashboardRuleFunc{ name: "template-required-variables-rule", description: "Checks that the dashboard has a template variable for required variables or matchers that use variables", + stability: "experimental", fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_uneditable.go b/lint/rule_uneditable.go index ac5f410..deb469a 100644 --- a/lint/rule_uneditable.go +++ b/lint/rule_uneditable.go @@ -4,6 +4,7 @@ func NewUneditableRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "uneditable-dashboard", description: "Checks that the dashboard is not editable.", + stability: "stable", fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} if d.Editable { diff --git a/lint/rules.go b/lint/rules.go index ab560f0..7cbefde 100644 --- a/lint/rules.go +++ b/lint/rules.go @@ -3,20 +3,22 @@ package lint type Rule interface { Description() string Name() string + Stability() string Lint(Dashboard, *ResultSet) } type DashboardRuleFunc struct { - name, description string - fn func(Dashboard) DashboardRuleResults + name, description, stability string + fn func(Dashboard) DashboardRuleResults } -func NewDashboardRuleFunc(name, description string, fn func(Dashboard) DashboardRuleResults) Rule { - return &DashboardRuleFunc{name, description, fn} +func NewDashboardRuleFunc(name, description, stability string, fn func(Dashboard) DashboardRuleResults) Rule { + return &DashboardRuleFunc{name, description, stability, fn} } func (f DashboardRuleFunc) Name() string { return f.name } func (f DashboardRuleFunc) Description() string { return f.description } +func (f DashboardRuleFunc) Stability() string { return f.stability } func (f DashboardRuleFunc) Lint(d Dashboard, s *ResultSet) { dashboardResults := f.fn(d).Results if len(dashboardResults) == 0 { @@ -49,16 +51,17 @@ func (f DashboardRuleFunc) Lint(d Dashboard, s *ResultSet) { } type PanelRuleFunc struct { - name, description string - fn func(Dashboard, Panel) PanelRuleResults + name, description, stability string + fn func(Dashboard, Panel) PanelRuleResults } -func NewPanelRuleFunc(name, description string, fn func(Dashboard, Panel) PanelRuleResults) Rule { - return &PanelRuleFunc{name, description, fn} +func NewPanelRuleFunc(name, description, stability string, fn func(Dashboard, Panel) PanelRuleResults) Rule { + return &PanelRuleFunc{name, description, stability, fn} } func (f PanelRuleFunc) Name() string { return f.name } func (f PanelRuleFunc) Description() string { return f.description } +func (f PanelRuleFunc) Stability() string { return f.stability } func (f PanelRuleFunc) Lint(d Dashboard, s *ResultSet) { for pi, p := range d.GetPanels() { p := p // capture loop variable @@ -104,16 +107,17 @@ func fixPanel(pi int, r PanelResult) func(dashboard *Dashboard) { } type TargetRuleFunc struct { - name, description string - fn func(Dashboard, Panel, Target) TargetRuleResults + name, description, stability string + fn func(Dashboard, Panel, Target) TargetRuleResults } -func NewTargetRuleFunc(name, description string, fn func(Dashboard, Panel, Target) TargetRuleResults) Rule { - return &TargetRuleFunc{name, description, fn} +func NewTargetRuleFunc(name, description, stability string, fn func(Dashboard, Panel, Target) TargetRuleResults) Rule { + return &TargetRuleFunc{name, description, stability, fn} } func (f TargetRuleFunc) Name() string { return f.name } func (f TargetRuleFunc) Description() string { return f.description } +func (f TargetRuleFunc) Stability() string { return f.stability } func (f TargetRuleFunc) Lint(d Dashboard, s *ResultSet) { for pi, p := range d.GetPanels() { p := p // capture loop variable diff --git a/lint/rules_test.go b/lint/rules_test.go index 0263a09..2a9509d 100644 --- a/lint/rules_test.go +++ b/lint/rules_test.go @@ -19,7 +19,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of dashboard rule", rule: lint.NewDashboardRuleFunc( - "test-dashboard-rule", "Test dashboard rule", + "test-dashboard-rule", "Test dashboard rule", "stable", func(lint.Dashboard) lint.DashboardRuleResults { return lint.DashboardRuleResults{Results: []lint.DashboardResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -30,7 +30,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of panel rule", rule: lint.NewPanelRuleFunc( - "test-panel-rule", "Test panel rule", + "test-panel-rule", "Test panel rule", "stable", func(d lint.Dashboard, p lint.Panel) lint.PanelRuleResults { return lint.PanelRuleResults{Results: []lint.PanelResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -41,7 +41,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of target rule", rule: lint.NewTargetRuleFunc( - "test-target-rule", "Test target rule", + "test-target-rule", "Test target rule", "stable", func(lint.Dashboard, lint.Panel, lint.Target) lint.TargetRuleResults { return lint.TargetRuleResults{Results: []lint.TargetResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, diff --git a/main.go b/main.go index dda20b1..20f0b1f 100644 --- a/main.go +++ b/main.go @@ -15,12 +15,12 @@ import ( ) var ( - lintStrictFlag bool - lintVerboseFlag bool - lintAutofixFlag bool - lintReadFromStdIn bool - lintExperimentalFlag bool - lintConfigFlag string + lintStrictFlag bool + lintVerboseFlag bool + lintAutofixFlag bool + lintReadFromStdIn bool + lintConfigFlag string + ExperimentalFlag bool ) // lintCmd represents the lint command @@ -72,7 +72,7 @@ var lintCmd = &cobra.Command{ config.Verbose = lintVerboseFlag config.Autofix = lintAutofixFlag - rules := lint.NewRuleSet(lintExperimentalFlag, config.RuleSettings) + rules := lint.NewRuleSet(ExperimentalFlag, config.RuleSettings) results, err := rules.Lint([]lint.Dashboard{dashboard}) if err != nil { return fmt.Errorf("failed to lint dashboard: %v", err) @@ -122,9 +122,11 @@ var rulesCmd = &cobra.Command{ Short: "Print documentation about each lint rule.", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - rules := lint.NewRuleSet(true, lint.ConfigurationRuleSettings{}) + rules := lint.NewRuleSet(ExperimentalFlag, lint.ConfigurationRuleSettings{}) + fmt.Fprintf(os.Stdout, "%-40s %-15s %s\n", "Rule Name", "Stability", "Description") + fmt.Fprintf(os.Stdout, "%-40s %-15s %s\n", "---------", "---------", "-----------") for _, rule := range rules.Rules() { - fmt.Fprintf(os.Stdout, "* `%s` - %s\n", rule.Name(), rule.Description()) + fmt.Fprintf(os.Stdout, "%-40s %-15s %s\n", rule.Name(), rule.Stability(), rule.Description()) } return nil }, @@ -132,7 +134,6 @@ var rulesCmd = &cobra.Command{ func init() { rootCmd.AddCommand(lintCmd) - rootCmd.AddCommand(rulesCmd) lintCmd.Flags().BoolVar( &lintStrictFlag, "strict", @@ -165,7 +166,14 @@ func init() { "read from stdin", ) lintCmd.Flags().BoolVar( - &lintExperimentalFlag, + &ExperimentalFlag, + "experimental", + false, + "enable experimental rules", + ) + rootCmd.AddCommand(rulesCmd) + rulesCmd.Flags().BoolVar( + &ExperimentalFlag, "experimental", false, "enable experimental rules", From 09de038dab84d3c5521548be35da8739c688edac Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:41:47 +0200 Subject: [PATCH 07/26] Light cleanup work + improved testing --- lint/rule_target_required_matchers.go | 9 ++- lint/rule_template_required_variables.go | 3 - lint/rule_template_required_variables_test.go | 57 +++++++++++++++---- lint/rule_uneditable.go | 2 +- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go index 9901513..a99401b 100644 --- a/lint/rule_target_required_matchers.go +++ b/lint/rule_target_required_matchers.go @@ -14,8 +14,8 @@ type TargetRequiredMatchersRuleSettings struct { func NewTargetRequiredMatchersRule(config *TargetRequiredMatchersRuleSettings) *TargetRuleFunc { return &TargetRuleFunc{ - name: "target-required-matchers", - description: "Checks that target expr has the required matchers", + name: "target-required-matchers-rule", + description: "Checks that target PromQL query has the required matchers", stability: "experimental", fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} @@ -50,7 +50,10 @@ func fixTargetRequiredMatcherRule(name string, ty labels.MatchType, value string // no need to check for errors here, as the expression was already parsed and validated expr, _ := parsePromQL(t.Expr, d.Templating.List) // Walk the expression tree and add the matcher to all vector selectors - parser.Walk(addMatchers(name, ty, value), expr, nil) + err := parser.Walk(addMatchers(name, ty, value), expr, nil) + if err != nil { + return + } t.Expr = expr.String() } } diff --git a/lint/rule_template_required_variables.go b/lint/rule_template_required_variables.go index 25b40fd..7b10494 100644 --- a/lint/rule_template_required_variables.go +++ b/lint/rule_template_required_variables.go @@ -1,7 +1,6 @@ package lint import ( - "fmt" "strings" ) @@ -17,7 +16,6 @@ func NewTemplateRequiredVariablesRule(config *TemplateRequiredVariablesRuleSetti fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} - fmt.Println("config: ", config.Variables) template := getTemplateDatasource(d) if template == nil || template.Query != Prometheus { return r @@ -26,7 +24,6 @@ func NewTemplateRequiredVariablesRule(config *TemplateRequiredVariablesRuleSetti // Convert the config.variables to a map to leverage uniqueness... variables := make(map[string]bool) for _, v := range config.Variables { - fmt.Println("v: ", v) variables[v] = true } diff --git a/lint/rule_template_required_variables_test.go b/lint/rule_template_required_variables_test.go index 5b51a52..c334697 100644 --- a/lint/rule_template_required_variables_test.go +++ b/lint/rule_template_required_variables_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/pkg/labels" ) func TestTemplateRequiredVariable(t *testing.T) { @@ -12,7 +13,13 @@ func TestTemplateRequiredVariable(t *testing.T) { Variables: []string{"job"}, }, &TargetRequiredMatchersRuleSettings{ - Matchers: config.Matchers{}, + Matchers: config.Matchers{ + { + Name: "instance", + Type: labels.MatchRegexp, + Value: "$instance", + }, + }, }) for _, tc := range []struct { @@ -28,11 +35,11 @@ func TestTemplateRequiredVariable(t *testing.T) { }, }, { - name: "Missing job template.", - result: []Result{{ - Severity: Error, - Message: "Dashboard 'test' is missing the job template", - }}, + name: "Missing job/instance template.", + result: []Result{ + {Severity: Error, Message: "Dashboard 'test' is missing the job template"}, + {Severity: Error, Message: "Dashboard 'test' is missing the instance template"}, + }, dashboard: Dashboard{ Title: "test", Templating: struct { @@ -54,7 +61,13 @@ func TestTemplateRequiredVariable(t *testing.T) { {Severity: Error, Message: "Dashboard 'test' job template should be a Prometheus query, is currently ''"}, {Severity: Warning, Message: "Dashboard 'test' job template should be a labeled 'Job', is currently ''"}, {Severity: Error, Message: "Dashboard 'test' job template should be a multi select"}, - {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}}, + {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' instance template should use datasource '$datasource', is currently 'foo'"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a Prometheus query, is currently ''"}, + {Severity: Warning, Message: "Dashboard 'test' instance template should be a labeled 'Instance', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' instance template allValue should be '.+', is currently ''"}, + }, dashboard: Dashboard{ Title: "test", Templating: struct { @@ -69,6 +82,10 @@ func TestTemplateRequiredVariable(t *testing.T) { Name: "job", Datasource: "foo", }, + { + Name: "instance", + Datasource: "foo", + }, }, }, }, @@ -79,7 +96,12 @@ func TestTemplateRequiredVariable(t *testing.T) { {Severity: Error, Message: "Dashboard 'test' job template should be a Prometheus query, is currently 'bar'"}, {Severity: Warning, Message: "Dashboard 'test' job template should be a labeled 'Job', is currently ''"}, {Severity: Error, Message: "Dashboard 'test' job template should be a multi select"}, - {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}}, + {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a Prometheus query, is currently 'bar'"}, + {Severity: Warning, Message: "Dashboard 'test' instance template should be a labeled 'Instance', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' instance template allValue should be '.+', is currently ''"}, + }, dashboard: Dashboard{ Title: "test", Templating: struct { @@ -95,16 +117,25 @@ func TestTemplateRequiredVariable(t *testing.T) { Datasource: "$datasource", Type: "bar", }, + { + Name: "instance", + Datasource: "$datasource", + Type: "bar", + }, }, }, }, }, { - name: "Wrong job label.", + name: "Wrong job/instance label.", result: []Result{ {Severity: Warning, Message: "Dashboard 'test' job template should be a labeled 'Job', is currently 'bar'"}, {Severity: Error, Message: "Dashboard 'test' job template should be a multi select"}, - {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}}, + {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}, + {Severity: Warning, Message: "Dashboard 'test' instance template should be a labeled 'Instance', is currently 'bar'"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' instance template allValue should be '.+', is currently ''"}, + }, dashboard: Dashboard{ Title: "test", Templating: struct { @@ -121,6 +152,12 @@ func TestTemplateRequiredVariable(t *testing.T) { Type: "query", Label: "bar", }, + { + Name: "instance", + Datasource: "$datasource", + Type: "query", + Label: "bar", + }, }, }, }, diff --git a/lint/rule_uneditable.go b/lint/rule_uneditable.go index deb469a..a5ccbe4 100644 --- a/lint/rule_uneditable.go +++ b/lint/rule_uneditable.go @@ -2,7 +2,7 @@ package lint func NewUneditableRule() *DashboardRuleFunc { return &DashboardRuleFunc{ - name: "uneditable-dashboard", + name: "uneditable-dashboard-rule", description: "Checks that the dashboard is not editable.", stability: "stable", fn: func(d Dashboard) DashboardRuleResults { From 4ed59dceae69f320c123724a334ddc02818e6f75 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:37:48 +0200 Subject: [PATCH 08/26] Add/Update docs --- Makefile | 2 +- docs/index.md | 42 ++++++++++++------- docs/rules/target-required-matchers-rule.md | 11 +++++ .../rules/template-required-variables-rule.md | 22 ++++++++++ 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 docs/rules/target-required-matchers-rule.md create mode 100644 docs/rules/template-required-variables-rule.md diff --git a/Makefile b/Makefile index f9a54d4..f8595ba 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ intermediate-docs: @go run ./main.go -h > ./docs/_intermediate/help.txt @go run ./main.go completion -h > ./docs/_intermediate/completion.txt @go run ./main.go lint -h > ./docs/_intermediate/lint.txt - @go run ./main.go rules > ./docs/_intermediate/rules.txt + @go run ./main.go rules --experimental > ./docs/_intermediate/rules.txt @echo "Can't automate everything, please replace the #Rules section of index.md with the contents of ./docs/_intermediate/rules.txt" embedmd: diff --git a/docs/index.md b/docs/index.md index 53f9a62..dd94589 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,8 +52,10 @@ Usage: Flags: -c, --config string path to a configuration file + --experimental enable experimental rules --fix automatically fix problems if possible -h, --help help for lint + --stdin read from stdin --strict fail upon linting error or warning --verbose show more information about linting ``` @@ -62,20 +64,32 @@ Flags: The linter implements the following rules: -* [template-datasource-rule](./rules/template-datasource-rule.md) - Checks that the dashboard has a templated datasource. -* [template-job-rule](./rules/template-job-rule.md) - Checks that the dashboard has a templated job. -* [template-instance-rule](./rules/template-instance-rule.md) - Checks that the dashboard has a templated instance. -* [template-label-promql-rule](./rules/template-label-promql-rule.md) - Checks that the dashboard templated labels have proper PromQL expressions. -* [template-on-time-change-reload-rule](./rules/template-on-time-change-reload-rule.md) - Checks that the dashboard template variables are configured to reload on time change. -* [panel-datasource-rule](./rules/panel-datasource-rule.md) - Checks that each panel uses the templated datasource. -* [panel-title-description-rule](./rules/panel-title-description-rule.md) - Checks that each panel has a title and description. -* [panel-units-rule](./rules/panel-units-rule.md) - Checks that each panel uses has valid units defined. -* `panel-no-targets-rule` - Checks that each panel has at least one target. -* [target-promql-rule](./rules/target-promql-rule.md) - Checks that each target uses a valid PromQL query. -* [target-rate-interval-rule](./rules/target-rate-interval-rule.md) - Checks that each target uses $__rate_interval. -* [target-job-rule](./rules/target-job-rule.md) - Checks that every PromQL query has a job matcher. -* [target-instance-rule](./rules/target-instance-rule.md) - Checks that every PromQL query has a instance matcher. -* `target-counter-agg-rule` - Checks that any counter metric (ending in _total) is aggregated with rate, irate, or increase. +* [template-datasource-rule](./rules/template-datasource-rule) - ``stable`` - Checks that the dashboard has a templated datasource. +* [template-job-rule](./rules/template-job-rule) - `stable` - Checks that the dashboard has a templated job. +* [template-instance-rule](./rules/template-instance-rule) - `stable` - Checks that the dashboard has a templated instance. +* [template-label-promql-rule](./rules/template-label-promql-rule) - `stable` - Checks that the dashboard templated labels have proper PromQL expressions. +* [template-on-time-change-reload-rule](./rules/template-on-time-change-reload-rule) - `stable` - Checks that the dashboard template variables are configured to reload on time change. +* [panel-datasource-rule](./rules/panel-datasource-rule) - `stable` - Checks that each panel uses the templated datasource. +* [panel-title-description-rule](./rules/panel-title-description-rule) - `stable` - Checks that each panel has a title and description. +* [panel-units-rule](./rules/panel-units-rule) - `stable` - Checks that each panel uses has valid units defined. +* [panel-no-targets-rule](./rules/panel-no-targets-rule) - `stable` - Checks that each panel has at least one target. +* [target-promql-rule](./rules/target-promql-rule) - `stable` - Checks that each target uses a valid PromQL query. +* [target-rate-interval-rule](./rules/target-rate-interval-rule) - `stable` - Checks that each target uses $__rate_interval. +* [target-job-rule](./rules/target-job-rule) - `stable` - Checks that every PromQL query has a job matcher. +* [target-instance-rule](./rules/target-instance-rule) - `stable` - Checks that every PromQL query has a instance matcher. +* [target-counter-agg-rule](./rules/target-counter-agg-rule) - `stable` - Checks that any counter metric (ending in _total) is aggregated with rate, irate, or increase. +* [uneditable-dashboard-rule](./rules/uneditable-dashboard-rule) - `stable` - Checks that the dashboard is not editable. +* [target-required-matchers-rule](./rules/target-required-matchers-rule) - `experimental` - Checks that target PromQL query has the required matchers +* [template-required-variables-rule](./rules/template-required-variables-rule) - `experimental` - Checks that the dashboard has a template variable for required variables or matchers that use variables + +## Rule stability +- **Stable** rules have gone through testing and been widely adopted. + +- **Experimental** rules are for new and experimental features. + These rules are not enabled by default, but can be enabled by providing the `experimental` flag. + Allowing early adopters to gain confidence with new features. + +- **Deprecated** rules may be removed or replaced when they are marked as deprecated. ## Related Rules diff --git a/docs/rules/target-required-matchers-rule.md b/docs/rules/target-required-matchers-rule.md new file mode 100644 index 0000000..7c5289b --- /dev/null +++ b/docs/rules/target-required-matchers-rule.md @@ -0,0 +1,11 @@ +# target-required-matchers-rule +Checks that each PromQL query has a the matchers specified in rule settings. This rule is experimental and is designed to work with Prometheus datasources. + +## Rule Settings + +```yaml +settings: + target-required-matchers-rule: + matchers: + - cluster=~"$cluster" + - someLabel="someValue" diff --git a/docs/rules/template-required-variables-rule.md b/docs/rules/template-required-variables-rule.md new file mode 100644 index 0000000..7258cfa --- /dev/null +++ b/docs/rules/template-required-variables-rule.md @@ -0,0 +1,22 @@ +# template-required-variables-rule +Checks that each dashboard has a templated variable based on provided rule settings and detected variable usage for the target-required-matchers-rule. + +# Best Practice +The rule ensures all of the following conditions. + +* The dashboard template exists. +* The dashboard template is named `xxx`. +* The dashboard template is labeled `xxx`. +* The dashboard template uses a templated datasource, specifically named `$datasource`. +* The dashboard template uses a Prometheus query to find available matching instances. +* The dashboard template is multi select +* The dashboard template has an allValue of `.+` + +## Rule Settings + +```yaml +settings: + template-required-variables-rule: + variables: + - cluster + - namespace From b08d388363abe9a4846bb638d66fc5af73801d52 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:38:27 +0200 Subject: [PATCH 09/26] :construction: Add test for Grafana variables :construction: --- lint/rule_target_required_matchers_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lint/rule_target_required_matchers_test.go b/lint/rule_target_required_matchers_test.go index 69b6d73..8572896 100644 --- a/lint/rule_target_required_matchers_test.go +++ b/lint/rule_target_required_matchers_test.go @@ -85,6 +85,20 @@ func TestTargetRequiredMatcherRule(t *testing.T) { Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), }, }, + // Using Grafana global-variable + { + name: "autofix-reverse-expanded-variables", + result: Result{ + Severity: Fixed, + Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo[$__rate_interval]))': %s selector not found", "instance"), + }, + target: Target{ + Expr: `sum(rate(foo[$__rate_interval]))`, + }, + fixed: &Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[$__rate_interval]))`, "instance", "instance"), + }, + }, } { dashboard := Dashboard{ Title: "dashboard", From 845df8a0415af61cc8c60f69d2d5799c53ec49fd Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:38:19 +0200 Subject: [PATCH 10/26] Add legacy config example --- docs/rules/target-required-matchers-rule.md | 9 +++++++++ docs/rules/template-required-variables-rule.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/rules/target-required-matchers-rule.md b/docs/rules/target-required-matchers-rule.md index 7c5289b..51fd807 100644 --- a/docs/rules/target-required-matchers-rule.md +++ b/docs/rules/target-required-matchers-rule.md @@ -9,3 +9,12 @@ settings: matchers: - cluster=~"$cluster" - someLabel="someValue" +``` +Legacy config example for job and instance +```yaml +settings: + target-required-matchers-rule: + matchers: + - job + - instance +``` \ No newline at end of file diff --git a/docs/rules/template-required-variables-rule.md b/docs/rules/template-required-variables-rule.md index 7258cfa..6c05500 100644 --- a/docs/rules/template-required-variables-rule.md +++ b/docs/rules/template-required-variables-rule.md @@ -20,3 +20,12 @@ settings: variables: - cluster - namespace +``` +Legacy config example for job and instance +```yaml +settings: + template-required-variables-rule: + variables: + - job + - instance +``` \ No newline at end of file From 99ff76a9a935f1d8bf38b306d7e099eaec9f9a3f Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:51:21 +0200 Subject: [PATCH 11/26] Go mod tidy --- go.sum | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/go.sum b/go.sum index fb88f8a..03d19ad 100644 --- a/go.sum +++ b/go.sum @@ -444,17 +444,12 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -<<<<<<< HEAD golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -======= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= ->>>>>>> eb2bc3ba25e3f0ae816b45ed3d05700002f76871 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -499,17 +494,12 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -<<<<<<< HEAD golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -======= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= ->>>>>>> eb2bc3ba25e3f0ae816b45ed3d05700002f76871 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 7076e64737d9d1e2dfa6c65fc66163cecb1ecef0 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:52:29 +0200 Subject: [PATCH 12/26] Use const for stability --- lint/constants.go | 6 ++++++ lint/rule_panel_datasource.go | 2 +- lint/rule_panel_no_targets.go | 2 +- lint/rule_panel_title_description.go | 2 +- lint/rule_panel_units.go | 2 +- lint/rule_target_counter_agg.go | 2 +- lint/rule_target_job_instance.go | 2 +- lint/rule_target_promql.go | 2 +- lint/rule_target_rate_interval.go | 2 +- lint/rule_target_required_matchers.go | 2 +- lint/rule_template_datasource.go | 2 +- lint/rule_template_instance.go | 2 +- lint/rule_template_job.go | 2 +- lint/rule_template_label_promql.go | 2 +- lint/rule_template_on_time_change_reload.go | 2 +- lint/rule_template_required_variables.go | 2 +- lint/rule_uneditable.go | 2 +- lint/rules_test.go | 6 +++--- 18 files changed, 25 insertions(+), 19 deletions(-) diff --git a/lint/constants.go b/lint/constants.go index ea96b9c..da9678d 100644 --- a/lint/constants.go +++ b/lint/constants.go @@ -10,3 +10,9 @@ const ( panelTypeTimeSeries = "timeseries" panelTypeTimeTable = "table" ) + +const ( + ruleStabilityStable = "stable" + ruleStabilityExperimental = "experimental" + ruleStabilityDeprecated = "deprecated" +) diff --git a/lint/rule_panel_datasource.go b/lint/rule_panel_datasource.go index 401cc52..55d51a5 100644 --- a/lint/rule_panel_datasource.go +++ b/lint/rule_panel_datasource.go @@ -8,7 +8,7 @@ func NewPanelDatasourceRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-datasource-rule", description: "Checks that each panel uses the templated datasource.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} diff --git a/lint/rule_panel_no_targets.go b/lint/rule_panel_no_targets.go index 29ab6ed..dd512a8 100644 --- a/lint/rule_panel_no_targets.go +++ b/lint/rule_panel_no_targets.go @@ -4,7 +4,7 @@ func NewPanelNoTargetsRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-no-targets-rule", description: "Checks that each panel has at least one target.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_panel_title_description.go b/lint/rule_panel_title_description.go index 993af64..76c6e7c 100644 --- a/lint/rule_panel_title_description.go +++ b/lint/rule_panel_title_description.go @@ -6,7 +6,7 @@ func NewPanelTitleDescriptionRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-title-description-rule", description: "Checks that each panel has a title and description.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_panel_units.go b/lint/rule_panel_units.go index 5de00b5..220b264 100644 --- a/lint/rule_panel_units.go +++ b/lint/rule_panel_units.go @@ -66,7 +66,7 @@ func NewPanelUnitsRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-units-rule", description: "Checks that each panel uses has valid units defined.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_target_counter_agg.go b/lint/rule_target_counter_agg.go index 87dfe61..9f9b065 100644 --- a/lint/rule_target_counter_agg.go +++ b/lint/rule_target_counter_agg.go @@ -11,7 +11,7 @@ func NewTargetCounterAggRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-counter-agg-rule", description: "Checks that any counter metric (ending in _total) is aggregated with rate, irate, or increase.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} expr, err := parsePromQL(t.Expr, d.Templating.List) diff --git a/lint/rule_target_job_instance.go b/lint/rule_target_job_instance.go index 1f2f322..4e52572 100644 --- a/lint/rule_target_job_instance.go +++ b/lint/rule_target_job_instance.go @@ -11,7 +11,7 @@ func newTargetRequiredMatcherRule(matcher string) *TargetRuleFunc { return &TargetRuleFunc{ name: fmt.Sprintf("target-%s-rule", matcher), description: fmt.Sprintf("Checks that every PromQL query has a %s matcher.", matcher), - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} // TODO: The RuleSet should be responsible for routing rule checks based on their query type (prometheus, loki, mysql, etc) diff --git a/lint/rule_target_promql.go b/lint/rule_target_promql.go index af00ff5..1e13d23 100644 --- a/lint/rule_target_promql.go +++ b/lint/rule_target_promql.go @@ -39,7 +39,7 @@ func NewTargetPromQLRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-promql-rule", description: "Checks that each target uses a valid PromQL query.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} diff --git a/lint/rule_target_rate_interval.go b/lint/rule_target_rate_interval.go index cf65238..c3af0b9 100644 --- a/lint/rule_target_rate_interval.go +++ b/lint/rule_target_rate_interval.go @@ -27,7 +27,7 @@ func NewTargetRateIntervalRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-rate-interval-rule", description: "Checks that each target uses $__rate_interval.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} if t := getTemplateDatasource(d); t == nil || t.Query != Prometheus { diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go index c773f62..9ccb85a 100644 --- a/lint/rule_target_required_matchers.go +++ b/lint/rule_target_required_matchers.go @@ -16,7 +16,7 @@ func NewTargetRequiredMatchersRule(config *TargetRequiredMatchersRuleSettings) * return &TargetRuleFunc{ name: "target-required-matchers-rule", description: "Checks that target PromQL query has the required matchers", - stability: "experimental", + stability: ruleStabilityExperimental, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} // TODO: The RuleSet should be responsible for routing rule checks based on their query type (prometheus, loki, mysql, etc) diff --git a/lint/rule_template_datasource.go b/lint/rule_template_datasource.go index 00f50ed..d3df644 100644 --- a/lint/rule_template_datasource.go +++ b/lint/rule_template_datasource.go @@ -12,7 +12,7 @@ func NewTemplateDatasourceRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-datasource-rule", description: "Checks that the dashboard has a templated datasource.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_instance.go b/lint/rule_template_instance.go index a8f1701..3b2df2d 100644 --- a/lint/rule_template_instance.go +++ b/lint/rule_template_instance.go @@ -4,7 +4,7 @@ func NewTemplateInstanceRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-instance-rule", description: "Checks that the dashboard has a templated instance.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_job.go b/lint/rule_template_job.go index 74be18d..f034ef4 100644 --- a/lint/rule_template_job.go +++ b/lint/rule_template_job.go @@ -4,7 +4,7 @@ func NewTemplateJobRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-job-rule", description: "Checks that the dashboard has a templated job.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_label_promql.go b/lint/rule_template_label_promql.go index 4576b1b..4ec0c13 100644 --- a/lint/rule_template_label_promql.go +++ b/lint/rule_template_label_promql.go @@ -43,7 +43,7 @@ func NewTemplateLabelPromQLRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-label-promql-rule", description: "Checks that the dashboard templated labels have proper PromQL expressions.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_on_time_change_reload.go b/lint/rule_template_on_time_change_reload.go index 6bf5772..75c6beb 100644 --- a/lint/rule_template_on_time_change_reload.go +++ b/lint/rule_template_on_time_change_reload.go @@ -8,7 +8,7 @@ func NewTemplateOnTimeRangeReloadRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-on-time-change-reload-rule", description: "Checks that the dashboard template variables are configured to reload on time change.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_required_variables.go b/lint/rule_template_required_variables.go index 31827b1..d7913c5 100644 --- a/lint/rule_template_required_variables.go +++ b/lint/rule_template_required_variables.go @@ -12,7 +12,7 @@ func NewTemplateRequiredVariablesRule(config *TemplateRequiredVariablesRuleSetti return &DashboardRuleFunc{ name: "template-required-variables-rule", description: "Checks that the dashboard has a template variable for required variables or matchers that use variables", - stability: "experimental", + stability: ruleStabilityExperimental, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_uneditable.go b/lint/rule_uneditable.go index a5ccbe4..c1d6194 100644 --- a/lint/rule_uneditable.go +++ b/lint/rule_uneditable.go @@ -4,7 +4,7 @@ func NewUneditableRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "uneditable-dashboard-rule", description: "Checks that the dashboard is not editable.", - stability: "stable", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} if d.Editable { diff --git a/lint/rules_test.go b/lint/rules_test.go index 2a9509d..98a175b 100644 --- a/lint/rules_test.go +++ b/lint/rules_test.go @@ -19,7 +19,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of dashboard rule", rule: lint.NewDashboardRuleFunc( - "test-dashboard-rule", "Test dashboard rule", "stable", + "test-dashboard-rule", "Test dashboard rule", ruleStabilityStable, func(lint.Dashboard) lint.DashboardRuleResults { return lint.DashboardRuleResults{Results: []lint.DashboardResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -30,7 +30,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of panel rule", rule: lint.NewPanelRuleFunc( - "test-panel-rule", "Test panel rule", "stable", + "test-panel-rule", "Test panel rule", ruleStabilityStable, func(d lint.Dashboard, p lint.Panel) lint.PanelRuleResults { return lint.PanelRuleResults{Results: []lint.PanelResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -41,7 +41,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of target rule", rule: lint.NewTargetRuleFunc( - "test-target-rule", "Test target rule", "stable", + "test-target-rule", "Test target rule", ruleStabilityStable, func(lint.Dashboard, lint.Panel, lint.Target) lint.TargetRuleResults { return lint.TargetRuleResults{Results: []lint.TargetResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, From f71ba507a7130e4c0e03a475f9b169fbe5389fc6 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:56:26 +0200 Subject: [PATCH 13/26] revert usage of const in test --- lint/rules_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lint/rules_test.go b/lint/rules_test.go index 98a175b..2a9509d 100644 --- a/lint/rules_test.go +++ b/lint/rules_test.go @@ -19,7 +19,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of dashboard rule", rule: lint.NewDashboardRuleFunc( - "test-dashboard-rule", "Test dashboard rule", ruleStabilityStable, + "test-dashboard-rule", "Test dashboard rule", "stable", func(lint.Dashboard) lint.DashboardRuleResults { return lint.DashboardRuleResults{Results: []lint.DashboardResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -30,7 +30,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of panel rule", rule: lint.NewPanelRuleFunc( - "test-panel-rule", "Test panel rule", ruleStabilityStable, + "test-panel-rule", "Test panel rule", "stable", func(d lint.Dashboard, p lint.Panel) lint.PanelRuleResults { return lint.PanelRuleResults{Results: []lint.PanelResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -41,7 +41,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of target rule", rule: lint.NewTargetRuleFunc( - "test-target-rule", "Test target rule", ruleStabilityStable, + "test-target-rule", "Test target rule", "stable", func(lint.Dashboard, lint.Panel, lint.Target) lint.TargetRuleResults { return lint.TargetRuleResults{Results: []lint.TargetResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, From 287dffd161f282379bd0179c724f3aa77d9481de Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:17:42 +0200 Subject: [PATCH 14/26] :construction: initial rework of variable expansion to support reversal :construction: --- lint/rule_target_promql_test.go | 11 +- lint/rule_target_rate_interval.go | 13 +- lint/rule_target_rate_interval_test.go | 1 - lint/variables.go | 493 ++++++++++++++++--------- lint/variables_test.go | 299 +++++++++++++-- 5 files changed, 591 insertions(+), 226 deletions(-) diff --git a/lint/rule_target_promql_test.go b/lint/rule_target_promql_test.go index 90554f8..00208ca 100644 --- a/lint/rule_target_promql_test.go +++ b/lint/rule_target_promql_test.go @@ -54,7 +54,7 @@ func TestTargetPromQLRule(t *testing.T) { { result: []Result{{ Severity: Error, - Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'foo(bar.baz)': 1:8: parse error: unexpected character: '.'", + Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'foo(bar.baz)': could not expand variables: failed to parse expression: foo(bar.baz)", }}, panel: Panel{ Title: "panel", @@ -119,7 +119,10 @@ func TestTargetPromQLRule(t *testing.T) { }, }, { - result: []Result{ResultSuccess}, + result: []Result{{ + Severity: Error, + Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'increase(foo{}[$sampling])': could not expand variables: failed to parse expression: increase(foo{}[bgludgvy_sampling_0])", + }}, panel: Panel{ Title: "panel", Type: "singlestat", @@ -134,7 +137,7 @@ func TestTargetPromQLRule(t *testing.T) { { result: []Result{{ Severity: Error, - Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query '': unknown position: parse error: no expression found in input", + Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query '': could not expand variables: failed to parse expression: ", }}, panel: Panel{ Title: "panel", @@ -155,7 +158,7 @@ func TestTargetPromQLRule(t *testing.T) { }, { Severity: Error, - Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query '': unknown position: parse error: no expression found in input", + Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query '': could not expand variables: failed to parse expression: ", }, }, panel: Panel{ diff --git a/lint/rule_target_rate_interval.go b/lint/rule_target_rate_interval.go index c3af0b9..4a6e9b0 100644 --- a/lint/rule_target_rate_interval.go +++ b/lint/rule_target_rate_interval.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/prometheus/common/model" "github.com/prometheus/prometheus/promql/parser" ) @@ -19,11 +20,6 @@ func (f inspector) Visit(node parser.Node, path []parser.Node) (parser.Visitor, // NewTargetRateIntervalRule builds a lint rule for panels with Prometheus queries which checks // all range vector selectors use $__rate_interval. func NewTargetRateIntervalRule() *TargetRuleFunc { - rateIntervalMagicDuration, err := time.ParseDuration(globalVariables["__rate_interval"].(string)) - if err != nil { - // Will not happen - panic(err) - } return &TargetRuleFunc{ name: "target-rate-interval-rule", description: "Checks that each target uses $__rate_interval.", @@ -45,6 +41,11 @@ func NewTargetRateIntervalRule() *TargetRuleFunc { // Invalid PromQL is another rule return r } + rateIntervalMagicDuration, err := model.ParseDuration(placeholderByVariable["$__rate_interval"].value) + if err != nil { + // Will not happen + panic(err) + } err = parser.Walk(inspector(func(node parser.Node, parents []parser.Node) error { selector, ok := node.(*parser.MatrixSelector) if !ok { @@ -52,7 +53,7 @@ func NewTargetRateIntervalRule() *TargetRuleFunc { return nil } - if selector.Range == rateIntervalMagicDuration { + if selector.Range == time.Duration(rateIntervalMagicDuration) { // Range vector selector is $__rate_interval return nil } diff --git a/lint/rule_target_rate_interval_test.go b/lint/rule_target_rate_interval_test.go index b7f84b6..bf0d5c1 100644 --- a/lint/rule_target_rate_interval_test.go +++ b/lint/rule_target_rate_interval_test.go @@ -6,7 +6,6 @@ import ( func TestTargetRateIntervalRule(t *testing.T) { linter := NewTargetRateIntervalRule() - for _, tc := range []struct { result Result panel Panel diff --git a/lint/variables.go b/lint/variables.go index 9effdc7..aff6287 100644 --- a/lint/variables.go +++ b/lint/variables.go @@ -1,215 +1,356 @@ package lint import ( - "encoding/json" "fmt" - "net/url" "regexp" "strconv" "strings" "time" + + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/promql/parser" +) + +const ( + rateInterval = "__rate_interval" + interval = "__interval" + intervalMs = "__interval_ms" + rangeMs = "__range_ms" + rangeS = "__range_s" + rangeVar = "__range" + dashboard = "__dashboard" + from = "__from" + to = "__to" + name = "__name" + org = "__org" + orgName = "__org.name" + userID = "__user.id" + userLogin = "__user.login" + userEmail = "__user.email" + timeFilter = "timeFilter" + timeFilter2 = "__timeFilter" + magicTimeRange = model.Duration(time.Hour*24*211 + time.Hour*12 + time.Minute*44 + time.Second*22 + time.Millisecond*50) // 211d12h44m22s50ms + magicEpoch = float64(1294671549254) + magicString = "bgludgvy" +) + +const ( + valTypeString valType = iota + valTypeTimeRange + valTypeEpoch ) -// https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/ -var globalVariables = map[string]interface{}{ - "__rate_interval": "8869990787ms", - "__interval": "4867856611ms", - "__interval_ms": "7781188786", - "__range_ms": "6737667980", - "__range_s": "9397795485", - "__range": "6069770749ms", - "__dashboard": "AwREbnft", - "__from": time.Date(2020, 7, 13, 20, 19, 9, 254000000, time.UTC), - "__to": time.Date(2020, 7, 13, 20, 19, 9, 254000000, time.UTC), - "__name": "name", - "__org": 42, - "__org.name": "orgname", - "__user.id": 42, - "__user.login": "user", - "__user.email": "user@test.com", - "timeFilter": "time > now() - 7d", - "__timeFilter": "time > now() - 7d", +type valType int + +type placeholder struct { + variable string // variable including the "variable syntax" i.e. $var, ${var}, [[var]] + valType valType + value string +} + +// placeholderByVariable key is the variable name, without the "variable syntax" i.e. var +var placeholderByVariable = make(map[string]*placeholder) +var placeholderByValue = make(map[string]*placeholder) + +var globalVariablesInit = false + +// list of global variables in the for om a list of placeholders +var globalVariables = []*placeholder{ + { + variable: rateInterval, + valType: valTypeTimeRange, + }, + { + variable: interval, + valType: valTypeTimeRange, + }, + { + variable: intervalMs, + valType: valTypeTimeRange, + }, + { + variable: rangeMs, + valType: valTypeTimeRange, + }, + { + variable: rangeS, + valType: valTypeTimeRange, + }, + { + variable: rangeVar, + valType: valTypeTimeRange, + }, + { + variable: dashboard, + valType: valTypeString, + }, + { + variable: from, + valType: valTypeEpoch, + }, + { + variable: to, + valType: valTypeEpoch, + }, + { + variable: name, + valType: valTypeString, + }, + { + variable: org, + valType: valTypeEpoch, // not really an epoch, but it is a float64 + }, + { + variable: orgName, + valType: valTypeString, + }, + { + variable: userID, + valType: valTypeEpoch, // not really an epoch, but it is a float64 + }, + { + variable: userLogin, + valType: valTypeString, + }, + { + variable: userEmail, + valType: valTypeString, + }, + { + variable: timeFilter, + valType: valTypeString, // not really a string, but currently we do only support prometheus queries, and this would not be a valid prometheus query... + }, + { + variable: timeFilter2, + valType: valTypeString, // not really a string, but currently we do only support prometheus queries, and this would not be a valid prometheus query... + }, } -func stringValue(name string, value interface{}, kind, format string) (string, error) { - switch val := value.(type) { - case int: - return strconv.Itoa(val), nil - case time.Time: - // Implements https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to - switch kind { - case "date": - switch format { - case "seconds": - return strconv.FormatInt(val.Unix(), 10), nil - case "iso": - return val.Format(time.RFC3339), nil - default: - return "", fmt.Errorf("Unsupported momentjs time format: " + format) +// var supportedFormatOptions = []string{"csv", "distributed", "doublequote", "glob", "json", "lucene", "percentencode", "pipe", "raw", "regex", "singlequote", "sqlstring", "text", "queryparam"} + +var variableRegexp = regexp.MustCompile( + strings.Join([]string{ + `("\$|\$)([[:word:]]+)`, // $var syntax + `("\$|\$)\{([^}]+)\}`, // ${var} syntax + `\[\[([^\[\]]+)\]\]`, // [[var]] syntax + }, "|"), +) + +func expandVariables(expr string, variables []Template) (string, error) { + // initialize global variables if not already initialized + if !globalVariablesInit { + for _, v := range globalVariables { + // assign placeholder to global variable 3 times to account for the 3 different ways a variable can be defined + // $var, ${var}, [[var]] + p := []placeholder{ + { + variable: fmt.Sprintf("$%s", v.variable), + valType: v.valType, + }, + { + variable: fmt.Sprintf("${%s}", v.variable), + valType: v.valType, + }, + { + variable: fmt.Sprintf("[[%s]]", v.variable), + valType: v.valType, + }, } - default: - switch format { - case "date": - return val.Format(time.RFC3339), nil - default: - return strconv.FormatInt(val.UnixMilli(), 10), nil + for _, v := range p { + createPlaceholder(v.variable, v.valType) } } - default: - // Use variable name as sample value - svalue := fmt.Sprintf("%s", value) - // For list types, repeat it 3 times (arbitrary value) - svalueList := []string{svalue, svalue, svalue} - // Implements https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options/ - switch format { - case "csv": - return strings.Join(svalueList, ","), nil - case "doublequote": - return "\"" + strings.Join(svalueList, "\",\"") + "\"", nil - case "glob": - return "{" + strings.Join(svalueList, ",") + "}", nil - case "json": - data, err := json.Marshal(svalueList) - if err != nil { - return "", err + globalVariablesInit = true + } + // add template variables to placeholder maps + for _, v := range variables { + if v.Name != "" { + // create placeholder 3 times to account for the 3 different ways a variable can be defined + // at this point, we do not care about the value of the variable, we just need a placeholder for it. + valType := getValueType(getTemplateVariableValue(v)) + createPlaceholder(fmt.Sprintf("$%s", v.Name), valType) + createPlaceholder(fmt.Sprintf("${%s}", v.Name), valType) + createPlaceholder(fmt.Sprintf("[[%s]]", v.Name), valType) + } + } + + expr = variableRegexp.ReplaceAllStringFunc(expr, RegexpExpandVariables) + + // Check if the expression can be parsed + _, err := parser.ParseExpr(expr) + if err != nil { + // not using promql parser error since it contains memory address which is hard to test... + return "", fmt.Errorf("failed to parse expression: %s", expr) + } + + return expr, nil +} + +func revertExpandedVariables(expr string) string { + for _, p := range placeholderByValue { + expr = strings.ReplaceAll(expr, p.value, p.variable) + } + return expr +} + +// Should not replace variables inside double quotes +func RegexpExpandVariables(s string) string { + // check if string starts with a double quote + if s[0:1] == `"` { + return s + } + + if strings.Contains(s, ":") { + // check if variable is __from or __to with advanced formatting + if strings.HasPrefix(trimVariableSyntax(s), from) || strings.HasPrefix(trimVariableSyntax(s), to) { + if strings.Count(s, ":") > 2 { + // Should not replace variables with more than 2 colons returning the original string, promql parser will handle the error. + return s } - return string(data), nil - case "lucene": - return "(\"" + strings.Join(svalueList, "\" OR \"") + "\")", nil - case "percentencode": - return url.QueryEscape(strings.Join(svalueList, ",")), nil - case "pipe": - return strings.Join(svalueList, "|"), nil - case "raw": - return strings.Join(svalueList, ","), nil - case "regex": - return strings.Join(svalueList, "|"), nil - case "singlequote": - return "'" + strings.Join(svalueList, "','") + "'", nil - case "sqlstring": - return "'" + strings.Join(svalueList, "','") + "'", nil - case "text": - return strings.Join(svalueList, " + "), nil - case "queryparam": - values := url.Values{} - for _, svalue := range svalueList { - values.Add("var-"+name, svalue) + return createPlaceholder(s, valTypeEpoch) + } + // check if variable contains more than 1 colon + if strings.Count(s, ":") > 1 { + // Should not replace variables with more than 1 colon returning the original string, promql parser will handle the error. + return s + } + } + return createPlaceholder(s, valTypeString) +} + +// getPlaceholder returns placeholder for a provided variable or value +func getPlaceholder(variable string, value string) *placeholder { + switch { + case variable != "" && value != "": + if p, ok := placeholderByVariable[variable]; ok { + if p.value == value { + return p } - return values.Encode(), nil - default: - return svalue, nil + } + case variable != "": + if p, ok := placeholderByVariable[variable]; ok { + return p + } + case value != "": + if p, ok := placeholderByValue[value]; ok { + return p } } + return nil } -func removeVariableByName(name string, variables []Template) []Template { - vars := make([]Template, 0, len(variables)) - for _, v := range variables { - if v.Name == name { - continue +// assignPlaceholder assigns a placeholder to a variable it ensures both placeholderByVariable and placeholderByValue are updated +func assignPlaceholder(placeholder placeholder) error { + if placeholder.variable == "" || placeholder.value == "" { + return fmt.Errorf("variable and value must not be empty") + } + // Check if variable and value combination already exists + if getPlaceholder(placeholder.variable, placeholder.value) != nil { + return nil + } + // check if value already exists but with a different variable + p := getPlaceholder("", placeholder.value) + if p != nil { + if p.variable != placeholder.variable { + return fmt.Errorf("value %s already assigned to variable %s", placeholder.value, p.variable) + } + } + // check if variable already exists but with a different value + p = getPlaceholder(placeholder.variable, "") + if p != nil { + if p.value != placeholder.value { + return fmt.Errorf("variable %s already assigned to value %s", placeholder.variable, p.value) } - vars = append(vars, v) } - return vars + // add placeholder to placeholderByVariable + placeholderByVariable[placeholder.variable] = &placeholder + // add placeholder to placeholderByValue + placeholderByValue[placeholder.value] = &placeholder + return nil } -func variableSampleValue(s string, variables []Template) (string, error) { - var name, kind, format string - parts := strings.Split(s, ":") - switch len(parts) { - case 1: - // No format - name = s - case 2: - // Could be __from:date, variable:csv, ... - name = parts[0] - format = parts[1] - case 3: - // Could be __from:date:iso, ... - name = parts[0] - kind = parts[1] - format = parts[2] - default: - return "", fmt.Errorf("unknown variable format: %s", s) - } - // If it is part of the globals, return a string representation of a sample value - if value, ok := globalVariables[name]; ok { - return stringValue(name, value, kind, format) - } - // If it is an auto interval variable, replace with a sample value of 10s - if strings.HasPrefix(name, "__auto_interval") { - return "10s", nil - } - // If it is a template variable and we have a value, we use it - for _, v := range variables { - if v.Name != name { - continue +func getValueType(value string) valType { + // check if variable is time range + if _, err := model.ParseDuration(value); err == nil { + return valTypeTimeRange + } + // check if variable is epoch, this is used for promql @ modifier + if _, err := strconv.ParseFloat(value, 64); err == nil { + return valTypeEpoch + } + return valTypeString +} + +// createPlaceholder returns a placeholder for a variable. +func createPlaceholder(variable string, valType valType) string { + // check if variable already has a placeholder + if p := getPlaceholder(variable, ""); p != nil { + return p.value + } + // create placeholder + counter := 0 + var value string + for { + if valType == valTypeTimeRange { + // Using magicTimeRange as a seed for the placeholder + timeRange := magicTimeRange + model.Duration(time.Millisecond*time.Duration(counter)) + value = timeRange.String() } - // if it has a current value, use it - c, err := v.Current.Get() - if err != nil { - return "", err + if valType == valTypeEpoch { + // Using magicEpoch as a seed for the placeholder + epoch := magicEpoch + float64(counter) + // trim epoch to 3 decimal places since that is the precision used in prometheus + value = fmt.Sprintf("%.3f", epoch) } - if c.Value != "" { - // Recursively expand, without the current variable to avoid infinite recursion - return expandVariables(c.Value, removeVariableByName(name, variables)) + if valType == valTypeString { + value = fmt.Sprintf("%s_%s_%d", magicString, trimVariableSyntax(variable), counter) } - // If it has options, use the first option - if len(v.Options) > 0 { - // Recursively expand, without the current variable to avoid infinite recursion - o, err := v.Options[0].Get() - if err != nil { - return "", err + + if _, ok := placeholderByValue[value]; !ok { + err := assignPlaceholder(placeholder{variable: variable, valType: valType, value: value}) + if err == nil { + return value } - return expandVariables(o.Value, removeVariableByName(name, variables)) + } + counter++ + if counter > 10000 { + // this should never happen... but just in case... lets panic... + panic("createPlaceholder: counter > 10000 - this should never happen :(") } } - // Assume variable type is a string - return stringValue(name, name, kind, format) } -var variableRegexp = regexp.MustCompile( - strings.Join([]string{ - `\$([[:word:]]+)`, // $var syntax - `\$\{([^}]+)\}`, // ${var} syntax - `\[\[([^\[\]]+)\]\]`, // [[var]] syntax - }, "|"), -) +// Helper func to remove the variable syntax from a string +func trimVariableSyntax(s string) string { + s = strings.TrimPrefix(s, "[[") + s = strings.TrimPrefix(s, "${") + s = strings.TrimPrefix(s, "$") -func expandVariables(expr string, variables []Template) (string, error) { - parts := strings.Split(expr, "\"") - for i, part := range parts { - if i%2 == 1 { - // Inside a double quote string, just add it - continue - } + s = strings.TrimSuffix(s, "]]") + s = strings.TrimSuffix(s, "}") + + // replace all ":" with "_" + s = strings.ReplaceAll(s, ":", "_") - // Accumulator to store the processed submatches - var subparts []string - // Cursor indicates where we are in the part being processed - cursor := 0 - for _, v := range variableRegexp.FindAllStringSubmatchIndex(part, -1) { - // Add all until match starts - subparts = append(subparts, part[cursor:v[0]]) - // Iterate on all the subgroups and find the one that matched - for j := 2; j < len(v); j += 2 { - if v[j] < 0 { - continue - } - // Replace the match with sample value - val, err := variableSampleValue(part[v[j]:v[j+1]], variables) - if err != nil { - return "", err - } - subparts = append(subparts, val) + return s +} + +// Helper func to get the value of a template variable +func getTemplateVariableValue(v Template) string { + // do not handle error + c, _ := v.Current.Get() + // check if variable has a value + if c.Value == "" { + if len(v.Options) > 0 { + // Do not handle error + o, _ := v.Options[0].Get() + if o.Value != "" { + return o.Value } - // Move the start cursor at the end of the current match - cursor = v[1] } - // Add rest of the string - subparts = append(subparts, parts[i][cursor:]) - // Merge all back into the parts - parts[i] = strings.Join(subparts, "") + // v.Current.Value is empty and no options are provided return empty string + return "" + // Helper func to check if a format option is supported } - return strings.Join(parts, "\""), nil + return c.Value } diff --git a/lint/variables_test.go b/lint/variables_test.go index 1f0e080..f20fe68 100644 --- a/lint/variables_test.go +++ b/lint/variables_test.go @@ -20,128 +20,128 @@ func TestVariableExpansion(t *testing.T) { expr: "up{job=~\"$job\"}", result: "up{job=~\"$job\"}", }, - // https://grafana.com/docs/grafana/latest/variables/syntax/ + // https: //grafana.com/docs/grafana/latest/variables/syntax/ { desc: "Should replace variables in metric name", expr: "up$var{job=~\"$job\"}", - result: "upvar{job=~\"$job\"}", + result: "upbgludgvy_var_0{job=~\"$job\"}", }, { desc: "Should replace global rate/range variables", expr: "rate(metric{}[$__rate_interval])", - result: "rate(metric{}[8869990787ms])", + result: "rate(metric{}[211d12h44m22s50ms])", }, { desc: "Should support ${...} syntax", expr: "rate(metric{}[${__rate_interval}])", - result: "rate(metric{}[8869990787ms])", + result: "rate(metric{}[211d12h44m22s51ms])", }, { desc: "Should support [[...]] syntax", expr: "rate(metric{}[[[__rate_interval]]])", - result: "rate(metric{}[8869990787ms])", + result: "rate(metric{}[211d12h44m22s52ms])", }, // https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/ { desc: "Should support ${__user.id}", expr: "sum(http_requests_total{method=\"GET\"} @ ${__user.id})", - result: "sum(http_requests_total{method=\"GET\"} @ 42)", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549264.000)", }, { desc: "Should support $__from/$__to", expr: "sum(http_requests_total{method=\"GET\"} @ $__from)", - result: "sum(http_requests_total{method=\"GET\"} @ 1594671549254)", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549254.000)", }, { desc: "Should support $__from/$__to with formatting option (unix seconds)", - expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:seconds}000)", - result: "sum(http_requests_total{method=\"GET\"} @ 1594671549000)", + expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:seconds})", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549266.000)", }, { desc: "Should support $__from/$__to with formatting option (iso default)", expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date})", - result: "sum(http_requests_total{method=\"GET\"} @ 2020-07-13T20:19:09Z)", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549267.000)", }, { desc: "Should support $__from/$__to with formatting option (iso)", expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:iso})", - result: "sum(http_requests_total{method=\"GET\"} @ 2020-07-13T20:19:09Z)", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549268.000)", }, { - desc: "Should not support $__from/$__to with momentjs formatting option (iso)", - expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:YYYY-MM})", - err: fmt.Errorf("Unsupported momentjs time format: YYYY-MM"), + desc: "Should not support $__from/$__to with momentjs formatting option (iso)", + expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:YYYY-MM})", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549269.000)", }, // https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options/ { desc: "Should support ${variable:csv} syntax", expr: "max by(${variable:csv}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable,variable,variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_csv_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:doublequote} syntax", expr: "max by(${variable:doublequote}) (rate(cpu{}[$__rate_interval]))", - result: "max by(\"variable\",\"variable\",\"variable\") (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_doublequote_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:glob} syntax", expr: "max by(${variable:glob}) (rate(cpu{}[$__rate_interval]))", - result: "max by({variable,variable,variable}) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_glob_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:json} syntax", expr: "max by(${variable:json}) (rate(cpu{}[$__rate_interval]))", - result: "max by([\"variable\",\"variable\",\"variable\"]) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_json_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:lucene} syntax", expr: "max by(${variable:lucene}) (rate(cpu{}[$__rate_interval]))", - result: "max by((\"variable\" OR \"variable\" OR \"variable\")) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_lucene_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:percentencode} syntax", expr: "max by(${variable:percentencode}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable%2Cvariable%2Cvariable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_percentencode_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:pipe} syntax", expr: "max by(${variable:pipe}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable|variable|variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_pipe_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:raw} syntax", expr: "max by(${variable:raw}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable,variable,variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_raw_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:regex} syntax", expr: "max by(${variable:regex}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable|variable|variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_regex_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:singlequote} syntax", expr: "max by(${variable:singlequote}) (rate(cpu{}[$__rate_interval]))", - result: "max by('variable','variable','variable') (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_singlequote_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:sqlstring} syntax", expr: "max by(${variable:sqlstring}) (rate(cpu{}[$__rate_interval]))", - result: "max by('variable','variable','variable') (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_sqlstring_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:text} syntax", expr: "max by(${variable:text}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable + variable + variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_text_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should support ${variable:queryparam} syntax", expr: "max by(${variable:queryparam}) (rate(cpu{}[$__rate_interval]))", - result: "max by(var-variable=variable&var-variable=variable&var-variable=variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_queryparam_0) (rate(cpu{}[211d12h44m22s50ms]))", }, { desc: "Should return an error for unknown syntax", expr: "max by(${a:b:c:d}) (rate(cpu{}[$__rate_interval]))", - err: fmt.Errorf("unknown variable format: a:b:c:d"), + err: fmt.Errorf("failed to parse expression: max by(${a:b:c:d}) (rate(cpu{}[211d12h44m22s50ms]))"), }, { desc: "Should replace variables present in the templating", @@ -169,7 +169,7 @@ func TestVariableExpansion(t *testing.T) { "value": "value", }}, }, - result: "max by(value) (rate(cpu{}[4h:5m]))", + result: "max by(bgludgvy_var_0) (rate(cpu{}[211d12h44m22s68ms:211d12h44m22s71ms]))", }, { desc: "Should recursively replace variables", @@ -177,7 +177,7 @@ func TestVariableExpansion(t *testing.T) { variables: []Template{ {Name: "interval", Current: map[string]interface{}{"value": "$__auto_interval_interval"}}, }, - result: "sum (rate(cpu{}[10s]))", + result: "sum (rate(cpu{}[211d12h44m22s68ms]))", }, { desc: "Should support plain $__auto_interval, generated by grafonnet-lib (https://github.com/grafana/grafonnet-lib/blob/master/grafonnet/template.libsonnet#L100)", @@ -185,15 +185,7 @@ func TestVariableExpansion(t *testing.T) { variables: []Template{ {Name: "interval", Current: map[string]interface{}{"value": "$__auto_interval"}}, }, - result: "sum (rate(cpu{}[10s]))", - }, - { - desc: "Should recursively replace variables, but not run into an infinite loop", - expr: "sum (rate(cpu{}[$interval]))", - variables: []Template{ - {Name: "interval", Current: map[string]interface{}{"value": "$interval"}}, - }, - result: "sum (rate(cpu{}[interval]))", + result: "sum (rate(cpu{}[211d12h44m22s68ms]))", }, } { s, err := expandVariables(tc.expr, tc.variables) @@ -201,3 +193,232 @@ func TestVariableExpansion(t *testing.T) { require.Equal(t, tc.result, s, tc.desc) } } + +func TestReverseVariableExpansion(t *testing.T) { + placeholderByValue = map[string]*placeholder{ + "bgludgvy_variable_queryparam_0": {variable: "${variable:queryparam}", value: "bgludgvy_variable_queryparam_0"}, + "211d12h44m22s63ms": {variable: "${__range_s}", value: "211d12h44m22s63ms"}, + "211d12h44m22s66ms": {variable: "${__range}", value: "211d12h44m22s66ms"}, + "1294671549257.000": {variable: "$__to", value: "1294671549257.000"}, + "bgludgvy___name_0": {variable: "$__name", value: "bgludgvy___name_0"}, + "bgludgvy_variable_regex_0": {variable: "${variable:regex}", value: "bgludgvy_variable_regex_0"}, + "bgludgvy_variable_glob_0": {variable: "${variable:glob}", value: "bgludgvy_variable_glob_0"}, + "211d12h44m22s52ms": {variable: "[[__rate_interval]]", value: "211d12h44m22s52ms"}, + "211d12h44m22s56ms": {variable: "$__interval_ms", value: "211d12h44m22s56ms"}, + "bgludgvy___dashboard_2": {variable: "[[__dashboard]]", value: "bgludgvy___dashboard_2"}, + "1294671549256.000": {variable: "[[__from]]", value: "1294671549256.000"}, + "bgludgvy___user.login_0": {variable: "$__user.login", value: "bgludgvy___user.login_0"}, + "bgludgvy_var_1": {variable: "${var}", value: "bgludgvy_var_1"}, + "bgludgvy_var_2": {variable: "[[var]]", value: "bgludgvy_var_2"}, + "bgludgvy_variable_sqlstring_0": {variable: "${variable:sqlstring}", value: "bgludgvy_variable_sqlstring_0"}, + "211d12h44m22s60ms": {variable: "${__range_ms}", value: "211d12h44m22s60ms"}, + "bgludgvy___org.name_2": {variable: "[[__org.name]]", value: "bgludgvy___org.name_2"}, + "bgludgvy_variable_csv_0": {variable: "${variable:csv}", value: "bgludgvy_variable_csv_0"}, + "bgludgvy_variable_lucene_0": {variable: "${variable:lucene}", value: "bgludgvy_variable_lucene_0"}, + "bgludgvy_variable_percentencode_0": {variable: "${variable:percentencode}", value: "bgludgvy_variable_percentencode_0"}, + "bgludgvy___user.email_1": {variable: "${__user.email}", value: "bgludgvy___user.email_1"}, + "211d12h44m22s53ms": {variable: "$__interval", value: "211d12h44m22s53ms"}, + "211d12h44m22s57ms": {variable: "${__interval_ms}", value: "211d12h44m22s57ms"}, + "211d12h44m22s58ms": {variable: "[[__interval_ms]]", value: "211d12h44m22s58ms"}, + "bgludgvy___dashboard_0": {variable: "$__dashboard", value: "bgludgvy___dashboard_0"}, + "1294671549265.000": {variable: "[[__user.id]]", value: "1294671549265.000"}, + "bgludgvy___timeFilter_0": {variable: "$__timeFilter", value: "bgludgvy___timeFilter_0"}, + "bgludgvy___timeFilter_1": {variable: "${__timeFilter}", value: "bgludgvy___timeFilter_1"}, + "bgludgvy___timeFilter_2": {variable: "[[__timeFilter]]", value: "bgludgvy___timeFilter_2"}, + "211d12h44m22s69ms": {variable: "${interval}", value: "211d12h44m22s69ms"}, + "1294671549268.000": {variable: "${__from:date:iso}", value: "1294671549268.000"}, + "211d12h44m22s54ms": {variable: "${__interval}", value: "211d12h44m22s54ms"}, + "211d12h44m22s64ms": {variable: "[[__range_s]]", value: "211d12h44m22s64ms"}, + "bgludgvy___name_2": {variable: "[[__name]]", value: "bgludgvy___name_2"}, + "bgludgvy___user.email_0": {variable: "$__user.email", value: "bgludgvy___user.email_0"}, + "bgludgvy___user.email_2": {variable: "[[__user.email]]", value: "bgludgvy___user.email_2"}, + "bgludgvy___dashboard_1": {variable: "${__dashboard}", value: "bgludgvy___dashboard_1"}, + "1294671549261.000": {variable: "${__org}", value: "1294671549261.000"}, + "1294671549264.000": {variable: "${__user.id}", value: "1294671549264.000"}, + "bgludgvy_variable_pipe_0": {variable: "${variable:pipe}", value: "bgludgvy_variable_pipe_0"}, + "bgludgvy_variable_raw_0": {variable: "${variable:raw}", value: "bgludgvy_variable_raw_0"}, + "211d12h44m22s50ms": {variable: "$__rate_interval", value: "211d12h44m22s50ms"}, + "211d12h44m22s62ms": {variable: "$__range_s", value: "211d12h44m22s62ms"}, + "1294671549259.000": {variable: "[[__to]]", value: "1294671549259.000"}, + "bgludgvy___org.name_0": {variable: "$__org.name", value: "bgludgvy___org.name_0"}, + "bgludgvy_timeFilter_1": {variable: "${timeFilter}", value: "bgludgvy_timeFilter_1"}, + "bgludgvy___user.login_2": {variable: "[[__user.login]]", value: "bgludgvy___user.login_2"}, + "1294671549267.000": {variable: "${__from:date}", value: "1294671549267.000"}, + "211d12h44m22s55ms": {variable: "[[__interval]]", value: "211d12h44m22s55ms"}, + "bgludgvy_var_0": {variable: "$var", value: "bgludgvy_var_0"}, + "1294671549269.000": {variable: "${__from:date:YYYY-MM}", value: "1294671549269.000"}, + "1294671549266.000": {variable: "${__from:date:seconds}", value: "1294671549266.000"}, + "bgludgvy_variable_singlequote_0": {variable: "${variable:singlequote}", value: "bgludgvy_variable_singlequote_0"}, + "211d12h44m22s51ms": {variable: "${__rate_interval}", value: "211d12h44m22s51ms"}, + "211d12h44m22s61ms": {variable: "[[__range_ms]]", value: "211d12h44m22s61ms"}, + "211d12h44m22s67ms": {variable: "[[__range]]", value: "211d12h44m22s67ms"}, + "1294671549258.000": {variable: "${__to}", value: "1294671549258.000"}, + "bgludgvy___user.login_1": {variable: "${__user.login}", value: "bgludgvy___user.login_1"}, + "211d12h44m22s70ms": {variable: "[[interval]]", value: "211d12h44m22s70ms"}, + "1294671549255.000": {variable: "${__from}", value: "1294671549255.000"}, + "bgludgvy_timeFilter_0": {variable: "$timeFilter", value: "bgludgvy_timeFilter_0"}, + "bgludgvy_timeFilter_2": {variable: "[[timeFilter]]", value: "bgludgvy_timeFilter_2"}, + "bgludgvy_variable_doublequote_0": {variable: "${variable:doublequote}", value: "bgludgvy_variable_doublequote_0"}, + "bgludgvy_variable_text_0": {variable: "${variable:text}", value: "bgludgvy_variable_text_0"}, + "bgludgvy___name_1": {variable: "${__name}", value: "bgludgvy___name_1"}, + "1294671549260.000": {variable: "$__org", value: "1294671549260.000"}, + "bgludgvy___org.name_1": {variable: "${__org.name}", value: "bgludgvy___org.name_1"}, + "211d12h44m22s59ms": {variable: "$__range_ms", value: "211d12h44m22s59ms"}, + "1294671549254.000": {variable: "$__from", value: "1294671549254.000"}, + "1294671549262.000": {variable: "[[__org]]", value: "1294671549262.000"}, + "211d12h44m22s68ms": {variable: "$interval", value: "211d12h44m22s68ms"}, + "211d12h44m22s72ms": {variable: "${resolution}", value: "211d12h44m22s72ms"}, + "211d12h44m22s65ms": {variable: "$__range", value: "211d12h44m22s65ms"}, + "1294671549263.000": {variable: "$__user.id", value: "1294671549263.000"}, + "bgludgvy_variable_json_0": {variable: "${variable:json}", value: "bgludgvy_variable_json_0"}, + "211d12h44m22s71ms": {variable: "$resolution", value: "211d12h44m22s71ms"}, + "211d12h44m22s73ms": {variable: "[[resolution]]", value: "211d12h44m22s73ms"}, + } + for _, tc := range []struct { + desc string + expr string + result string + }{ + { + desc: "Should not replace variables in quoted strings", + expr: "up{job=~\"$job\"}", + result: "up{job=~\"$job\"}", + }, + // https: //grafana.com/docs/grafana/latest/variables/syntax/ + { + desc: "Should replace variables in metric name", + expr: "upbgludgvy_var_0{job=~\"$job\"}", + result: "up$var{job=~\"$job\"}", + }, + { + desc: "Should replace global rate/range variables", + expr: "rate(metric{}[211d12h44m22s50ms])", + result: "rate(metric{}[$__rate_interval])", + }, + { + desc: "Should support ${...} syntax", + expr: "rate(metric{}[211d12h44m22s51ms])", + result: "rate(metric{}[${__rate_interval}])", + }, + { + desc: "Should support [[...]] syntax", + expr: "rate(metric{}[211d12h44m22s52ms])", + result: "rate(metric{}[[[__rate_interval]]])", + }, + // https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/ + { + desc: "Should support ${__user.id}", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549264.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__user.id})", + }, + { + desc: "Should support $__from/$__to", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549254.000)", + result: "sum(http_requests_total{method=\"GET\"} @ $__from)", + }, + { + desc: "Should support $__from/$__to with formatting option (unix seconds)", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549266.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:seconds})", + }, + { + desc: "Should support $__from/$__to with formatting option (iso default)", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549267.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__from:date})", + }, + { + desc: "Should support $__from/$__to with formatting option (iso)", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549268.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:iso})", + }, + { + desc: "Should not support $__from/$__to with momentjs formatting option (iso)", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549269.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:YYYY-MM})", + }, + // https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options/ + { + desc: "Should support ${variable:csv} syntax", + expr: "max by(bgludgvy_variable_csv_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:csv}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:doublequote} syntax", + expr: "max by(bgludgvy_variable_doublequote_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:doublequote}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:glob} syntax", + expr: "max by(bgludgvy_variable_glob_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:glob}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:json} syntax", + expr: "max by(bgludgvy_variable_json_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:json}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:lucene} syntax", + expr: "max by(bgludgvy_variable_lucene_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:lucene}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:percentencode} syntax", + expr: "max by(bgludgvy_variable_percentencode_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:percentencode}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:pipe} syntax", + expr: "max by(bgludgvy_variable_pipe_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:pipe}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:raw} syntax", + expr: "max by(bgludgvy_variable_raw_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:raw}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:regex} syntax", + expr: "max by(bgludgvy_variable_regex_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:regex}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:singlequote} syntax", + expr: "max by(bgludgvy_variable_singlequote_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:singlequote}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:sqlstring} syntax", + expr: "max by(bgludgvy_variable_sqlstring_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:sqlstring}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:text} syntax", + expr: "max by(bgludgvy_variable_text_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:text}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:queryparam} syntax", + expr: "max by(bgludgvy_variable_queryparam_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(${variable:queryparam}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should replace variables present in the templating", + expr: "max by(bgludgvy_var_0) (rate(cpu{}[211d12h44m22s68ms:211d12h44m22s71ms]))", + result: "max by($var) (rate(cpu{}[$interval:$resolution]))", + }, + { + desc: "Should recursively replace variables", + expr: "sum (rate(cpu{}[211d12h44m22s68ms]))", + result: "sum (rate(cpu{}[$interval]))", + }, + { + desc: "Should support plain $__auto_interval, generated by grafonnet-lib (https://github.com/grafana/grafonnet-lib/blob/master/grafonnet/template.libsonnet#L100)", + expr: "sum (rate(cpu{}[211d12h44m22s68ms]))", + result: "sum (rate(cpu{}[$interval]))", + }, + } { + s := revertExpandedVariables(tc.expr) + require.Equal(t, tc.result, s, tc.desc) + } +} From 15d2e818fe3e897279b95481545c6a007ca4a374 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:19:20 +0200 Subject: [PATCH 15/26] remove misleading comment --- lint/variables.go | 1 - 1 file changed, 1 deletion(-) diff --git a/lint/variables.go b/lint/variables.go index aff6287..37449d0 100644 --- a/lint/variables.go +++ b/lint/variables.go @@ -48,7 +48,6 @@ type placeholder struct { value string } -// placeholderByVariable key is the variable name, without the "variable syntax" i.e. var var placeholderByVariable = make(map[string]*placeholder) var placeholderByValue = make(map[string]*placeholder) From d2cc9d842f92fb0fbba18438840f392ece575f8e Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:16:39 +0200 Subject: [PATCH 16/26] :adhesive_bandage: - support different uses of variables --- go.mod | 53 ++++----- go.sum | 126 ++++++++++----------- lint/variables.go | 67 +++++++---- lint/variables_test.go | 246 +++++++++++++++++++++-------------------- 4 files changed, 262 insertions(+), 230 deletions(-) diff --git a/go.mod b/go.mod index 676ba53..046317f 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/grafana/dashboard-linter -go 1.21 +go 1.21.0 -toolchain go1.22.1 +toolchain go1.22.5 require ( - github.com/prometheus/prometheus v0.53.1 + github.com/prometheus/prometheus v0.54.1 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 @@ -15,20 +15,21 @@ require ( ) require ( - github.com/aws/aws-sdk-go v1.53.16 // indirect + github.com/aws/aws-sdk-go v1.54.19 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect ) require ( - cloud.google.com/go v0.114.0 // indirect - cloud.google.com/go/auth v0.5.1 // indirect + cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go/auth v0.7.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect - cloud.google.com/go/iam v1.1.8 // indirect - cloud.google.com/go/storage v1.40.0 // indirect + cloud.google.com/go/compute/metadata v0.4.0 // indirect + cloud.google.com/go/iam v1.1.10 // indirect + cloud.google.com/go/storage v1.41.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -39,14 +40,14 @@ require ( github.com/ghodss/yaml v1.0.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.4 // indirect + github.com/googleapis/gax-go/v2 v2.12.5 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -57,8 +58,8 @@ require ( github.com/prometheus/alertmanager v0.27.0 github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.54.0 // indirect - github.com/prometheus/procfs v0.13.0 // indirect + github.com/prometheus/common v0.55.0 + github.com/prometheus/procfs v0.15.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -71,25 +72,25 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect - go.opentelemetry.io/otel v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/api v0.183.0 // indirect - google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/grpc v1.64.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect + google.golang.org/api v0.188.0 // indirect + google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 03d19ad..f054b89 100644 --- a/go.sum +++ b/go.sum @@ -13,10 +13,10 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY= -cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E= -cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw= -cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts= +cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw= cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -25,12 +25,12 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c= +cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= -cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= +cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI= +cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -40,15 +40,15 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= -cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= +cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0= +cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 h1:FDif4R1+UUR+00q6wquyX90K7A8dN+R5E8GEadoP7sU= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2/go.mod h1:aiYBYui4BJ/BJCAIKs92XiPyQfTaBWqvHujDwKb6CBU= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 h1:sUFnFjzDUie80h24I7mrKtwCKgLY9L8h5Tp2x9+TWqk= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0/go.mod h1:52JbnQTp15qg5mRkMBHwp0j0ZFwHJ42Sx3zVV5RE9p0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -60,11 +60,11 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= -github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg= +github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.53.16 h1:8oZjKQO/ml1WLUZw5hvF7pvYjPf8o9f57Wldoy/q9Qc= -github.com/aws/aws-sdk-go v1.53.16/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go v1.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbVI= +github.com/aws/aws-sdk-go v1.54.19/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -113,8 +113,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -189,8 +189,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= -github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= +github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= +github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -214,8 +214,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -237,6 +237,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -270,18 +272,18 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= -github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= -github.com/prometheus/prometheus v0.53.1 h1:B0xu4VuVTKYrIuBMn/4YSUoIPYxs956qsOfcS4rqCuA= -github.com/prometheus/prometheus v0.53.1/go.mod h1:RZDkzs+ShMBDkAPQkLEaLBXpjmDcjhNxU2drUVPgKUU= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/prometheus v0.54.1 h1:vKuwQNjnYN2/mDoWfHXDhAsz/68q/dQDb+YbcEqU7MQ= +github.com/prometheus/prometheus v0.54.1/go.mod h1:xlLByHhk2g3ycakQGrMaU8K7OySZx98BzeCR99991NY= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -343,16 +345,16 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= -go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -365,8 +367,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -429,8 +431,8 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -486,8 +488,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -564,8 +566,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE= -google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ= +google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw= +google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -601,12 +603,12 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= -google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b h1:dSTjko30weBaMj3eERKc0ZVXW4GudCswM3m+P++ukU0= +google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -620,8 +622,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -633,8 +635,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -667,8 +669,8 @@ k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQI k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/lint/variables.go b/lint/variables.go index 37449d0..1c7db72 100644 --- a/lint/variables.go +++ b/lint/variables.go @@ -5,31 +5,31 @@ import ( "regexp" "strconv" "strings" - "time" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/promql/parser" ) const ( - rateInterval = "__rate_interval" - interval = "__interval" - intervalMs = "__interval_ms" - rangeMs = "__range_ms" - rangeS = "__range_s" - rangeVar = "__range" - dashboard = "__dashboard" - from = "__from" - to = "__to" - name = "__name" - org = "__org" - orgName = "__org.name" - userID = "__user.id" - userLogin = "__user.login" - userEmail = "__user.email" - timeFilter = "timeFilter" - timeFilter2 = "__timeFilter" - magicTimeRange = model.Duration(time.Hour*24*211 + time.Hour*12 + time.Minute*44 + time.Second*22 + time.Millisecond*50) // 211d12h44m22s50ms + rateInterval = "__rate_interval" + interval = "__interval" + intervalMs = "__interval_ms" + rangeMs = "__range_ms" + rangeS = "__range_s" + rangeVar = "__range" + dashboard = "__dashboard" + from = "__from" + to = "__to" + name = "__name" + org = "__org" + orgName = "__org.name" + userID = "__user.id" + userLogin = "__user.login" + userEmail = "__user.email" + timeFilter = "timeFilter" + timeFilter2 = "__timeFilter" + // magicTimeRange = model.Duration(time.Hour*24*211 + time.Hour*12 + time.Minute*44 + time.Second*22 + time.Millisecond*50) // 211d12h44m22s50ms + magicTimeRange = 11277964 // seconds 130d12h46m4s magicEpoch = float64(1294671549254) magicString = "bgludgvy" ) @@ -185,11 +185,30 @@ func expandVariables(expr string, variables []Template) (string, error) { return expr, nil } -func revertExpandedVariables(expr string) string { +func revertExpandedVariables(expr string) (string, error) { for _, p := range placeholderByValue { - expr = strings.ReplaceAll(expr, p.value, p.variable) + if p.valType == valTypeTimeRange { + // Replace all versions of time range placeholder + expr = strings.ReplaceAll(expr, p.value, p.variable) + + // Parse time duration + d, err := model.ParseDuration(p.value + "s") + if err != nil { + return "", fmt.Errorf("failed to parse duration: %s when reverting expanded variable: %s", p.value, p.variable) + } + expr = strings.ReplaceAll(expr, d.String(), p.variable) + + // Parse as float64 + f, err := strconv.ParseFloat(p.value, 64) + if err != nil { + return "", fmt.Errorf("failed to parse float64: %s when reverting expanded variable: %s", p.value, p.variable) + } + expr = strings.ReplaceAll(expr, fmt.Sprint(f), p.variable) + } else { + expr = strings.ReplaceAll(expr, p.value, p.variable) + } } - return expr + return expr, nil } // Should not replace variables inside double quotes @@ -292,8 +311,8 @@ func createPlaceholder(variable string, valType valType) string { for { if valType == valTypeTimeRange { // Using magicTimeRange as a seed for the placeholder - timeRange := magicTimeRange + model.Duration(time.Millisecond*time.Duration(counter)) - value = timeRange.String() + timeRange := magicTimeRange + counter + value = strconv.Itoa(timeRange) } if valType == valTypeEpoch { // Using magicEpoch as a seed for the placeholder diff --git a/lint/variables_test.go b/lint/variables_test.go index f20fe68..bc5751b 100644 --- a/lint/variables_test.go +++ b/lint/variables_test.go @@ -28,18 +28,18 @@ func TestVariableExpansion(t *testing.T) { }, { desc: "Should replace global rate/range variables", - expr: "rate(metric{}[$__rate_interval])", - result: "rate(metric{}[211d12h44m22s50ms])", + expr: "rate(metric{}[11277964])", + result: "rate(metric{}[11277964])", }, { desc: "Should support ${...} syntax", expr: "rate(metric{}[${__rate_interval}])", - result: "rate(metric{}[211d12h44m22s51ms])", + result: "rate(metric{}[11277965])", }, { desc: "Should support [[...]] syntax", expr: "rate(metric{}[[[__rate_interval]]])", - result: "rate(metric{}[211d12h44m22s52ms])", + result: "rate(metric{}[11277966])", }, // https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/ { @@ -76,72 +76,77 @@ func TestVariableExpansion(t *testing.T) { { desc: "Should support ${variable:csv} syntax", expr: "max by(${variable:csv}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_csv_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_csv_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:doublequote} syntax", expr: "max by(${variable:doublequote}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_doublequote_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_doublequote_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:glob} syntax", expr: "max by(${variable:glob}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_glob_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_glob_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:json} syntax", expr: "max by(${variable:json}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_json_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_json_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:lucene} syntax", expr: "max by(${variable:lucene}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_lucene_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_lucene_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:percentencode} syntax", expr: "max by(${variable:percentencode}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_percentencode_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_percentencode_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:pipe} syntax", expr: "max by(${variable:pipe}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_pipe_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_pipe_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:raw} syntax", expr: "max by(${variable:raw}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_raw_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_raw_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:regex} syntax", expr: "max by(${variable:regex}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_regex_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_regex_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:singlequote} syntax", expr: "max by(${variable:singlequote}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_singlequote_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_singlequote_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:sqlstring} syntax", expr: "max by(${variable:sqlstring}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_sqlstring_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_sqlstring_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:text} syntax", expr: "max by(${variable:text}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_text_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_text_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:queryparam} syntax", expr: "max by(${variable:queryparam}) (rate(cpu{}[$__rate_interval]))", - result: "max by(bgludgvy_variable_queryparam_0) (rate(cpu{}[211d12h44m22s50ms]))", + result: "max by(bgludgvy_variable_queryparam_0) (rate(cpu{}[11277964]))", + }, + { + desc: "Should support using variables for multiplication", + expr: "sum(rate(foo[$__rate_interval])) * $__range_s", + result: "sum(rate(foo[11277964])) * 11277976", }, { desc: "Should return an error for unknown syntax", expr: "max by(${a:b:c:d}) (rate(cpu{}[$__rate_interval]))", - err: fmt.Errorf("failed to parse expression: max by(${a:b:c:d}) (rate(cpu{}[211d12h44m22s50ms]))"), + err: fmt.Errorf("failed to parse expression: max by(${a:b:c:d}) (rate(cpu{}[11277964]))"), }, { desc: "Should replace variables present in the templating", @@ -169,7 +174,7 @@ func TestVariableExpansion(t *testing.T) { "value": "value", }}, }, - result: "max by(bgludgvy_var_0) (rate(cpu{}[211d12h44m22s68ms:211d12h44m22s71ms]))", + result: "max by(bgludgvy_var_0) (rate(cpu{}[11277982:11277985]))", }, { desc: "Should recursively replace variables", @@ -177,7 +182,7 @@ func TestVariableExpansion(t *testing.T) { variables: []Template{ {Name: "interval", Current: map[string]interface{}{"value": "$__auto_interval_interval"}}, }, - result: "sum (rate(cpu{}[211d12h44m22s68ms]))", + result: "sum (rate(cpu{}[11277982]))", }, { desc: "Should support plain $__auto_interval, generated by grafonnet-lib (https://github.com/grafana/grafonnet-lib/blob/master/grafonnet/template.libsonnet#L100)", @@ -185,7 +190,7 @@ func TestVariableExpansion(t *testing.T) { variables: []Template{ {Name: "interval", Current: map[string]interface{}{"value": "$__auto_interval"}}, }, - result: "sum (rate(cpu{}[211d12h44m22s68ms]))", + result: "sum (rate(cpu{}[11277982]))", }, } { s, err := expandVariables(tc.expr, tc.variables) @@ -196,83 +201,83 @@ func TestVariableExpansion(t *testing.T) { func TestReverseVariableExpansion(t *testing.T) { placeholderByValue = map[string]*placeholder{ - "bgludgvy_variable_queryparam_0": {variable: "${variable:queryparam}", value: "bgludgvy_variable_queryparam_0"}, - "211d12h44m22s63ms": {variable: "${__range_s}", value: "211d12h44m22s63ms"}, - "211d12h44m22s66ms": {variable: "${__range}", value: "211d12h44m22s66ms"}, - "1294671549257.000": {variable: "$__to", value: "1294671549257.000"}, - "bgludgvy___name_0": {variable: "$__name", value: "bgludgvy___name_0"}, - "bgludgvy_variable_regex_0": {variable: "${variable:regex}", value: "bgludgvy_variable_regex_0"}, - "bgludgvy_variable_glob_0": {variable: "${variable:glob}", value: "bgludgvy_variable_glob_0"}, - "211d12h44m22s52ms": {variable: "[[__rate_interval]]", value: "211d12h44m22s52ms"}, - "211d12h44m22s56ms": {variable: "$__interval_ms", value: "211d12h44m22s56ms"}, - "bgludgvy___dashboard_2": {variable: "[[__dashboard]]", value: "bgludgvy___dashboard_2"}, - "1294671549256.000": {variable: "[[__from]]", value: "1294671549256.000"}, - "bgludgvy___user.login_0": {variable: "$__user.login", value: "bgludgvy___user.login_0"}, - "bgludgvy_var_1": {variable: "${var}", value: "bgludgvy_var_1"}, - "bgludgvy_var_2": {variable: "[[var]]", value: "bgludgvy_var_2"}, - "bgludgvy_variable_sqlstring_0": {variable: "${variable:sqlstring}", value: "bgludgvy_variable_sqlstring_0"}, - "211d12h44m22s60ms": {variable: "${__range_ms}", value: "211d12h44m22s60ms"}, - "bgludgvy___org.name_2": {variable: "[[__org.name]]", value: "bgludgvy___org.name_2"}, - "bgludgvy_variable_csv_0": {variable: "${variable:csv}", value: "bgludgvy_variable_csv_0"}, - "bgludgvy_variable_lucene_0": {variable: "${variable:lucene}", value: "bgludgvy_variable_lucene_0"}, - "bgludgvy_variable_percentencode_0": {variable: "${variable:percentencode}", value: "bgludgvy_variable_percentencode_0"}, - "bgludgvy___user.email_1": {variable: "${__user.email}", value: "bgludgvy___user.email_1"}, - "211d12h44m22s53ms": {variable: "$__interval", value: "211d12h44m22s53ms"}, - "211d12h44m22s57ms": {variable: "${__interval_ms}", value: "211d12h44m22s57ms"}, - "211d12h44m22s58ms": {variable: "[[__interval_ms]]", value: "211d12h44m22s58ms"}, - "bgludgvy___dashboard_0": {variable: "$__dashboard", value: "bgludgvy___dashboard_0"}, - "1294671549265.000": {variable: "[[__user.id]]", value: "1294671549265.000"}, - "bgludgvy___timeFilter_0": {variable: "$__timeFilter", value: "bgludgvy___timeFilter_0"}, - "bgludgvy___timeFilter_1": {variable: "${__timeFilter}", value: "bgludgvy___timeFilter_1"}, - "bgludgvy___timeFilter_2": {variable: "[[__timeFilter]]", value: "bgludgvy___timeFilter_2"}, - "211d12h44m22s69ms": {variable: "${interval}", value: "211d12h44m22s69ms"}, - "1294671549268.000": {variable: "${__from:date:iso}", value: "1294671549268.000"}, - "211d12h44m22s54ms": {variable: "${__interval}", value: "211d12h44m22s54ms"}, - "211d12h44m22s64ms": {variable: "[[__range_s]]", value: "211d12h44m22s64ms"}, - "bgludgvy___name_2": {variable: "[[__name]]", value: "bgludgvy___name_2"}, - "bgludgvy___user.email_0": {variable: "$__user.email", value: "bgludgvy___user.email_0"}, - "bgludgvy___user.email_2": {variable: "[[__user.email]]", value: "bgludgvy___user.email_2"}, - "bgludgvy___dashboard_1": {variable: "${__dashboard}", value: "bgludgvy___dashboard_1"}, - "1294671549261.000": {variable: "${__org}", value: "1294671549261.000"}, - "1294671549264.000": {variable: "${__user.id}", value: "1294671549264.000"}, - "bgludgvy_variable_pipe_0": {variable: "${variable:pipe}", value: "bgludgvy_variable_pipe_0"}, - "bgludgvy_variable_raw_0": {variable: "${variable:raw}", value: "bgludgvy_variable_raw_0"}, - "211d12h44m22s50ms": {variable: "$__rate_interval", value: "211d12h44m22s50ms"}, - "211d12h44m22s62ms": {variable: "$__range_s", value: "211d12h44m22s62ms"}, - "1294671549259.000": {variable: "[[__to]]", value: "1294671549259.000"}, - "bgludgvy___org.name_0": {variable: "$__org.name", value: "bgludgvy___org.name_0"}, - "bgludgvy_timeFilter_1": {variable: "${timeFilter}", value: "bgludgvy_timeFilter_1"}, - "bgludgvy___user.login_2": {variable: "[[__user.login]]", value: "bgludgvy___user.login_2"}, - "1294671549267.000": {variable: "${__from:date}", value: "1294671549267.000"}, - "211d12h44m22s55ms": {variable: "[[__interval]]", value: "211d12h44m22s55ms"}, - "bgludgvy_var_0": {variable: "$var", value: "bgludgvy_var_0"}, - "1294671549269.000": {variable: "${__from:date:YYYY-MM}", value: "1294671549269.000"}, - "1294671549266.000": {variable: "${__from:date:seconds}", value: "1294671549266.000"}, - "bgludgvy_variable_singlequote_0": {variable: "${variable:singlequote}", value: "bgludgvy_variable_singlequote_0"}, - "211d12h44m22s51ms": {variable: "${__rate_interval}", value: "211d12h44m22s51ms"}, - "211d12h44m22s61ms": {variable: "[[__range_ms]]", value: "211d12h44m22s61ms"}, - "211d12h44m22s67ms": {variable: "[[__range]]", value: "211d12h44m22s67ms"}, - "1294671549258.000": {variable: "${__to}", value: "1294671549258.000"}, - "bgludgvy___user.login_1": {variable: "${__user.login}", value: "bgludgvy___user.login_1"}, - "211d12h44m22s70ms": {variable: "[[interval]]", value: "211d12h44m22s70ms"}, - "1294671549255.000": {variable: "${__from}", value: "1294671549255.000"}, - "bgludgvy_timeFilter_0": {variable: "$timeFilter", value: "bgludgvy_timeFilter_0"}, - "bgludgvy_timeFilter_2": {variable: "[[timeFilter]]", value: "bgludgvy_timeFilter_2"}, - "bgludgvy_variable_doublequote_0": {variable: "${variable:doublequote}", value: "bgludgvy_variable_doublequote_0"}, - "bgludgvy_variable_text_0": {variable: "${variable:text}", value: "bgludgvy_variable_text_0"}, - "bgludgvy___name_1": {variable: "${__name}", value: "bgludgvy___name_1"}, - "1294671549260.000": {variable: "$__org", value: "1294671549260.000"}, - "bgludgvy___org.name_1": {variable: "${__org.name}", value: "bgludgvy___org.name_1"}, - "211d12h44m22s59ms": {variable: "$__range_ms", value: "211d12h44m22s59ms"}, - "1294671549254.000": {variable: "$__from", value: "1294671549254.000"}, - "1294671549262.000": {variable: "[[__org]]", value: "1294671549262.000"}, - "211d12h44m22s68ms": {variable: "$interval", value: "211d12h44m22s68ms"}, - "211d12h44m22s72ms": {variable: "${resolution}", value: "211d12h44m22s72ms"}, - "211d12h44m22s65ms": {variable: "$__range", value: "211d12h44m22s65ms"}, - "1294671549263.000": {variable: "$__user.id", value: "1294671549263.000"}, - "bgludgvy_variable_json_0": {variable: "${variable:json}", value: "bgludgvy_variable_json_0"}, - "211d12h44m22s71ms": {variable: "$resolution", value: "211d12h44m22s71ms"}, - "211d12h44m22s73ms": {variable: "[[resolution]]", value: "211d12h44m22s73ms"}, + "bgludgvy___org.name_1": {variable: "${__org.name}", valType: 0, value: "bgludgvy___org.name_1"}, + "bgludgvy_variable_doublequote_0": {variable: "${variable:doublequote}", valType: 0, value: "bgludgvy_variable_doublequote_0"}, + "11277965": {variable: "${__rate_interval}", valType: 1, value: "11277965"}, + "11277966": {variable: "[[__rate_interval]]", valType: 1, value: "11277966"}, + "1294671549258.000": {variable: "${__to}", valType: 2, value: "1294671549258.000"}, + "bgludgvy___name_1": {variable: "${__name}", valType: 0, value: "bgludgvy___name_1"}, + "bgludgvy_variable_csv_0": {variable: "${variable:csv}", valType: 0, value: "bgludgvy_variable_csv_0"}, + "bgludgvy_variable_regex_0": {variable: "${variable:regex}", valType: 0, value: "bgludgvy_variable_regex_0"}, + "11277981": {variable: "[[__range]]", valType: 1, value: "11277981"}, + "1294671549264.000": {variable: "${__user.id}", valType: 2, value: "1294671549264.000"}, + "bgludgvy_timeFilter_2": {variable: "[[timeFilter]]", valType: 0, value: "bgludgvy_timeFilter_2"}, + "1294671549269.000": {variable: "${__from:date:YYYY-MM}", valType: 2, value: "1294671549269.000"}, + "1294671549266.000": {variable: "${__from:date:seconds}", valType: 2, value: "1294671549266.000"}, + "1294671549268.000": {variable: "${__from:date:iso}", valType: 2, value: "1294671549268.000"}, + "bgludgvy_variable_json_0": {variable: "${variable:json}", valType: 0, value: "bgludgvy_variable_json_0"}, + "bgludgvy_variable_singlequote_0": {variable: "${variable:singlequote}", valType: 0, value: "bgludgvy_variable_singlequote_0"}, + "1294671549256.000": {variable: "[[__from]]", valType: 2, value: "1294671549256.000"}, + "1294671549260.000": {variable: "$__org", valType: 2, value: "1294671549260.000"}, + "1294671549263.000": {variable: "$__user.id", valType: 2, value: "1294671549263.000"}, + "bgludgvy___timeFilter_0": {variable: "$__timeFilter", valType: 0, value: "bgludgvy___timeFilter_0"}, + "bgludgvy_var_2": {variable: "[[var]]", valType: 0, value: "bgludgvy_var_2"}, + "11277977": {variable: "${__range_s}", valType: 1, value: "11277977"}, + "11277979": {variable: "$__range", valType: 1, value: "11277979"}, + "1294671549259.000": {variable: "[[__to]]", valType: 2, value: "1294671549259.000"}, + "bgludgvy___user.email_0": {variable: "$__user.email", valType: 0, value: "bgludgvy___user.email_0"}, + "bgludgvy_variable_glob_0": {variable: "${variable:glob}", valType: 0, value: "bgludgvy_variable_glob_0"}, + "bgludgvy_variable_lucene_0": {variable: "${variable:lucene}", valType: 0, value: "bgludgvy_variable_lucene_0"}, + "11277983": {variable: "${interval}", valType: 1, value: "11277983"}, + "11277980": {variable: "${__range}", valType: 1, value: "11277980"}, + "1294671549262.000": {variable: "[[__org]]", valType: 2, value: "1294671549262.000"}, + "bgludgvy___timeFilter_1": {variable: "${__timeFilter}", valType: 0, value: "bgludgvy___timeFilter_1"}, + "bgludgvy_var_0": {variable: "$var", valType: 0, value: "bgludgvy_var_0"}, + "bgludgvy_variable_pipe_0": {variable: "${variable:pipe}", valType: 0, value: "bgludgvy_variable_pipe_0"}, + "11277964": {variable: "$__rate_interval", valType: 1, value: "11277964"}, + "11277969": {variable: "[[__interval]]", valType: 1, value: "11277969"}, + "bgludgvy___dashboard_1": {variable: "${__dashboard}", valType: 0, value: "bgludgvy___dashboard_1"}, + "1294671549255.000": {variable: "${__from}", valType: 2, value: "1294671549255.000"}, + "bgludgvy_variable_raw_0": {variable: "${variable:raw}", valType: 0, value: "bgludgvy_variable_raw_0"}, + "bgludgvy_variable_sqlstring_0": {variable: "${variable:sqlstring}", valType: 0, value: "bgludgvy_variable_sqlstring_0"}, + "bgludgvy_variable_text_0": {variable: "${variable:text}", valType: 0, value: "bgludgvy_variable_text_0"}, + "11277978": {variable: "[[__range_s]]", valType: 1, value: "11277978"}, + "bgludgvy___dashboard_2": {variable: "[[__dashboard]]", valType: 0, value: "bgludgvy___dashboard_2"}, + "bgludgvy___user.email_2": {variable: "[[__user.email]]", valType: 0, value: "bgludgvy___user.email_2"}, + "bgludgvy_timeFilter_1": {variable: "${timeFilter}", valType: 0, value: "bgludgvy_timeFilter_1"}, + "11277972": {variable: "[[__interval_ms]]", valType: 1, value: "11277972"}, + "bgludgvy___name_0": {variable: "$__name", valType: 0, value: "bgludgvy___name_0"}, + "bgludgvy___name_2": {variable: "[[__name]]", valType: 0, value: "bgludgvy___name_2"}, + "bgludgvy_var_1": {variable: "${var}", valType: 0, value: "bgludgvy_var_1"}, + "11277971": {variable: "${__interval_ms}", valType: 1, value: "11277971"}, + "bgludgvy___user.login_0": {variable: "$__user.login", valType: 0, value: "bgludgvy___user.login_0"}, + "bgludgvy___user.login_1": {variable: "${__user.login}", valType: 0, value: "bgludgvy___user.login_1"}, + "bgludgvy_variable_queryparam_0": {variable: "${variable:queryparam}", valType: 0, value: "bgludgvy_variable_queryparam_0"}, + "bgludgvy_timeFilter_0": {variable: "$timeFilter", valType: 0, value: "bgludgvy_timeFilter_0"}, + "11277967": {variable: "$__interval", valType: 1, value: "11277967"}, + "11277974": {variable: "${__range_ms}", valType: 1, value: "11277974"}, + "bgludgvy___org.name_2": {variable: "[[__org.name]]", valType: 0, value: "bgludgvy___org.name_2"}, + "bgludgvy___user.email_1": {variable: "${__user.email}", valType: 0, value: "bgludgvy___user.email_1"}, + "11277985": {variable: "$resolution", valType: 1, value: "11277985"}, + "11277987": {variable: "[[resolution]]", valType: 1, value: "11277987"}, + "11277973": {variable: "$__range_ms", valType: 1, value: "11277973"}, + "11277976": {variable: "$__range_s", valType: 1, value: "11277976"}, + "bgludgvy___dashboard_0": {variable: "$__dashboard", valType: 0, value: "bgludgvy___dashboard_0"}, + "bgludgvy___user.login_2": {variable: "[[__user.login]]", valType: 0, value: "bgludgvy___user.login_2"}, + "bgludgvy___timeFilter_2": {variable: "[[__timeFilter]]", valType: 0, value: "bgludgvy___timeFilter_2"}, + "11277984": {variable: "[[interval]]", valType: 1, value: "11277984"}, + "1294671549254.000": {variable: "$__from", valType: 2, value: "1294671549254.000"}, + "bgludgvy_variable_percentencode_0": {variable: "${variable:percentencode}", valType: 0, value: "bgludgvy_variable_percentencode_0"}, + "bgludgvy___org.name_0": {variable: "$__org.name", valType: 0, value: "bgludgvy___org.name_0"}, + "11277986": {variable: "${resolution}", valType: 1, value: "11277986"}, + "11277970": {variable: "$__interval_ms", valType: 1, value: "11277970"}, + "11277975": {variable: "[[__range_ms]]", valType: 1, value: "11277975"}, + "1294671549257.000": {variable: "$__to", valType: 2, value: "1294671549257.000"}, + "1294671549265.000": {variable: "[[__user.id]]", valType: 2, value: "1294671549265.000"}, + "11277968": {variable: "${__interval}", valType: 1, value: "11277968"}, + "1294671549261.000": {variable: "${__org}", valType: 2, value: "1294671549261.000"}, + "1294671549267.000": {variable: "${__from:date}", valType: 2, value: "1294671549267.000"}, + "11277982": {variable: "$interval", valType: 1, value: "11277982"}, } for _, tc := range []struct { desc string @@ -292,17 +297,17 @@ func TestReverseVariableExpansion(t *testing.T) { }, { desc: "Should replace global rate/range variables", - expr: "rate(metric{}[211d12h44m22s50ms])", + expr: "rate(metric{}[130d12h46m4s])", result: "rate(metric{}[$__rate_interval])", }, { desc: "Should support ${...} syntax", - expr: "rate(metric{}[211d12h44m22s51ms])", + expr: "rate(metric{}[130d12h46m5s])", result: "rate(metric{}[${__rate_interval}])", }, { desc: "Should support [[...]] syntax", - expr: "rate(metric{}[211d12h44m22s52ms])", + expr: "rate(metric{}[130d12h46m6s])", result: "rate(metric{}[[[__rate_interval]]])", }, // https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/ @@ -339,86 +344,91 @@ func TestReverseVariableExpansion(t *testing.T) { // https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options/ { desc: "Should support ${variable:csv} syntax", - expr: "max by(bgludgvy_variable_csv_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_csv_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:csv}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:doublequote} syntax", - expr: "max by(bgludgvy_variable_doublequote_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_doublequote_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:doublequote}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:glob} syntax", - expr: "max by(bgludgvy_variable_glob_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_glob_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:glob}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:json} syntax", - expr: "max by(bgludgvy_variable_json_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_json_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:json}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:lucene} syntax", - expr: "max by(bgludgvy_variable_lucene_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_lucene_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:lucene}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:percentencode} syntax", - expr: "max by(bgludgvy_variable_percentencode_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_percentencode_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:percentencode}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:pipe} syntax", - expr: "max by(bgludgvy_variable_pipe_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_pipe_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:pipe}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:raw} syntax", - expr: "max by(bgludgvy_variable_raw_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_raw_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:raw}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:regex} syntax", - expr: "max by(bgludgvy_variable_regex_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_regex_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:regex}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:singlequote} syntax", - expr: "max by(bgludgvy_variable_singlequote_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_singlequote_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:singlequote}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:sqlstring} syntax", - expr: "max by(bgludgvy_variable_sqlstring_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_sqlstring_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:sqlstring}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:text} syntax", - expr: "max by(bgludgvy_variable_text_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_text_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:text}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should support ${variable:queryparam} syntax", - expr: "max by(bgludgvy_variable_queryparam_0) (rate(cpu{}[211d12h44m22s50ms]))", + expr: "max by(bgludgvy_variable_queryparam_0) (rate(cpu{}[130d12h46m4s]))", result: "max by(${variable:queryparam}) (rate(cpu{}[$__rate_interval]))", }, { desc: "Should replace variables present in the templating", - expr: "max by(bgludgvy_var_0) (rate(cpu{}[211d12h44m22s68ms:211d12h44m22s71ms]))", + expr: "max by(bgludgvy_var_0) (rate(cpu{}[130d12h46m22s:130d12h46m25s]))", result: "max by($var) (rate(cpu{}[$interval:$resolution]))", }, + { + desc: "Should support using variables for multiplication", + expr: "sum(rate(foo[130d12h46m4s])) * 11277976", + result: "sum(rate(foo[$__rate_interval])) * $__range_s", + }, { desc: "Should recursively replace variables", - expr: "sum (rate(cpu{}[211d12h44m22s68ms]))", + expr: "sum (rate(cpu{}[130d12h46m22s]))", result: "sum (rate(cpu{}[$interval]))", }, { desc: "Should support plain $__auto_interval, generated by grafonnet-lib (https://github.com/grafana/grafonnet-lib/blob/master/grafonnet/template.libsonnet#L100)", - expr: "sum (rate(cpu{}[211d12h44m22s68ms]))", + expr: "sum (rate(cpu{}[130d12h46m22s]))", result: "sum (rate(cpu{}[$interval]))", }, } { - s := revertExpandedVariables(tc.expr) + s, _ := revertExpandedVariables(tc.expr) require.Equal(t, tc.result, s, tc.desc) } } From c4bb4fc6d0586c2e19d5da551de6ad7eaa4dd9ee Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:17:51 +0200 Subject: [PATCH 17/26] fix failing test --- lint/rule_target_rate_interval.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lint/rule_target_rate_interval.go b/lint/rule_target_rate_interval.go index 4a6e9b0..c5ca1e7 100644 --- a/lint/rule_target_rate_interval.go +++ b/lint/rule_target_rate_interval.go @@ -41,7 +41,7 @@ func NewTargetRateIntervalRule() *TargetRuleFunc { // Invalid PromQL is another rule return r } - rateIntervalMagicDuration, err := model.ParseDuration(placeholderByVariable["$__rate_interval"].value) + rateIntervalMagicDuration, err := model.ParseDuration(placeholderByVariable["$__rate_interval"].value + "s") if err != nil { // Will not happen panic(err) From 98ee145843a4bd5e3bcb7d4280f6f5c914a58e6f Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:23:46 +0200 Subject: [PATCH 18/26] Reverse variable expansion for: target-required-matchers-rule --- lint/rule_target_required_matchers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go index 9ccb85a..072cf14 100644 --- a/lint/rule_target_required_matchers.go +++ b/lint/rule_target_required_matchers.go @@ -56,7 +56,11 @@ func fixTargetRequiredMatcherRule(name string, ty labels.MatchType, value string if err != nil { return } - t.Expr = expr.String() + e, err := revertExpandedVariables(expr.String()) + if err != nil { + return + } + t.Expr = e } } From 54aeb32aae8a749bf8f8a1d7f340d51b986eb326 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:17:46 +0200 Subject: [PATCH 19/26] Do not omitempty dashboard editable --- lint/lint.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lint/lint.go b/lint/lint.go index 59f0332..2b147b9 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -273,7 +273,7 @@ type Dashboard struct { } `json:"templating"` Rows []Row `json:"rows,omitempty"` Panels []Panel `json:"panels,omitempty"` - Editable bool `json:"editable,omitempty"` + Editable bool `json:"editable"` // Do not omitempty, since false is seen as empty, and if it is not included, it defaults to true. } // GetPanels returns the all panels whether they are nested in the (now deprecated) "rows" property or From ae10cf28e0d881606009bd48445534bcaf109181 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:43:21 +0200 Subject: [PATCH 20/26] Support templating variables having a variable as value (expand recursive) --- lint/variables.go | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/lint/variables.go b/lint/variables.go index 1c7db72..b537829 100644 --- a/lint/variables.go +++ b/lint/variables.go @@ -288,7 +288,11 @@ func assignPlaceholder(placeholder placeholder) error { } func getValueType(value string) valType { - // check if variable is time range + // value might be provided as an integer, so we need to check if it can be parsed as an integer and then add s to the end) + if _, err := strconv.Atoi(value); err == nil { + value = value + "s" + } + // check if variable is a time range if _, err := model.ParseDuration(value); err == nil { return valTypeTimeRange } @@ -353,8 +357,14 @@ func trimVariableSyntax(s string) string { return s } +// Helper func to check if string has variable syntax +func checkVariableSyntax(s string) bool { + return strings.Contains(s, "$") || strings.Contains(s, "[[") || strings.Contains(s, "{") +} + // Helper func to get the value of a template variable func getTemplateVariableValue(v Template) string { + var value string // do not handle error c, _ := v.Current.Get() // check if variable has a value @@ -363,12 +373,25 @@ func getTemplateVariableValue(v Template) string { // Do not handle error o, _ := v.Options[0].Get() if o.Value != "" { - return o.Value + value = o.Value + } + } + } else { + value = c.Value + } + // check value for variable syntax + if checkVariableSyntax(value) { + // lazy way of dealing with __auto_interval... + if strings.HasPrefix(trimVariableSyntax(value), "__auto_interval") { + // This will result in a placeholder with type timeRange + value = "9001s" + } else { + // try to expand variable + varValue := getPlaceholder(value, "") + if varValue != nil { + value = varValue.value } } - // v.Current.Value is empty and no options are provided return empty string - return "" - // Helper func to check if a format option is supported } - return c.Value + return value } From 99d1d7c7593e64dc6c685474935a5eeb29a88203 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:02:17 +0200 Subject: [PATCH 21/26] :adhesive_bandage: Modfiy regexp matching to only match string that promql parser can parse --- lint/rule_template_label_promql.go | 44 ++++++++++++++++++------------ 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/lint/rule_template_label_promql.go b/lint/rule_template_label_promql.go index 4ec0c13..eec0c5f 100644 --- a/lint/rule_template_label_promql.go +++ b/lint/rule_template_label_promql.go @@ -5,34 +5,45 @@ import ( "regexp" ) -var templatedLabelRegexp = regexp.MustCompile(`([a-z_]+)\((.+)\)`) +var ( + lvNoQueryRegexp = regexp.MustCompile(`(?s)label_values\((.+)\)`) // label_values(label) + lvRegexp = regexp.MustCompile(`(?s)label_values\((.+),.+\)`) // label_values(metric, label) + mRegexp = regexp.MustCompile(`(?s)metrics\((.+)\)`) // metrics(metric) + lnRegexp = regexp.MustCompile(`(?s)label_names\((.+)\)`) // label_names() + qrRegexp = regexp.MustCompile(`(?s)query_result\((.+)\)`) // query_result(query) +) -func labelHasValidDataSourceFunction(name string) bool { - // https://grafana.com/docs/grafana/v8.1/datasources/prometheus/#query-variable - names := []string{"label_names", "label_values", "metrics", "query_result"} - for _, n := range names { - if name == n { - return true - } +func extractPromQLQuery(q string) []string { + // label_values(query, label) + switch { + case lvRegexp.MatchString(q): + return lvRegexp.FindStringSubmatch(q) + case lvNoQueryRegexp.MatchString(q): + return nil // No query so no metric. + case mRegexp.MatchString(q): + return mRegexp.FindStringSubmatch(q) + case lnRegexp.MatchString(q): + return lnRegexp.FindStringSubmatch(q) + case qrRegexp.MatchString(q): + return qrRegexp.FindStringSubmatch(q) + default: + return nil } - return false } // parseTemplatedLabelPromQL returns error in case // 1) The given PromQL expressions is invalid // 2) Use of invalid label function func parseTemplatedLabelPromQL(t Template, variables []Template) error { - // regex capture must return slice of 3 strings. - // 1) given query 2) function name 3) function arg. - tokens := templatedLabelRegexp.FindStringSubmatch(t.Query) + // regex capture must return slice of 2 strings. + // 1) given query 2) function arg. + + tokens := extractPromQLQuery(t.Query) if tokens == nil { return fmt.Errorf("invalid 'query': %v", t.Query) } - if !labelHasValidDataSourceFunction(tokens[1]) { - return fmt.Errorf("invalid 'function': %v", tokens[1]) - } - expr, err := parsePromQL(tokens[2], variables) + expr, err := parsePromQL(tokens[1], variables) if expr != nil { return nil } @@ -59,7 +70,6 @@ func NewTemplateLabelPromQLRule() *DashboardRuleFunc { r.AddError(d, fmt.Sprintf("template '%s' invalid templated label '%s': %v", template.Name, template.Query, err)) } } - return r }, } From 17956a8facff3ec635f07e5e4c252ee6244ba0c3 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:03:45 +0200 Subject: [PATCH 22/26] :adhesive_bandage: Use a combination of slices and maps to ensure output is same everytime... tests... --- lint/rule_template_required_variables.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lint/rule_template_required_variables.go b/lint/rule_template_required_variables.go index d7913c5..479475d 100644 --- a/lint/rule_template_required_variables.go +++ b/lint/rule_template_required_variables.go @@ -21,12 +21,18 @@ func NewTemplateRequiredVariablesRule(config *TemplateRequiredVariablesRuleSetti return r } - var variables = make(map[string]bool) + // Create a map and a slice, map for uniqueness and slice to keep the order... + var varMap = make(map[string]bool) + var varSlice = []string{} if config != nil { // Convert the config.variables to a map to leverage uniqueness... for _, v := range config.Variables { - variables[v] = true + if varMap[v] { + continue + } + varMap[v] = true + varSlice = append(varSlice, v) } } @@ -34,12 +40,16 @@ func NewTemplateRequiredVariablesRule(config *TemplateRequiredVariablesRuleSetti // Check that all required matchers that use variables form target-required-matchers have a corresponding template variable for _, m := range requiredMatchers.Matchers { if strings.HasPrefix(m.Value, "$") { - variables[m.Value[1:]] = true + if varMap[m.Value[1:]] { + continue + } + varMap[m.Value[1:]] = true + varSlice = append(varSlice, m.Value[1:]) } } } - for v := range variables { + for _, v := range varSlice { checkTemplate(d, v, &r) } return r From 2594e21afed3fc3633124f1f02a070d8966bba51 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:06:01 +0200 Subject: [PATCH 23/26] :white_check_mark: Update tests to match new variable expansion method + make them work with single test run and full package run --- lint/rule_target_promql_test.go | 12 +- lint/rule_template_label_promql_test.go | 4 +- lint/variables_test.go | 150 +++++++++++++----------- 3 files changed, 92 insertions(+), 74 deletions(-) diff --git a/lint/rule_target_promql_test.go b/lint/rule_target_promql_test.go index 00208ca..2a15a0a 100644 --- a/lint/rule_target_promql_test.go +++ b/lint/rule_target_promql_test.go @@ -113,16 +113,13 @@ func TestTargetPromQLRule(t *testing.T) { Type: "singlestat", Targets: []Target{ { - Expr: `sum (rate(foo[$interval:$resolution]))`, + Expr: `max by($var) (rate(cpu{}[$interval:$resolution]))`, }, }, }, }, { - result: []Result{{ - Severity: Error, - Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'increase(foo{}[$sampling])': could not expand variables: failed to parse expression: increase(foo{}[bgludgvy_sampling_0])", - }}, + result: []Result{ResultSuccess}, panel: Panel{ Title: "panel", Type: "singlestat", @@ -197,6 +194,11 @@ func TestTargetPromQLRule(t *testing.T) { Name: "sampling", Current: map[string]interface{}{"value": "$__auto_interval_sampling"}, }, + { + Name: "var", + Type: "query", + Current: map[string]interface{}{"value": "value"}, + }, { Type: "resolution", Name: "resolution", diff --git a/lint/rule_template_label_promql_test.go b/lint/rule_template_label_promql_test.go index 8ef33ac..aec7778 100644 --- a/lint/rule_template_label_promql_test.go +++ b/lint/rule_template_label_promql_test.go @@ -57,7 +57,7 @@ func TestTemplateLabelPromQLRule(t *testing.T) { name: "Error", result: Result{ Severity: Error, - Message: `Dashboard 'test' template 'namespaces' invalid templated label 'label_values(up{, namespace)': 1:4: parse error: unexpected "," in label matching, expected identifier or "}"`, + Message: `Dashboard 'test' template 'namespaces' invalid templated label 'label_values(up{, namespace)': could not expand variables: failed to parse expression: up{`, }, dashboard: Dashboard{ Title: "test", @@ -84,7 +84,7 @@ func TestTemplateLabelPromQLRule(t *testing.T) { name: "Invalid function.", result: Result{ Severity: Error, - Message: `Dashboard 'test' template 'namespaces' invalid templated label 'foo(up, namespace)': invalid 'function': foo`, + Message: `Dashboard 'test' template 'namespaces' invalid templated label 'foo(up, namespace)': invalid 'query': foo(up, namespace)`, }, dashboard: Dashboard{ Title: "test", diff --git a/lint/variables_test.go b/lint/variables_test.go index bc5751b..495b461 100644 --- a/lint/variables_test.go +++ b/lint/variables_test.go @@ -160,21 +160,31 @@ func TestVariableExpansion(t *testing.T) { }, }, }, + { + Name: "sampling", + Options: []RawTemplateValue{ + map[string]interface{}{ + "value": "1h", + }, + }, + }, { Name: "resolution", Options: []RawTemplateValue{ map[string]interface{}{ "value": "5m", }, - }}, + }, + }, { Name: "var", Type: "query", Current: map[string]interface{}{ "value": "value", - }}, + }, + }, }, - result: "max by(bgludgvy_var_0) (rate(cpu{}[11277982:11277985]))", + result: "max by(bgludgvy_var_0) (rate(cpu{}[11277982:11277988]))", }, { desc: "Should recursively replace variables", @@ -201,83 +211,89 @@ func TestVariableExpansion(t *testing.T) { func TestReverseVariableExpansion(t *testing.T) { placeholderByValue = map[string]*placeholder{ - "bgludgvy___org.name_1": {variable: "${__org.name}", valType: 0, value: "bgludgvy___org.name_1"}, - "bgludgvy_variable_doublequote_0": {variable: "${variable:doublequote}", valType: 0, value: "bgludgvy_variable_doublequote_0"}, + "11277964": {variable: "$__rate_interval", valType: 1, value: "11277964"}, "11277965": {variable: "${__rate_interval}", valType: 1, value: "11277965"}, "11277966": {variable: "[[__rate_interval]]", valType: 1, value: "11277966"}, - "1294671549258.000": {variable: "${__to}", valType: 2, value: "1294671549258.000"}, - "bgludgvy___name_1": {variable: "${__name}", valType: 0, value: "bgludgvy___name_1"}, - "bgludgvy_variable_csv_0": {variable: "${variable:csv}", valType: 0, value: "bgludgvy_variable_csv_0"}, - "bgludgvy_variable_regex_0": {variable: "${variable:regex}", valType: 0, value: "bgludgvy_variable_regex_0"}, - "11277981": {variable: "[[__range]]", valType: 1, value: "11277981"}, - "1294671549264.000": {variable: "${__user.id}", valType: 2, value: "1294671549264.000"}, - "bgludgvy_timeFilter_2": {variable: "[[timeFilter]]", valType: 0, value: "bgludgvy_timeFilter_2"}, - "1294671549269.000": {variable: "${__from:date:YYYY-MM}", valType: 2, value: "1294671549269.000"}, - "1294671549266.000": {variable: "${__from:date:seconds}", valType: 2, value: "1294671549266.000"}, - "1294671549268.000": {variable: "${__from:date:iso}", valType: 2, value: "1294671549268.000"}, - "bgludgvy_variable_json_0": {variable: "${variable:json}", valType: 0, value: "bgludgvy_variable_json_0"}, - "bgludgvy_variable_singlequote_0": {variable: "${variable:singlequote}", valType: 0, value: "bgludgvy_variable_singlequote_0"}, - "1294671549256.000": {variable: "[[__from]]", valType: 2, value: "1294671549256.000"}, - "1294671549260.000": {variable: "$__org", valType: 2, value: "1294671549260.000"}, - "1294671549263.000": {variable: "$__user.id", valType: 2, value: "1294671549263.000"}, - "bgludgvy___timeFilter_0": {variable: "$__timeFilter", valType: 0, value: "bgludgvy___timeFilter_0"}, - "bgludgvy_var_2": {variable: "[[var]]", valType: 0, value: "bgludgvy_var_2"}, + "11277967": {variable: "$__interval", valType: 1, value: "11277967"}, + "11277968": {variable: "${__interval}", valType: 1, value: "11277968"}, + "11277969": {variable: "[[__interval]]", valType: 1, value: "11277969"}, + "11277970": {variable: "$__interval_ms", valType: 1, value: "11277970"}, + "11277971": {variable: "${__interval_ms}", valType: 1, value: "11277971"}, + "11277972": {variable: "[[__interval_ms]]", valType: 1, value: "11277972"}, + "11277973": {variable: "$__range_ms", valType: 1, value: "11277973"}, + "11277974": {variable: "${__range_ms}", valType: 1, value: "11277974"}, + "11277975": {variable: "[[__range_ms]]", valType: 1, value: "11277975"}, + "11277976": {variable: "$__range_s", valType: 1, value: "11277976"}, "11277977": {variable: "${__range_s}", valType: 1, value: "11277977"}, + "11277978": {variable: "[[__range_s]]", valType: 1, value: "11277978"}, "11277979": {variable: "$__range", valType: 1, value: "11277979"}, - "1294671549259.000": {variable: "[[__to]]", valType: 2, value: "1294671549259.000"}, - "bgludgvy___user.email_0": {variable: "$__user.email", valType: 0, value: "bgludgvy___user.email_0"}, - "bgludgvy_variable_glob_0": {variable: "${variable:glob}", valType: 0, value: "bgludgvy_variable_glob_0"}, - "bgludgvy_variable_lucene_0": {variable: "${variable:lucene}", valType: 0, value: "bgludgvy_variable_lucene_0"}, - "11277983": {variable: "${interval}", valType: 1, value: "11277983"}, "11277980": {variable: "${__range}", valType: 1, value: "11277980"}, + "11277981": {variable: "[[__range]]", valType: 1, value: "11277981"}, + "11277982": {variable: "$interval", valType: 1, value: "11277982"}, + "11277983": {variable: "${interval}", valType: 1, value: "11277983"}, + "11277984": {variable: "[[interval]]", valType: 1, value: "11277984"}, + "11277985": {variable: "$sampling", valType: 1, value: "11277985"}, + "11277986": {variable: "${sampling}", valType: 1, value: "11277986"}, + "11277987": {variable: "[[sampling]]", valType: 1, value: "11277987"}, + "11277988": {variable: "$resolution", valType: 1, value: "11277988"}, + "11277989": {variable: "${resolution}", valType: 1, value: "11277989"}, + "11277990": {variable: "[[resolution]]", valType: 1, value: "11277990"}, + "1294671549254.000": {variable: "$__from", valType: 2, value: "1294671549254.000"}, + "1294671549255.000": {variable: "${__from}", valType: 2, value: "1294671549255.000"}, + "1294671549256.000": {variable: "[[__from]]", valType: 2, value: "1294671549256.000"}, + "1294671549257.000": {variable: "$__to", valType: 2, value: "1294671549257.000"}, + "1294671549258.000": {variable: "${__to}", valType: 2, value: "1294671549258.000"}, + "1294671549259.000": {variable: "[[__to]]", valType: 2, value: "1294671549259.000"}, + "1294671549260.000": {variable: "$__org", valType: 2, value: "1294671549260.000"}, + "1294671549261.000": {variable: "${__org}", valType: 2, value: "1294671549261.000"}, "1294671549262.000": {variable: "[[__org]]", valType: 2, value: "1294671549262.000"}, - "bgludgvy___timeFilter_1": {variable: "${__timeFilter}", valType: 0, value: "bgludgvy___timeFilter_1"}, - "bgludgvy_var_0": {variable: "$var", valType: 0, value: "bgludgvy_var_0"}, - "bgludgvy_variable_pipe_0": {variable: "${variable:pipe}", valType: 0, value: "bgludgvy_variable_pipe_0"}, - "11277964": {variable: "$__rate_interval", valType: 1, value: "11277964"}, - "11277969": {variable: "[[__interval]]", valType: 1, value: "11277969"}, + "1294671549263.000": {variable: "$__user.id", valType: 2, value: "1294671549263.000"}, + "1294671549264.000": {variable: "${__user.id}", valType: 2, value: "1294671549264.000"}, + "1294671549265.000": {variable: "[[__user.id]]", valType: 2, value: "1294671549265.000"}, + "1294671549266.000": {variable: "${__from:date:seconds}", valType: 2, value: "1294671549266.000"}, + "1294671549267.000": {variable: "${__from:date}", valType: 2, value: "1294671549267.000"}, + "1294671549268.000": {variable: "${__from:date:iso}", valType: 2, value: "1294671549268.000"}, + "1294671549269.000": {variable: "${__from:date:YYYY-MM}", valType: 2, value: "1294671549269.000"}, + "bgludgvy___dashboard_0": {variable: "$__dashboard", valType: 0, value: "bgludgvy___dashboard_0"}, "bgludgvy___dashboard_1": {variable: "${__dashboard}", valType: 0, value: "bgludgvy___dashboard_1"}, - "1294671549255.000": {variable: "${__from}", valType: 2, value: "1294671549255.000"}, - "bgludgvy_variable_raw_0": {variable: "${variable:raw}", valType: 0, value: "bgludgvy_variable_raw_0"}, - "bgludgvy_variable_sqlstring_0": {variable: "${variable:sqlstring}", valType: 0, value: "bgludgvy_variable_sqlstring_0"}, - "bgludgvy_variable_text_0": {variable: "${variable:text}", valType: 0, value: "bgludgvy_variable_text_0"}, - "11277978": {variable: "[[__range_s]]", valType: 1, value: "11277978"}, "bgludgvy___dashboard_2": {variable: "[[__dashboard]]", valType: 0, value: "bgludgvy___dashboard_2"}, - "bgludgvy___user.email_2": {variable: "[[__user.email]]", valType: 0, value: "bgludgvy___user.email_2"}, - "bgludgvy_timeFilter_1": {variable: "${timeFilter}", valType: 0, value: "bgludgvy_timeFilter_1"}, - "11277972": {variable: "[[__interval_ms]]", valType: 1, value: "11277972"}, "bgludgvy___name_0": {variable: "$__name", valType: 0, value: "bgludgvy___name_0"}, + "bgludgvy___name_1": {variable: "${__name}", valType: 0, value: "bgludgvy___name_1"}, "bgludgvy___name_2": {variable: "[[__name]]", valType: 0, value: "bgludgvy___name_2"}, - "bgludgvy_var_1": {variable: "${var}", valType: 0, value: "bgludgvy_var_1"}, - "11277971": {variable: "${__interval_ms}", valType: 1, value: "11277971"}, - "bgludgvy___user.login_0": {variable: "$__user.login", valType: 0, value: "bgludgvy___user.login_0"}, - "bgludgvy___user.login_1": {variable: "${__user.login}", valType: 0, value: "bgludgvy___user.login_1"}, - "bgludgvy_variable_queryparam_0": {variable: "${variable:queryparam}", valType: 0, value: "bgludgvy_variable_queryparam_0"}, - "bgludgvy_timeFilter_0": {variable: "$timeFilter", valType: 0, value: "bgludgvy_timeFilter_0"}, - "11277967": {variable: "$__interval", valType: 1, value: "11277967"}, - "11277974": {variable: "${__range_ms}", valType: 1, value: "11277974"}, + "bgludgvy___org.name_0": {variable: "$__org.name", valType: 0, value: "bgludgvy___org.name_0"}, + "bgludgvy___org.name_1": {variable: "${__org.name}", valType: 0, value: "bgludgvy___org.name_1"}, "bgludgvy___org.name_2": {variable: "[[__org.name]]", valType: 0, value: "bgludgvy___org.name_2"}, + "bgludgvy___timeFilter_0": {variable: "$__timeFilter", valType: 0, value: "bgludgvy___timeFilter_0"}, + "bgludgvy___timeFilter_1": {variable: "${__timeFilter}", valType: 0, value: "bgludgvy___timeFilter_1"}, + "bgludgvy___timeFilter_2": {variable: "[[__timeFilter]]", valType: 0, value: "bgludgvy___timeFilter_2"}, + "bgludgvy___user.email_0": {variable: "$__user.email", valType: 0, value: "bgludgvy___user.email_0"}, "bgludgvy___user.email_1": {variable: "${__user.email}", valType: 0, value: "bgludgvy___user.email_1"}, - "11277985": {variable: "$resolution", valType: 1, value: "11277985"}, - "11277987": {variable: "[[resolution]]", valType: 1, value: "11277987"}, - "11277973": {variable: "$__range_ms", valType: 1, value: "11277973"}, - "11277976": {variable: "$__range_s", valType: 1, value: "11277976"}, - "bgludgvy___dashboard_0": {variable: "$__dashboard", valType: 0, value: "bgludgvy___dashboard_0"}, + "bgludgvy___user.email_2": {variable: "[[__user.email]]", valType: 0, value: "bgludgvy___user.email_2"}, + "bgludgvy___user.login_0": {variable: "$__user.login", valType: 0, value: "bgludgvy___user.login_0"}, + "bgludgvy___user.login_1": {variable: "${__user.login}", valType: 0, value: "bgludgvy___user.login_1"}, "bgludgvy___user.login_2": {variable: "[[__user.login]]", valType: 0, value: "bgludgvy___user.login_2"}, - "bgludgvy___timeFilter_2": {variable: "[[__timeFilter]]", valType: 0, value: "bgludgvy___timeFilter_2"}, - "11277984": {variable: "[[interval]]", valType: 1, value: "11277984"}, - "1294671549254.000": {variable: "$__from", valType: 2, value: "1294671549254.000"}, + "bgludgvy_namespaces_0": {variable: "$namespaces", valType: 0, value: "bgludgvy_namespaces_0"}, + "bgludgvy_namespaces_1": {variable: "${namespaces}", valType: 0, value: "bgludgvy_namespaces_1"}, + "bgludgvy_namespaces_2": {variable: "[[namespaces]]", valType: 0, value: "bgludgvy_namespaces_2"}, + "bgludgvy_timeFilter_0": {variable: "$timeFilter", valType: 0, value: "bgludgvy_timeFilter_0"}, + "bgludgvy_timeFilter_1": {variable: "${timeFilter}", valType: 0, value: "bgludgvy_timeFilter_1"}, + "bgludgvy_timeFilter_2": {variable: "[[timeFilter]]", valType: 0, value: "bgludgvy_timeFilter_2"}, + "bgludgvy_var_0": {variable: "$var", valType: 0, value: "bgludgvy_var_0"}, + "bgludgvy_var_1": {variable: "${var}", valType: 0, value: "bgludgvy_var_1"}, + "bgludgvy_var_2": {variable: "[[var]]", valType: 0, value: "bgludgvy_var_2"}, + "bgludgvy_variable_csv_0": {variable: "${variable:csv}", valType: 0, value: "bgludgvy_variable_csv_0"}, + "bgludgvy_variable_doublequote_0": {variable: "${variable:doublequote}", valType: 0, value: "bgludgvy_variable_doublequote_0"}, + "bgludgvy_variable_glob_0": {variable: "${variable:glob}", valType: 0, value: "bgludgvy_variable_glob_0"}, + "bgludgvy_variable_json_0": {variable: "${variable:json}", valType: 0, value: "bgludgvy_variable_json_0"}, + "bgludgvy_variable_lucene_0": {variable: "${variable:lucene}", valType: 0, value: "bgludgvy_variable_lucene_0"}, "bgludgvy_variable_percentencode_0": {variable: "${variable:percentencode}", valType: 0, value: "bgludgvy_variable_percentencode_0"}, - "bgludgvy___org.name_0": {variable: "$__org.name", valType: 0, value: "bgludgvy___org.name_0"}, - "11277986": {variable: "${resolution}", valType: 1, value: "11277986"}, - "11277970": {variable: "$__interval_ms", valType: 1, value: "11277970"}, - "11277975": {variable: "[[__range_ms]]", valType: 1, value: "11277975"}, - "1294671549257.000": {variable: "$__to", valType: 2, value: "1294671549257.000"}, - "1294671549265.000": {variable: "[[__user.id]]", valType: 2, value: "1294671549265.000"}, - "11277968": {variable: "${__interval}", valType: 1, value: "11277968"}, - "1294671549261.000": {variable: "${__org}", valType: 2, value: "1294671549261.000"}, - "1294671549267.000": {variable: "${__from:date}", valType: 2, value: "1294671549267.000"}, - "11277982": {variable: "$interval", valType: 1, value: "11277982"}, + "bgludgvy_variable_pipe_0": {variable: "${variable:pipe}", valType: 0, value: "bgludgvy_variable_pipe_0"}, + "bgludgvy_variable_queryparam_0": {variable: "${variable:queryparam}", valType: 0, value: "bgludgvy_variable_queryparam_0"}, + "bgludgvy_variable_raw_0": {variable: "${variable:raw}", valType: 0, value: "bgludgvy_variable_raw_0"}, + "bgludgvy_variable_regex_0": {variable: "${variable:regex}", valType: 0, value: "bgludgvy_variable_regex_0"}, + "bgludgvy_variable_singlequote_0": {variable: "${variable:singlequote}", valType: 0, value: "bgludgvy_variable_singlequote_0"}, + "bgludgvy_variable_sqlstring_0": {variable: "${variable:sqlstring}", valType: 0, value: "bgludgvy_variable_sqlstring_0"}, + "bgludgvy_variable_text_0": {variable: "${variable:text}", valType: 0, value: "bgludgvy_variable_text_0"}, } for _, tc := range []struct { desc string @@ -409,7 +425,7 @@ func TestReverseVariableExpansion(t *testing.T) { }, { desc: "Should replace variables present in the templating", - expr: "max by(bgludgvy_var_0) (rate(cpu{}[130d12h46m22s:130d12h46m25s]))", + expr: "max by(bgludgvy_var_0) (rate(cpu{}[130d12h46m22s:130d12h46m28s]))", result: "max by($var) (rate(cpu{}[$interval:$resolution]))", }, { From 09e53e8e089fc078da24ec9b6986b1c02df7517f Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:09:48 +0100 Subject: [PATCH 24/26] Support cases where variable is matchers Signed-off-by: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> --- lint/variables.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lint/variables.go b/lint/variables.go index b537829..b5b0578 100644 --- a/lint/variables.go +++ b/lint/variables.go @@ -38,6 +38,7 @@ const ( valTypeString valType = iota valTypeTimeRange valTypeEpoch + valTypeMatcher ) type valType int @@ -300,6 +301,11 @@ func getValueType(value string) valType { if _, err := strconv.ParseFloat(value, 64); err == nil { return valTypeEpoch } + // naive check if variable is a matcher + if strings.Contains(value, "=") && strings.Contains(value, "\"") { + return valTypeMatcher + } + // default to string return valTypeString } @@ -324,6 +330,10 @@ func createPlaceholder(variable string, valType valType) string { // trim epoch to 3 decimal places since that is the precision used in prometheus value = fmt.Sprintf("%.3f", epoch) } + if valType == valTypeMatcher { + part := fmt.Sprintf("%s_%s_%d", magicString, trimVariableSyntax(variable), counter) + value = fmt.Sprintf(`%s="%s"`, part, part) + } if valType == valTypeString { value = fmt.Sprintf("%s_%s_%d", magicString, trimVariableSyntax(variable), counter) } From c2e61716a38b20205ed06c50cce499bc1e9e2a85 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:17:40 +0100 Subject: [PATCH 25/26] Add hacky check and override configured match type if needed Signed-off-by: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> --- lint/rule_target_required_matchers.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go index 072cf14..4c96fc9 100644 --- a/lint/rule_target_required_matchers.go +++ b/lint/rule_target_required_matchers.go @@ -35,7 +35,16 @@ func NewTargetRequiredMatchersRule(config *TargetRequiredMatchersRuleSettings) * if config != nil { for _, m := range config.Matchers { for _, selector := range parser.ExtractSelectors(expr) { - if err := checkForMatcher(selector, m.Name, labels.MatchType(m.Type), m.Value); err != nil { + // Check if the template variable would require a matcher to be regexp... + mType := labels.MatchType(m.Type) + for _, v := range d.Templating.List { + if fmt.Sprintf("$%s", v.Name) == m.Value { + if v.Multi || v.AllValue != "" { + mType = labels.MatchRegexp + } + } + } + if err := checkForMatcher(selector, m.Name, mType, m.Value); err != nil { r.AddFixableError(d, p, t, fmt.Sprintf("invalid PromQL query '%s': %v", t.Expr, err), fixTargetRequiredMatcherRule(m.Name, labels.MatchType(m.Type), m.Value)) } } From 89661c5addc798925715013f85261b4fbb9e1746 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:19:39 +0100 Subject: [PATCH 26/26] mType should also be used when autofix Signed-off-by: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> --- lint/rule_target_required_matchers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go index 4c96fc9..9f27743 100644 --- a/lint/rule_target_required_matchers.go +++ b/lint/rule_target_required_matchers.go @@ -45,7 +45,7 @@ func NewTargetRequiredMatchersRule(config *TargetRequiredMatchersRuleSettings) * } } if err := checkForMatcher(selector, m.Name, mType, m.Value); err != nil { - r.AddFixableError(d, p, t, fmt.Sprintf("invalid PromQL query '%s': %v", t.Expr, err), fixTargetRequiredMatcherRule(m.Name, labels.MatchType(m.Type), m.Value)) + r.AddFixableError(d, p, t, fmt.Sprintf("invalid PromQL query '%s': %v", t.Expr, err), fixTargetRequiredMatcherRule(m.Name, mType, m.Value)) } } }