diff --git a/cmd/thanos/check.go b/cmd/thanos/check.go new file mode 100644 index 00000000000..8730eaa75b4 --- /dev/null +++ b/cmd/thanos/check.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/go-kit/kit/log" + thanosrule "github.com/improbable-eng/thanos/pkg/rule" + "github.com/oklog/run" + "github.com/opentracing/opentracing-go" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/prometheus/pkg/rulefmt" + "gopkg.in/alecthomas/kingpin.v2" + "gopkg.in/yaml.v2" +) + +func registerChecks(m map[string]setupFunc, app *kingpin.Application, name string) { + cmd := app.Command(name, "Linting tools for Thanos") + registerRulesCheck(m, cmd, name) + return +} + +func registerRulesCheck(m map[string]setupFunc, root *kingpin.CmdClause, name string) { + checkRulesCmd := root.Command("rules", "Check if the rule files are valid or not.") + ruleFiles := checkRulesCmd.Arg( + "rule-files", + "The rule files to check.", + ).Required().ExistingFiles() + + m[name+" rules"] = func(g *run.Group, logger log.Logger, reg *prometheus.Registry, _ opentracing.Tracer, _ bool) error { + // Dummy actor to immediately kill the group after the run function returns. + g.Add(func() error { return nil }, func(error) {}) + return checkRulesFiles(ruleFiles) + } +} + +func checkRulesFiles(files *[]string) error { + var failed error + + for _, f := range *files { + n, errs := checkRules(f) + if errs != nil { + fmt.Fprintln(os.Stderr, " FAILED:") + for _, e := range errs { + fmt.Fprintln(os.Stderr, e.Error()) + } + fmt.Println() + failed = fmt.Errorf("Errors: %v\n\n", errs) + continue + } + fmt.Printf(" SUCCESS: %d rules found\n\n", n) + } + if failed != nil { + return failed + } + return nil +} + +func checkRules(filename string) (int, []error) { + fmt.Println("Checking", filename) + + b, err := ioutil.ReadFile(filename) + if err != nil { + return 0, []error{err} + } + + var rgs thanosrule.RuleGroups + if err := yaml.Unmarshal(b, &rgs); err != nil { + return 0, []error{err} + } + + // We need to convert Thanos rules to Prometheus rules so we can use their validation + promRgs := thanosRuleGroupsToPromRuleGroups(rgs) + if errs := promRgs.Validate(); errs != nil { + return 0, errs + } + + numRules := 0 + for _, rg := range rgs.Groups { + numRules += len(rg.Rules) + } + + return numRules, nil +} + +func thanosRuleGroupsToPromRuleGroups(ruleGroups thanosrule.RuleGroups) rulefmt.RuleGroups { + promRuleGroups := rulefmt.RuleGroups{Groups: []rulefmt.RuleGroup{}} + for _, g := range ruleGroups.Groups { + group := rulefmt.RuleGroup{ + Name: g.Name, + Interval: g.Interval, + Rules: []rulefmt.Rule{}, + } + for _, r := range g.Rules { + group.Rules = append( + group.Rules, + rulefmt.Rule{ + Record: r.Record, + Alert: r.Alert, + Expr: r.Expr, + For: r.For, + Labels: r.Labels, + Annotations: r.Annotations, + }, + ) + } + promRuleGroups.Groups = append( + promRuleGroups.Groups, + group, + ) + } + return promRuleGroups +} diff --git a/cmd/thanos/check_test.go b/cmd/thanos/check_test.go new file mode 100644 index 00000000000..069feeae753 --- /dev/null +++ b/cmd/thanos/check_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "testing" + + "github.com/improbable-eng/thanos/pkg/testutil" +) + +func Test_checkRules(t *testing.T) { + + validFiles := []string{ + "./testdata/rules-files/valid.yaml", + } + + invalidFiles := [][]string{ + []string{"./testdata/rules-files/non-existing-file.yaml"}, + []string{"./testdata/rules-files/invalid-yaml-format.yaml"}, + []string{"./testdata/rules-files/invalid-rules-data.yaml"}, + } + + testutil.Ok(t, checkRulesFiles(&validFiles)) + + for _, fn := range invalidFiles { + testutil.NotOk(t, checkRulesFiles(&fn)) + } +} diff --git a/cmd/thanos/main.go b/cmd/thanos/main.go index 2dbbad88a33..063836530a6 100644 --- a/cmd/thanos/main.go +++ b/cmd/thanos/main.go @@ -79,6 +79,7 @@ func main() { registerBucket(cmds, app, "bucket") registerDownsample(cmds, app, "downsample") registerReceive(cmds, app, "receive") + registerChecks(cmds, app, "check") cmd, err := app.Parse(os.Args[1:]) if err != nil { diff --git a/cmd/thanos/testdata/rules-files/invalid-rules-data.yaml b/cmd/thanos/testdata/rules-files/invalid-rules-data.yaml new file mode 100644 index 00000000000..575783d7166 --- /dev/null +++ b/cmd/thanos/testdata/rules-files/invalid-rules-data.yaml @@ -0,0 +1,12 @@ +groups: + - name: null + partial_response_strategy: "warn" + interval: 2m + rules: + - alert: TestAlert + partial_response_strategy: "warn" + expr: 1 + labels: + key: value + annotations: + key: value diff --git a/cmd/thanos/testdata/rules-files/invalid-yaml-format.yaml b/cmd/thanos/testdata/rules-files/invalid-yaml-format.yaml new file mode 100644 index 00000000000..58efff20109 --- /dev/null +++ b/cmd/thanos/testdata/rules-files/invalid-yaml-format.yaml @@ -0,0 +1,3 @@ +groups: + - name: test + invalid_yaml_reason diff --git a/cmd/thanos/testdata/rules-files/valid.yaml b/cmd/thanos/testdata/rules-files/valid.yaml new file mode 100644 index 00000000000..c160a6f337d --- /dev/null +++ b/cmd/thanos/testdata/rules-files/valid.yaml @@ -0,0 +1,20 @@ +groups: + - name: test-alert-group + partial_response_strategy: "warn" + interval: 2m + rules: + - alert: TestAlert + partial_response_strategy: "warn" + expr: 1 + labels: + key: value + annotations: + key: value + + - name: test-rule-group + partial_response_strategy: "warn" + interval: 2m + rules: + - record: test_metric + expr: 1 + partial_response_strategy: "warn"