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 d8b1885862f4074f61bea3dba6d5e121d1e1c54e Mon Sep 17 00:00:00 2001
From: Ryan Geyer <me@ryangeyer.com>
Date: Wed, 6 Nov 2024 11:23:56 -0800
Subject: [PATCH 24/26] clean up go modules

---
 go.mod            |  55 ++++----
 go.sum            | 326 ++++++++++++++++++++++++++++++++++++++++------
 lint/variables.go |   4 +-
 3 files changed, 320 insertions(+), 65 deletions(-)

diff --git a/go.mod b/go.mod
index c1d2be3..a702f0a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,10 @@
 module github.com/grafana/dashboard-linter
 
-go 1.21.0
-
-toolchain go1.22.5
+go 1.23.1
 
 require (
+	github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241106131137-6420b47bdc17
+	github.com/grafana/loki/v3 v3.2.1
 	github.com/prometheus/prometheus v0.54.1
 	github.com/spf13/cobra v1.8.1
 	github.com/spf13/viper v1.19.0
@@ -16,20 +16,32 @@ require (
 
 require (
 	github.com/aws/aws-sdk-go v1.54.19 // indirect
+	github.com/gorilla/mux v1.8.1 // indirect
+	github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b // indirect
+	github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 // indirect
+	github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect
+	github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect
+	github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // 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
+	github.com/prometheus/exporter-toolkit v0.11.0 // indirect
+	go.opentelemetry.io/collector/pdata v1.12.0 // indirect
+	go.uber.org/zap v1.21.0 // indirect
+	go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect
+	golang.org/x/mod v0.19.0 // indirect
+	golang.org/x/tools v0.23.0 // indirect
 )
 
 require (
-	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.4.0 // indirect
-	cloud.google.com/go/iam v1.1.10 // indirect
-	cloud.google.com/go/storage v1.41.0 // indirect
+	cloud.google.com/go v0.115.1 // indirect
+	cloud.google.com/go/auth v0.9.0 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
+	cloud.google.com/go/compute/metadata v0.5.0 // indirect
+	cloud.google.com/go/iam v1.2.0 // indirect
+	cloud.google.com/go/storage v1.43.0 // indirect
 	github.com/BurntSushi/toml v1.3.2 // indirect
 	github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
@@ -68,7 +80,7 @@ require (
 	github.com/google/s2a-go v0.1.8 // 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.5 // indirect
+	github.com/googleapis/gax-go/v2 v2.13.0 // indirect
 	github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
 	github.com/hashicorp/consul/api v1.29.4 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -86,7 +98,6 @@ require (
 	github.com/huandu/xstrings v1.3.3 // indirect
 	github.com/imdario/mergo v0.3.16 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/jpillora/backoff v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.17.9 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
@@ -99,8 +110,6 @@ require (
 	github.com/mitchellh/reflectwalk v1.0.1 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
-	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
 	github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // indirect
 	github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
@@ -134,24 +143,24 @@ require (
 	go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect
 	go.etcd.io/etcd/client/v3 v3.5.12 // 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/google.golang.org/grpc/otelgrpc v0.53.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.25.0 // indirect
+	golang.org/x/crypto v0.26.0 // indirect
 	golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
-	golang.org/x/net v0.27.0 // indirect
-	golang.org/x/oauth2 v0.21.0 // indirect
+	golang.org/x/net v0.28.0 // indirect
+	golang.org/x/oauth2 v0.22.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
-	golang.org/x/sys v0.22.0 // indirect
-	golang.org/x/time v0.5.0 // 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
+	golang.org/x/sys v0.24.0 // indirect
+	golang.org/x/time v0.6.0 // indirect
+	google.golang.org/api v0.193.0 // indirect
+	google.golang.org/genproto v0.0.0-20240820151423-278611b39280 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240820151423-278611b39280 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280 // 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
diff --git a/go.sum b/go.sum
index 4397249..07d978a 100644
--- a/go.sum
+++ b/go.sum
@@ -13,24 +13,26 @@ 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.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 v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
+cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
+cloud.google.com/go/auth v0.9.0 h1:cYhKl1JUhynmxjXfrk4qdPc6Amw7i+GC9VLflgT0p5M=
+cloud.google.com/go/auth v0.9.0/go.mod h1:2HsApZBr9zGZhC9QAXsYVYaWk8kNUt37uny+XVKi7wM=
+cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
+cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 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.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c=
-cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
 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.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI=
-cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps=
+cloud.google.com/go/iam v1.2.0 h1:kZKMKVNk/IsSSc/udOb83K0hL/Yh/Gcqpz+oAkoIFN8=
+cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q=
+cloud.google.com/go/longrunning v0.6.0 h1:mM1ZmaNsQsnb+5n1DNPeL0KwQd9jQRqSqSDEkBZr+aI=
+cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts=
 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,8 +42,8 @@ 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.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0=
-cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=
+cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
+cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
 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.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
@@ -55,6 +57,20 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/Workiva/go-datastructures v1.1.5 h1:5YfhQ4ry7bZc2Mc7R0YZyYwpf5c6t1cEFvdAhd6Mkf4=
+github.com/Workiva/go-datastructures v1.1.5/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -62,11 +78,25 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 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/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
+github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
+github.com/alicebob/miniredis/v2 v2.30.4 h1:8S4/o1/KoUArAGbGwPxcwf0krlzceva2XVOSchFS7Eo=
+github.com/alicebob/miniredis/v2 v2.30.4/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
+github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
 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/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
+github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -75,12 +105,16 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4=
 github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
+github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
@@ -139,8 +173,18 @@ 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-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
+github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
+github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg=
+github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU=
 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
@@ -177,6 +221,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -203,8 +249,9 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
-github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -212,15 +259,82 @@ 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.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
-github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
+github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
+github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b h1:x2HCzk29I0o5pRPfqWP/qwhXaPGlcz8pohq5kO1NZoE=
+github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ=
+github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 h1:X8IKQ0wu40wpvYcKfBcc5T4QnhdQjUhtUtB/1CY89lE=
+github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU=
+github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241106131137-6420b47bdc17 h1:+QqxnP5g2QzomcXrhmwbpHDZUKxvKtwQjgI+fi/UL5Y=
+github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241106131137-6420b47bdc17/go.mod h1:WtWosval1KCZP9BGa42b8aVoJmVXSg0EvQXi9LDSVZQ=
+github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ=
+github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32/go.mod h1:796sq+UcONnSlzA3RtlBZ+b/hrerkZXiEmO8oMjyRwY=
+github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 h1:ZYk42718kSXOiIKdjZKljWLgBpzL5z1yutKABksQCMg=
+github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608/go.mod h1:f3JSoxBTPXX5ec4FxxeC19nTBSxoTz+cBgS3cYLMcr0=
+github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw=
+github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y=
+github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
+github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
 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/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
+github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE=
+github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg=
+github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0=
+github.com/hashicorp/consul/proto-public v0.6.2/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg=
+github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
+github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs=
+github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I=
+github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
+github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
+github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
+github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
+github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
+github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
+github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
+github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -230,12 +344,17 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
 github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 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/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
@@ -253,13 +372,44 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
+github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
+github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
+github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
+github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
+github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 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/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 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=
@@ -294,14 +444,18 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
 github.com/prometheus/alertmanager v0.27.0 h1:V6nTa2J5V4s8TG4C4HtrBP/WNSebCCTYGGv4qecA/+I=
 github.com/prometheus/alertmanager v0.27.0/go.mod h1:8Ia/R3urPmbzJ8OsdvmZvIprDwvwmYCmUbwBL+jlPOE=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
 github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
@@ -313,6 +467,7 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
 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=
@@ -320,8 +475,11 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
 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/exporter-toolkit v0.11.0 h1:yNTsuZ0aNCNFQ3aFTD2uhPOvr4iD7fdBvKPAEGkNf+g=
+github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrPB/OLRIgN74qlQbV+FK1Q=
 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.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
@@ -337,9 +495,17 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
 github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
 github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY=
+github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
+github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -365,6 +531,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
@@ -390,8 +557,19 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
+github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
 github.com/zeitlinger/conflate v0.0.0-20230622100834-279724abda8c h1:PtECnCzGLw8MuQ0tmPRaN5c95ZfNTFZOobvgC6A83zk=
 github.com/zeitlinger/conflate v0.0.0-20230622100834-279724abda8c/go.mod h1:KsJBt1tGR0Q7u+3T7CLN+zITAI06GiXVi/cgP9Xrpb8=
+go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c=
+go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4=
+go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A=
+go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4=
+go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg=
+go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -399,8 +577,10 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 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/collector/pdata v1.12.0 h1:Xx5VK1p4VO0md8MWm2icwC1MnJ7f8EimKItMWw46BmA=
+go.opentelemetry.io/collector/pdata v1.12.0/go.mod h1:MYeB0MmMAxeM0hstCFrCqWLzdyeYySim2dG6pDT6nYI=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
 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=
@@ -411,6 +591,7 @@ go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBq
 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.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 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.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
@@ -419,15 +600,25 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s=
+go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
 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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
-golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
+golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/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=
 golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
@@ -439,6 +630,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
 golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -459,6 +651,10 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
+golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -473,6 +669,8 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -487,18 +685,24 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 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=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
-golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
+golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -507,7 +711,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 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-20201020160332-67f06af15bc9/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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -515,6 +722,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
 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=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -523,11 +731,15 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -545,24 +757,42 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-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/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 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=
 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=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.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/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/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -570,6 +800,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -577,6 +808,7 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -601,15 +833,23 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
+golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
-golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -626,14 +866,15 @@ 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.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
-google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
+google.golang.org/api v0.193.0 h1:eOGDoJFsLU+HpCBaDJex2fWiYujAw9KbXgpOAMePoUs=
+google.golang.org/api v0.193.0/go.mod h1:Po3YMV1XZx+mTku3cfJrlIYR03wiGrCOsdpC67hjZvw=
 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=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -663,12 +904,13 @@ 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-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/genproto v0.0.0-20240820151423-278611b39280 h1:oKt8r1ZvaPqBe3oeGTdyx1iNjuBS+VJcc9QdU1CD3d8=
+google.golang.org/genproto v0.0.0-20240820151423-278611b39280/go.mod h1:wxEc5TmU9JSLs1rSqG4z1YzeSNigp/9yIojIPuZVvKQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20240820151423-278611b39280 h1:YDFM9oOjiFhaMAVgbDxfxW+66nRrsvzQzJ51wp3OxC0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240820151423-278611b39280/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280 h1:XQMA2e105XNlEZ8NRF0HqnUOZzP14sUSsgL09kpdNnU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 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=
@@ -702,11 +944,14 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
 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=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -736,5 +981,6 @@ 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=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/lint/variables.go b/lint/variables.go
index 005bb88..5404dd8 100644
--- a/lint/variables.go
+++ b/lint/variables.go
@@ -17,7 +17,7 @@ const (
 	rangeMs      = "__range_ms"
 	rangeS       = "__range_s"
 	rangeVar     = "__range"
-	dashboard    = "__dashboard"
+	dashboardVar = "__dashboard"
 	from         = "__from"
 	to           = "__to"
 	name         = "__name"
@@ -80,7 +80,7 @@ var globalVariables = []*placeholder{
 		valType:  valTypeTimeRange,
 	},
 	{
-		variable: dashboard,
+		variable: dashboardVar,
 		valType:  valTypeString,
 	},
 	{

From 81dcd35ac5bff152c38c05f6b19e94ec960a9f8e Mon Sep 17 00:00:00 2001
From: Ryan Geyer <me@ryangeyer.com>
Date: Wed, 6 Nov 2024 14:12:03 -0800
Subject: [PATCH 25/26] Patch up LogQL rules to match new stability level, and
 variable substitution

---
 lint/rule_target_logql.go      |  1 +
 lint/rule_target_logql_auto.go | 11 +++---
 lint/rule_target_promql.go     |  2 +-
 lint/rules.go                  |  4 +-
 lint/rules_test.go             |  2 +-
 lint/variables.go              | 69 ++++++++++++----------------------
 lint/variables_test.go         |  2 +-
 7 files changed, 36 insertions(+), 55 deletions(-)

diff --git a/lint/rule_target_logql.go b/lint/rule_target_logql.go
index f8cb168..900fd40 100644
--- a/lint/rule_target_logql.go
+++ b/lint/rule_target_logql.go
@@ -8,6 +8,7 @@ func NewTargetLogQLRule() *TargetRuleFunc {
 	return &TargetRuleFunc{
 		name:        "target-logql-rule",
 		description: "Checks that each target uses a valid LogQL query.",
+		stability:   ruleStabilityExperimental,
 		fn: func(d Dashboard, p Panel, t Target) TargetRuleResults {
 			r := TargetRuleResults{}
 
diff --git a/lint/rule_target_logql_auto.go b/lint/rule_target_logql_auto.go
index 52cdd46..65abfa2 100644
--- a/lint/rule_target_logql_auto.go
+++ b/lint/rule_target_logql_auto.go
@@ -17,14 +17,10 @@ func parseLogQL(expr string, variables []Template) (syntax.Expr, error) {
 }
 
 func NewTargetLogQLAutoRule() *TargetRuleFunc {
-	autoDuration, err := time.ParseDuration(globalVariables["__auto"].(string))
-	if err != nil {
-		panic(err)
-	}
-
 	return &TargetRuleFunc{
 		name:        "target-logql-auto-rule",
 		description: "Checks that each Loki target uses $__auto for range vectors when appropriate.",
+		stability:   ruleStabilityExperimental,
 		fn: func(d Dashboard, p Panel, t Target) TargetRuleResults {
 			r := TargetRuleResults{}
 
@@ -51,6 +47,11 @@ func NewTargetLogQLAutoRule() *TargetRuleFunc {
 				return r
 			}
 
+			autoDuration, err := time.ParseDuration(placeholderByVariable["$__auto"].value)
+			if err != nil {
+				panic(err)
+			}
+
 			parsedExpr, err := parseLogQL(t.Expr, d.Templating.List)
 			if err != nil {
 				r.AddError(d, p, t, fmt.Sprintf("Invalid LogQL query: %v", err))
diff --git a/lint/rule_target_promql.go b/lint/rule_target_promql.go
index 1e13d23..2a3ddbc 100644
--- a/lint/rule_target_promql.go
+++ b/lint/rule_target_promql.go
@@ -23,7 +23,7 @@ func panelHasQueries(p Panel) bool {
 // replacing eg [$__rate_interval] with [5m] so queries parse correctly.
 // We also replace various other Grafana global variables.
 func parsePromQL(expr string, variables []Template) (parser.Expr, error) {
-	expr, err := expandVariables(expr, variables)
+	expr, err := expandPromQlVariables(expr, variables)
 	if err != nil {
 		return nil, fmt.Errorf("could not expand variables: %w", err)
 	}
diff --git a/lint/rules.go b/lint/rules.go
index 1aa74ad..8ac117f 100644
--- a/lint/rules.go
+++ b/lint/rules.go
@@ -186,8 +186,6 @@ func NewRuleSet(experimental bool, ruleSettings ConfigurationRuleSettings) RuleS
 		NewPanelTitleDescriptionRule(),
 		NewPanelUnitsRule(),
 		NewPanelNoTargetsRule(),
-		NewTargetLogQLRule(),
-		NewTargetLogQLAutoRule(),
 		NewTargetPromQLRule(),
 		NewTargetRateIntervalRule(),
 		NewTargetJobRule(),
@@ -198,6 +196,8 @@ func NewRuleSet(experimental bool, ruleSettings ConfigurationRuleSettings) RuleS
 	// Add experimental rules here
 	if experimental {
 		rules = append(rules,
+			NewTargetLogQLRule(),
+			NewTargetLogQLAutoRule(),
 			NewTargetRequiredMatchersRule(ruleSettings.TargetRequiredMatchersRule),
 			NewTemplateRequiredVariablesRule(ruleSettings.TemplateRequiredVariablesRule, ruleSettings.TargetRequiredMatchersRule),
 		)
diff --git a/lint/rules_test.go b/lint/rules_test.go
index ea32a93..7e1b5c6 100644
--- a/lint/rules_test.go
+++ b/lint/rules_test.go
@@ -73,7 +73,7 @@ func TestFixableRules(t *testing.T) {
 	assert.NoError(t, err)
 
 	rule := lint.NewDashboardRuleFunc(
-		"test-fixable-rule", "Test fixable rule",
+		"test-fixable-rule", "Test fixable rule", "stable",
 		func(d lint.Dashboard) lint.DashboardRuleResults {
 			rr := lint.DashboardRuleResults{}
 			rr.AddFixableError(d, "fixing first issue", func(d *lint.Dashboard) {
diff --git a/lint/variables.go b/lint/variables.go
index 5404dd8..d95b9e7 100644
--- a/lint/variables.go
+++ b/lint/variables.go
@@ -11,6 +11,7 @@ import (
 )
 
 const (
+	auto         = "__auto"
 	rateInterval = "__rate_interval"
 	interval     = "__interval"
 	intervalMs   = "__interval_ms"
@@ -38,6 +39,9 @@ const (
 	valTypeString valType = iota
 	valTypeTimeRange
 	valTypeEpoch
+	// This is effectively a valTypeTimeRange, but it will always have 's' appended to it to identify that the duration unit is seconds.
+	// Initially used only for logql which is more strict about requiring the duration unit for range vectors, while promql implies 's' as the unit of duration if not specified.
+	valTypeDuration
 )
 
 type valType int
@@ -123,6 +127,10 @@ var globalVariables = []*placeholder{
 		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...
 	},
+	{
+		variable: auto,
+		valType:  valTypeDuration,
+	},
 }
 
 // var supportedFormatOptions = []string{"csv", "distributed", "doublequote", "glob", "json", "lucene", "percentencode", "pipe", "raw", "regex", "singlequote", "sqlstring", "text", "queryparam"}
@@ -135,7 +143,9 @@ var variableRegexp = regexp.MustCompile(
 	}, "|"),
 )
 
-func expandVariables(expr string, variables []Template) (string, error) {
+// Initializes the global variables if not already initialized.
+// Creates placeholders for every variable found in the expression, returning the expression with placeholders
+func expandVariables(expr string, variables []Template) string {
 	// initialize global variables if not already initialized
 	if !globalVariablesInit {
 		for _, v := range globalVariables {
@@ -174,6 +184,11 @@ func expandVariables(expr string, variables []Template) (string, error) {
 	}
 
 	expr = variableRegexp.ReplaceAllStringFunc(expr, RegexpExpandVariables)
+	return expr
+}
+
+func expandPromQlVariables(expr string, variables []Template) (string, error) {
+	expr = expandVariables(expr, variables)
 
 	// Check if the expression can be parsed
 	_, err := parser.ParseExpr(expr)
@@ -318,6 +333,11 @@ func createPlaceholder(variable string, valType valType) string {
 			timeRange := magicTimeRange + counter
 			value = strconv.Itoa(timeRange)
 		}
+		if valType == valTypeDuration {
+			// Using magicTimeRange as a seed for the placeholder
+			duration := magicTimeRange + counter
+			value = strconv.Itoa(duration) + "s"
+		}
 		if valType == valTypeEpoch {
 			// Using magicEpoch as a seed for the placeholder
 			epoch := magicEpoch + float64(counter)
@@ -397,48 +417,7 @@ func getTemplateVariableValue(v Template) string {
 }
 
 func expandLogQLVariables(expr string, variables []Template) (string, error) {
-	lines := strings.Split(expr, "\n")
-	for i, line := range lines {
-		parts := strings.Split(line, "\"")
-		for j, part := range parts {
-			if j%2 == 1 {
-				// Inside a double quote string, just add it
-				continue
-			}
-
-			// 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 k := 2; k < len(v); k += 2 {
-					if v[k] < 0 {
-						continue
-					}
-					// Replace the match with sample value
-					val, err := variableSampleValue(part[v[k]:v[k+1]], variables)
-					if err != nil {
-						return "", err
-					}
-					// If the variable is within square brackets, remove the '$' prefix
-					if strings.HasPrefix(part[v[0]-1:v[0]], "[") && strings.HasSuffix(part[v[1]:v[1]+1], "]") {
-						val = strings.TrimPrefix(val, "$")
-					}
-					subparts = append(subparts, val)
-				}
-				// Move the start cursor at the end of the current match
-				cursor = v[1]
-			}
-			// Add rest of the string
-			subparts = append(subparts, part[cursor:])
-			// Merge all back into the parts
-			parts[j] = strings.Join(subparts, "")
-		}
-		lines[i] = strings.Join(parts, "\"")
-	}
-	result := strings.Join(lines, "\n")
-	return result, nil
+	expr = expandVariables(expr, variables)
+	// TODO: Use the logql parsing to validate this is parsable logql.
+	return expr, nil
 }
diff --git a/lint/variables_test.go b/lint/variables_test.go
index 495b461..84ae467 100644
--- a/lint/variables_test.go
+++ b/lint/variables_test.go
@@ -203,7 +203,7 @@ func TestVariableExpansion(t *testing.T) {
 			result: "sum (rate(cpu{}[11277982]))",
 		},
 	} {
-		s, err := expandVariables(tc.expr, tc.variables)
+		s, err := expandPromQlVariables(tc.expr, tc.variables)
 		require.Equal(t, tc.err, err)
 		require.Equal(t, tc.result, s, tc.desc)
 	}

From 7b77e1a122950f36286100f3e70f6e69ee81fa48 Mon Sep 17 00:00:00 2001
From: Ryan Geyer <me@ryangeyer.com>
Date: Wed, 6 Nov 2024 14:30:15 -0800
Subject: [PATCH 26/26] Fix up docs

---
 docs/index.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 53 insertions(+)

diff --git a/docs/index.md b/docs/index.md
index d7873ea..b0eabee 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -2,6 +2,24 @@
 All Commands:
 
 [embedmd]:# (_intermediate/help.txt)
+```txt
+A command-line application to lint Grafana dashboards.
+
+Usage:
+  dashboard-linter [flags]
+  dashboard-linter [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  lint        Lint a dashboard
+  rules       Print documentation about each lint rule.
+
+Flags:
+  -h, --help   help for dashboard-linter
+
+Use "dashboard-linter [command] --help" for more information about a command.
+```
 
 ```txt
 A command-line application to lint Grafana dashboards.
@@ -25,6 +43,24 @@ Use "dashboard-linter [command] --help" for more information about a command.
 ## Completion
 
 [embedmd]:# (_intermediate/completion.txt)
+```txt
+Generate the autocompletion script for dashboard-linter for the specified shell.
+See each sub-command's help for details on how to use the generated script.
+
+Usage:
+  dashboard-linter completion [command]
+
+Available Commands:
+  bash        Generate the autocompletion script for bash
+  fish        Generate the autocompletion script for fish
+  powershell  Generate the autocompletion script for powershell
+  zsh         Generate the autocompletion script for zsh
+
+Flags:
+  -h, --help   help for completion
+
+Use "dashboard-linter completion [command] --help" for more information about a command.
+```
 
 ```txt
 Generate the autocompletion script for dashboard-linter for the specified shell.
@@ -48,6 +84,21 @@ Use "dashboard-linter completion [command] --help" for more information about a
 ## Lint
 
 [embedmd]:# (_intermediate/lint.txt)
+```txt
+Returns warnings or errors for dashboard which do not adhere to accepted standards
+
+Usage:
+  dashboard-linter lint [dashboard.json] [flags]
+
+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
+```
 
 ```txt
 Returns warnings or errors for dashboard which do not adhere to accepted standards
@@ -84,6 +135,8 @@ The linter implements the following rules:
 * [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-logql-rule](./rules/target-logql-rule) - `experimental` - Checks that each target uses a valid LogQL query.
+* [target-logql-auto-rule](./rules/target-logql-auto-rule) - `experimental` - Checks that each Loki target uses $__auto for range vectors when appropriate.
 * [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