diff --git a/cmd/pint/scan.go b/cmd/pint/scan.go index f859c9ef..01686397 100644 --- a/cmd/pint/scan.go +++ b/cmd/pint/scan.go @@ -49,6 +49,12 @@ func checkRules(ctx context.Context, workers int, cfg config.Config, entries []d results := make(chan reporter.Report, workers*5) wg := sync.WaitGroup{} + for _, s := range cfg.Check { + settings, _ := s.Decode() + key := checks.SettingsKey(s.Name) + ctx = context.WithValue(ctx, key, settings) + } + for w := 1; w <= workers; w++ { wg.Add(1) go func() { diff --git a/cmd/pint/tests/0025_config.txt b/cmd/pint/tests/0025_config.txt index 4f6721a8..9e9ed9c4 100644 --- a/cmd/pint/tests/0025_config.txt +++ b/cmd/pint/tests/0025_config.txt @@ -36,8 +36,21 @@ level=info msg="Loading configuration file" path=.pint.hcl "promql/series", "rule/label", "rule/reject" + ], + "disabled": [ + "promql/fragile" ] }, + "check": [ + { + "ignoreMetrics": [ + ".*_error", + ".*_error_.*", + ".*_errors", + ".*_errors_.*" + ] + } + ], "rules": [ {}, { @@ -111,9 +124,6 @@ level=info msg="Loading configuration file" path=.pint.hcl "bytesPerSample": 4036 } } - ], - "PrometheusServers": [ - {} ] } -- .pint.hcl -- @@ -123,6 +133,19 @@ prometheus "prom" { required = true } +checks { + disabled = ["promql/fragile"] +} + +check "promql/series" { + ignoreMetrics = [ + ".*_error", + ".*_error_.*", + ".*_errors", + ".*_errors_.*", + ] +} + rule{ } rule { diff --git a/cmd/pint/tests/0079_check_promql_series_invalid.txt b/cmd/pint/tests/0079_check_promql_series_invalid.txt new file mode 100644 index 00000000..8096f030 --- /dev/null +++ b/cmd/pint/tests/0079_check_promql_series_invalid.txt @@ -0,0 +1,22 @@ +pint.error --no-color config +! stdout . +cmp stderr stderr.txt + +-- stderr.txt -- +level=info msg="Loading configuration file" path=.pint.hcl +level=fatal msg="Fatal error" error="failed to load config file \".pint.hcl\": .pint.hcl:8,3-6: Unsupported argument; An argument named \"bob\" is not expected here." +-- .pint.hcl -- +prometheus "prom" { + uri = "https://" + timeout = "2m" + required = true +} + +check "promql/series" { + bob = [ + ".*_error", + ".*_error_.*", + ".*_errors", + ".*_errors_.*", + ] +} diff --git a/cmd/pint/tests/0080_lint_online.txt b/cmd/pint/tests/0080_lint_online.txt new file mode 100644 index 00000000..24600413 --- /dev/null +++ b/cmd/pint/tests/0080_lint_online.txt @@ -0,0 +1,122 @@ +exec bash -x ./prometheus.sh & +exec bash -c 'I=0 ; while [ ! -f prometheus.pid ] && [ $I -lt 30 ]; do sleep 1; I=$((I+1)); done' + +pint.ok --no-color lint rules +! stdout . +cmp stderr stderr.txt +exec bash -c 'cat prometheus.pid | xargs kill' + +-- stderr.txt -- +level=info msg="Loading configuration file" path=.pint.hcl +level=info msg="File parsed" path=rules/1.yml rules=1 +rules/1.yml:2: prometheus "prom1" at http://127.0.0.1:7080 didn't have any series for "http_errors_total" metric in the last 1w. Metric name "http_errors_total" matches "promql/series" check ignore regexp "^.+_errors_.+$" (promql/series) + expr: rate(http_errors_total[2m]) > 0 + +level=info msg="Problems found" Warning=1 +-- rules/1.yml -- +- alert: http errors + expr: rate(http_errors_total[2m]) > 0 + +-- .pint.hcl -- +prometheus "prom1" { + uri = "http://127.0.0.1:7080" + timeout = "5s" + required = true +} +parser { + relaxed = [".*"] +} +check "promql/series" { + ignoreMetrics = [ + ".+_error", + ".+_error_.+", + ".+_errors", + ".+_errors_.+", + ] +} + +-- prometheus.go -- +package main + +import ( + "context" + "log" + "net" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" +) + +func main() { + http.HandleFunc("/api/v1/metadata", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":"success","data":{}}`)) + }) + + http.HandleFunc("/api/v1/status/config", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}}`)) + }) + + http.HandleFunc("/api/v1/query_range", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "status":"success", + "data":{ + "resultType":"matrix", + "result":[] + } + }`)) + }) + + http.HandleFunc("/api/v1/query", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "status":"success", + "data":{ + "resultType":"vector", + "result":[] + } + }`)) + }) + + listener, err := net.Listen("tcp", "127.0.0.1:7080") + if err != nil { + log.Fatal(err) + } + + server := &http.Server{ + Addr: "127.0.0.1:7080", + } + + go func() { + _ = server.Serve(listener) + }() + + pid := os.Getpid() + err = os.WriteFile("prometheus.pid", []byte(strconv.Itoa(pid)), 0644) + if err != nil { + log.Fatal(err) + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + go func() { + time.Sleep(time.Minute * 2) + stop <- syscall.SIGTERM + }() + <-stop + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + server.Shutdown(ctx) +} + +-- prometheus.sh -- +env GOCACHE=$TMPDIR go run prometheus.go diff --git a/docs/changelog.md b/docs/changelog.md index f53c0cae..a2cd8727 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # Changelog +## v0.23.0 + +### Added + +- Added extra global configuration for `promql/series` check. + See check [documentation](checks/promql/series.md) for details. + ## v0.22.2 ### Fixed diff --git a/docs/checks/promql/series.md b/docs/checks/promql/series.md index 7182708a..3447e546 100644 --- a/docs/checks/promql/series.md +++ b/docs/checks/promql/series.md @@ -121,8 +121,32 @@ that. ## Configuration -This check doesn't have any configuration options but it supports a few control -comments that can be placed around each rule. +This check supports setting extra configuration option to fine tune its behavior. + +Syntax: + +```js +check "promql/series" { + ignoreMetrics = [ "(.*)", ... ] +} +``` + +- `ignoreMetrics` - list of regexp matchers, if a metric is missing from Prometheus + but the name matches any of provided regexp matchers then pint will only report a + warning, instead of a bug level report. + +Example: + +```js +check "promql/series" { + ignoreMetrics = [ + ".*_error", + ".*_error_.*", + ".*_errors", + ".*_errors_.*", + ] +} +``` ### min-age diff --git a/docs/examples/ignore_error_metrics.hcl b/docs/examples/ignore_error_metrics.hcl new file mode 100644 index 00000000..f86c2081 --- /dev/null +++ b/docs/examples/ignore_error_metrics.hcl @@ -0,0 +1,23 @@ +# Define "prod" Prometheus instance that will only be used for +# rules defined in file matching "alerting/prod/.+" or "recording/prod/.+". +prometheus "prod" { + uri = "https://prod.example.com" + timeout = "30s" + + paths = [ + "alerting/prod/.+", + "recording/prod/.+", + ] +} + +# Extra global configuration for the promql/series check. +check "promql/series" { + # Don't report missing metrics for any metric with name matching + # one of the regexp matchers below. + ignoreMetrics = [ + ".+_error", + ".+_error_.+", + ".+_errors", + ".+_errors_.+", + ] +} diff --git a/internal/checks/base.go b/internal/checks/base.go index 9f5e1f2b..2e222317 100644 --- a/internal/checks/base.go +++ b/internal/checks/base.go @@ -83,6 +83,8 @@ const ( Fatal ) +type SettingsKey string + type Problem struct { Fragment string Lines []int diff --git a/internal/checks/base_test.go b/internal/checks/base_test.go index 5d93fce1..ea9be677 100644 --- a/internal/checks/base_test.go +++ b/internal/checks/base_test.go @@ -82,10 +82,13 @@ type problemsFn func(string) []checks.Problem type newPrometheusFn func(string) *promapi.FailoverGroup +type newCtxFn func() context.Context + type checkTest struct { description string content string prometheus newPrometheusFn + ctx newCtxFn checker newCheckFn entries []discovery.Entry problems problemsFn @@ -121,7 +124,11 @@ func runTests(t *testing.T, testCases []checkTest) { entries, err := parseContent(tc.content) require.NoError(t, err, "cannot parse rule content") for _, entry := range entries { - problems := tc.checker(prom).Check(context.Background(), entry.Rule, tc.entries) + ctx := context.Background() + if tc.ctx != nil { + ctx = tc.ctx() + } + problems := tc.checker(prom).Check(ctx, entry.Rule, tc.entries) require.Equal(t, tc.problems(uri), problems) } diff --git a/internal/checks/promql_series.go b/internal/checks/promql_series.go index d05ad6a8..35b23a22 100644 --- a/internal/checks/promql_series.go +++ b/internal/checks/promql_series.go @@ -3,6 +3,7 @@ package checks import ( "context" "fmt" + "regexp" "time" "github.com/rs/zerolog/log" @@ -17,6 +18,23 @@ import ( promParser "github.com/prometheus/prometheus/promql/parser" ) +type PromqlSeriesSettings struct { + IgnoreMetrics []string `hcl:"ignoreMetrics,optional" json:"ignoreMetrics,omitempty"` + ignoreMetricsRe []*regexp.Regexp +} + +func (c *PromqlSeriesSettings) Validate() error { + for _, re := range c.IgnoreMetrics { + re, err := regexp.Compile("^" + re + "$") + if err != nil { + return err + } + c.ignoreMetricsRe = append(c.ignoreMetricsRe, re) + } + + return nil +} + const ( SeriesCheckName = "promql/series" ) @@ -38,6 +56,11 @@ func (c SeriesCheck) Reporter() string { } func (c SeriesCheck) Check(ctx context.Context, rule parser.Rule, entries []discovery.Entry) (problems []Problem) { + var settings *PromqlSeriesSettings + if s := ctx.Value(SettingsKey(c.Reporter())); s != nil { + settings = s.(*PromqlSeriesSettings) + } + expr := rule.Expr() if expr.SyntaxError != nil { @@ -172,13 +195,19 @@ func (c SeriesCheck) Check(ctx context.Context, rule parser.Rule, entries []disc continue } + text, severity := c.textAndSeverity( + settings, + bareSelector.String(), + fmt.Sprintf("%s didn't have any series for %q metric in the last %s", + promText(c.prom.Name(), trs.uri), bareSelector.String(), trs.sinceDesc(trs.from)), + Bug, + ) problems = append(problems, Problem{ Fragment: bareSelector.String(), Lines: expr.Lines(), Reporter: c.Reporter(), - Text: fmt.Sprintf("%s didn't have any series for %q metric in the last %s", - promText(c.prom.Name(), trs.uri), bareSelector.String(), trs.sinceDesc(trs.from)), - Severity: Bug, + Text: text, + Severity: severity, }) log.Debug().Str("check", c.Reporter()).Stringer("selector", &bareSelector).Msg("No historical series for base metric") continue @@ -239,14 +268,20 @@ func (c SeriesCheck) Check(ctx context.Context, rule parser.Rule, entries []disc continue } + text, severity := c.textAndSeverity( + settings, + bareSelector.String(), + fmt.Sprintf( + "%s doesn't currently have %q, it was last present %s ago", + promText(c.prom.Name(), trs.uri), bareSelector.String(), trs.sinceDesc(trs.newest())), + Bug, + ) problems = append(problems, Problem{ Fragment: bareSelector.String(), Lines: expr.Lines(), Reporter: c.Reporter(), - Text: fmt.Sprintf( - "%s doesn't currently have %q, it was last present %s ago", - promText(c.prom.Name(), trs.uri), bareSelector.String(), trs.sinceDesc(trs.newest())), - Severity: Bug, + Text: text, + Severity: severity, }) log.Debug().Str("check", c.Reporter()).Stringer("selector", &bareSelector).Msg("Series disappeared from prometheus") continue @@ -289,6 +324,7 @@ func (c SeriesCheck) Check(ctx context.Context, rule parser.Rule, entries []disc } } + text, s = c.textAndSeverity(settings, bareSelector.String(), text, s) problems = append(problems, Problem{ Fragment: selector.String(), Lines: expr.Lines(), @@ -320,14 +356,20 @@ func (c SeriesCheck) Check(ctx context.Context, rule parser.Rule, entries []disc continue } + text, severity := c.textAndSeverity( + settings, + bareSelector.String(), + fmt.Sprintf( + "%s has %q metric but doesn't currently have series matching {%s}, such series was last present %s ago", + promText(c.prom.Name(), trs.uri), bareSelector.String(), lm.String(), trsLabel.sinceDesc(trsLabel.newest())), + Bug, + ) problems = append(problems, Problem{ Fragment: labelSelector.String(), Lines: expr.Lines(), Reporter: c.Reporter(), - Text: fmt.Sprintf( - "%s has %q metric but doesn't currently have series matching {%s}, such series was last present %s ago", - promText(c.prom.Name(), trs.uri), bareSelector.String(), lm.String(), trsLabel.sinceDesc(trsLabel.newest())), - Severity: Bug, + Text: text, + Severity: severity, }) log.Debug().Str("check", c.Reporter()).Stringer("selector", &selector).Stringer("matcher", lm).Msg("Series matching filter disappeared from prometheus ") continue @@ -506,6 +548,18 @@ func (c SeriesCheck) isLabelValueIgnored(rule parser.Rule, selector promParser.V return false } +func (c SeriesCheck) textAndSeverity(settings *PromqlSeriesSettings, name, text string, s Severity) (string, Severity) { + if settings != nil { + for _, re := range settings.ignoreMetricsRe { + if name != "" && re.MatchString(name) { + log.Debug().Str("check", c.Reporter()).Str("metric", name).Stringer("regexp", re).Msg("Metric matches check ignore rules") + return fmt.Sprintf("%s. Metric name %q matches %q check ignore regexp %q", text, name, c.Reporter(), re), Warning + } + } + } + return text, s +} + func getSelectors(n *parser.PromQLNode) (selectors []promParser.VectorSelector) { if node, ok := n.Node.(*promParser.VectorSelector); ok { // copy node without offset diff --git a/internal/checks/promql_series_test.go b/internal/checks/promql_series_test.go index c99c2bee..998cbed4 100644 --- a/internal/checks/promql_series_test.go +++ b/internal/checks/promql_series_test.go @@ -1,6 +1,7 @@ package checks_test import ( + "context" "fmt" "testing" "time" @@ -59,6 +60,10 @@ func alertMissing(metric, alertname string) string { return fmt.Sprintf("%s metric is generated by alerts but didn't found any rule named %q", metric, alertname) } +func metricIgnored(metric, check, re string) string { + return fmt.Sprintf("Metric name %q matches %q check ignore regexp %q", metric, check, re) +} + func TestSeriesCheck(t *testing.T) { testCases := []checkTest{ { @@ -506,6 +511,43 @@ func TestSeriesCheck(t *testing.T) { }, }, }, + { + description: "#2 series never present but metric ignored", + content: "- record: foo\n expr: sum(notfound)\n", + checker: newSeriesCheck, + ctx: func() context.Context { + s := checks.PromqlSeriesSettings{ + IgnoreMetrics: []string{"foo", "bar", "not.+"}, + } + if err := s.Validate(); err != nil { + t.Error(err) + t.FailNow() + } + return context.WithValue(context.Background(), checks.SettingsKey(checks.SeriesCheckName), &s) + }, + prometheus: newSimpleProm, + problems: func(uri string) []checks.Problem { + return []checks.Problem{ + { + Fragment: "notfound", + Lines: []int{2}, + Reporter: checks.SeriesCheckName, + Text: noMetricText("prom", uri, "notfound", "1w") + ". " + metricIgnored("notfound", checks.SeriesCheckName, "^not.+$"), + Severity: checks.Warning, + }, + } + }, + mocks: []*prometheusMock{ + { + conds: []requestCondition{requireQueryPath}, + resp: respondWithEmptyVector(), + }, + { + conds: []requestCondition{requireRangeQueryPath}, + resp: respondWithEmptyMatrix(), + }, + }, + }, { description: "#3 metric present, label missing", content: "- record: foo\n expr: sum(found{job=\"foo\", notfound=\"xxx\"})\n", @@ -768,6 +810,97 @@ func TestSeriesCheck(t *testing.T) { }, }, }, + { + description: "#4 metric was present but disappeared over 1h ago / ignored", + content: "- record: foo\n expr: sum(found{job=\"foo\", instance=\"bar\"})\n", + checker: newSeriesCheck, + ctx: func() context.Context { + s := checks.PromqlSeriesSettings{ + IgnoreMetrics: []string{"foo", "found", "not.+"}, + } + if err := s.Validate(); err != nil { + t.Error(err) + t.FailNow() + } + return context.WithValue(context.Background(), checks.SettingsKey(checks.SeriesCheckName), &s) + }, + prometheus: newSimpleProm, + problems: func(uri string) []checks.Problem { + return []checks.Problem{ + { + Fragment: `found`, + Lines: []int{2}, + Reporter: checks.SeriesCheckName, + Text: seriesDisappearedText("prom", uri, "found", "4d") + ". " + metricIgnored("found", checks.SeriesCheckName, "^found$"), + Severity: checks.Warning, + }, + } + }, + mocks: []*prometheusMock{ + { + conds: []requestCondition{ + requireQueryPath, + formCond{key: "query", value: `count(found{instance="bar",job="foo"})`}, + }, + resp: respondWithEmptyVector(), + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found)`}, + }, + resp: matrixResponse{ + samples: []*model.SampleStream{ + generateSampleStream( + map[string]string{}, + time.Now().Add(time.Hour*24*-7), + time.Now().Add(time.Hour*24*-4).Add(time.Minute*-5), + time.Minute*5, + ), + }, + }, + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found{job=~".+"}) by (job)`}, + }, + resp: matrixResponse{ + samples: []*model.SampleStream{ + generateSampleStream( + map[string]string{"job": "foo"}, + time.Now().Add(time.Hour*24*-7), + time.Now().Add(time.Hour*24*-4).Add(time.Minute*-5), + time.Minute*5, + ), + }, + }, + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found{instance=~".+"}) by (instance)`}, + }, + resp: matrixResponse{ + samples: []*model.SampleStream{ + generateSampleStream( + map[string]string{"instance": "bar"}, + time.Now().Add(time.Hour*24*-7), + time.Now().Add(time.Hour*24*-4).Add(time.Minute*-5), + time.Minute*5, + ), + }, + }, + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: "count(up)"}, + }, + resp: respondWithSingleRangeVector1W(), + }, + }, + }, { description: "#4 metric was present but disappeared / min-age / ok", content: ` @@ -1181,7 +1314,128 @@ func TestSeriesCheck(t *testing.T) { }, }, { - description: "#5 metric was present but not with label", + description: "#5 metric was present but not with label value", + content: "- record: foo\n expr: sum(found{notfound=\"notfound\", instance=~\".+\", not!=\"negative\", instance!~\"bad\"})\n", + checker: newSeriesCheck, + ctx: func() context.Context { + s := checks.PromqlSeriesSettings{ + IgnoreMetrics: []string{"foo", "bar", "found"}, + } + if err := s.Validate(); err != nil { + t.Error(err) + t.FailNow() + } + return context.WithValue(context.Background(), checks.SettingsKey(checks.SeriesCheckName), &s) + }, + prometheus: newSimpleProm, + problems: func(uri string) []checks.Problem { + return []checks.Problem{ + { + Fragment: `found{instance!~"bad",instance=~".+",not!="negative",notfound="notfound"}`, + Lines: []int{2}, + Reporter: checks.SeriesCheckName, + Text: noFilterMatchText("prom", uri, "found", "notfound", `{notfound="notfound"}`, "1w") + ". " + metricIgnored("found", checks.SeriesCheckName, "^found$"), + Severity: checks.Warning, + }, + } + }, + mocks: []*prometheusMock{ + { + conds: []requestCondition{ + requireQueryPath, + formCond{key: "query", value: `count(found{instance!~"bad",instance=~".+",not!="negative",notfound="notfound"})`}, + }, + resp: respondWithEmptyVector(), + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found)`}, + }, + resp: respondWithSingleRangeVector1W(), + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found{instance=~".+"}) by (instance)`}, + }, + resp: matrixResponse{ + samples: []*model.SampleStream{ + generateSampleStream( + map[string]string{"instance": "bar"}, + time.Now().Add(time.Hour*24*-7), + time.Now(), + time.Minute*5, + ), + }, + }, + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found{not=~".+"}) by (not)`}, + }, + resp: matrixResponse{ + samples: []*model.SampleStream{ + generateSampleStream( + map[string]string{"not": "yyy"}, + time.Now().Add(time.Hour*24*-7), + time.Now(), + time.Minute*5, + ), + }, + }, + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found{notfound=~".+"}) by (notfound)`}, + }, + resp: matrixResponse{ + samples: []*model.SampleStream{ + generateSampleStream( + map[string]string{"notfound": "found"}, + time.Now().Add(time.Hour*24*-7), + time.Now(), + time.Minute*5, + ), + }, + }, + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found{instance=~".+"})`}, + }, + resp: matrixResponse{ + samples: []*model.SampleStream{ + generateSampleStream( + map[string]string{"instance": "bar"}, + time.Now().Add(time.Hour*24*-7), + time.Now(), + time.Minute*5, + ), + }, + }, + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: `count(found{notfound="notfound"})`}, + }, + resp: respondWithEmptyMatrix(), + }, + { + conds: []requestCondition{ + requireRangeQueryPath, + formCond{key: "query", value: "count(up)"}, + }, + resp: respondWithSingleRangeVector1W(), + }, + }, + }, + { + description: "#5 metric was present but not with label value / ignored metric", content: "- record: foo\n expr: sum(found{notfound=\"notfound\", instance=~\".+\", not!=\"negative\", instance!~\"bad\"})\n", checker: newSeriesCheck, prometheus: newSimpleProm, diff --git a/internal/config/__snapshots__/config_test.snap b/internal/config/__snapshots__/config_test.snap index d7d5ebb5..ea9db2eb 100755 --- a/internal/config/__snapshots__/config_test.snap +++ b/internal/config/__snapshots__/config_test.snap @@ -24,8 +24,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": null + } } --- @@ -63,10 +62,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -107,10 +103,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -151,10 +144,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -205,11 +195,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -241,8 +227,7 @@ }, "rules": [ {} - ], - "PrometheusServers": null + ] } --- @@ -292,8 +277,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -343,8 +327,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -382,8 +365,7 @@ "severity": "warning" } } - ], - "PrometheusServers": null + ] } --- @@ -441,10 +423,6 @@ "bytesPerSample": 4096 } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -519,8 +497,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -593,10 +570,6 @@ "severity": "bug" } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -646,8 +619,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -697,8 +669,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -748,8 +719,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -799,8 +769,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -850,8 +819,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -901,8 +869,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -952,8 +919,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -1003,8 +969,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -1058,9 +1023,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -1093,9 +1055,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -1133,9 +1092,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -1178,9 +1134,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -1223,9 +1176,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -1274,11 +1224,7 @@ "disabled": [ "alerts/template" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -1320,10 +1266,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -1367,8 +1310,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -1412,8 +1354,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -1457,8 +1398,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -1502,8 +1442,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -1547,8 +1486,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -1592,8 +1530,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -1622,8 +1559,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": null + } } --- @@ -1661,10 +1597,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -1706,10 +1639,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -1757,11 +1687,7 @@ "disabled": [ "alerts/template" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -1802,10 +1728,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -1846,10 +1769,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -1900,11 +1820,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -1936,8 +1852,7 @@ }, "rules": [ {} - ], - "PrometheusServers": null + ] } --- @@ -1987,8 +1902,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2038,8 +1952,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2077,8 +1990,7 @@ "severity": "warning" } } - ], - "PrometheusServers": null + ] } --- @@ -2136,10 +2048,6 @@ "bytesPerSample": 4096 } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -2214,8 +2122,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2288,10 +2195,6 @@ "severity": "bug" } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -2341,8 +2244,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2392,8 +2294,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2443,8 +2344,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2494,8 +2394,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2545,8 +2444,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2596,8 +2494,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2647,8 +2544,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2698,8 +2594,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -2753,9 +2648,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -2788,9 +2680,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -2828,9 +2717,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -2873,9 +2759,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -2918,9 +2801,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -2965,8 +2845,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3010,8 +2889,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3055,8 +2933,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3100,8 +2977,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3145,8 +3021,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3190,8 +3065,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3220,8 +3094,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": null + } } --- @@ -3259,10 +3132,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -3304,10 +3174,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -3355,11 +3222,7 @@ "disabled": [ "alerts/template" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -3400,10 +3263,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -3444,10 +3304,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -3498,11 +3355,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -3534,8 +3387,7 @@ }, "rules": [ {} - ], - "PrometheusServers": null + ] } --- @@ -3585,8 +3437,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3636,8 +3487,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3675,8 +3525,7 @@ "severity": "warning" } } - ], - "PrometheusServers": null + ] } --- @@ -3734,10 +3583,6 @@ "bytesPerSample": 4096 } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -3812,8 +3657,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3886,10 +3730,6 @@ "severity": "bug" } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -3939,8 +3779,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -3990,8 +3829,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4041,8 +3879,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4092,8 +3929,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4143,8 +3979,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4194,8 +4029,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4245,8 +4079,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4296,8 +4129,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4351,9 +4183,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -4386,9 +4215,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -4426,9 +4252,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -4471,9 +4294,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -4516,9 +4336,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -4563,8 +4380,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4608,8 +4424,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4653,8 +4468,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4698,8 +4512,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4743,8 +4556,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4788,8 +4600,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -4818,8 +4629,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": null + } } --- @@ -4857,10 +4667,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -4902,10 +4709,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -4953,11 +4757,7 @@ "disabled": [ "alerts/template" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -4998,10 +4798,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -5042,10 +4839,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -5096,11 +4890,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -5132,8 +4922,7 @@ }, "rules": [ {} - ], - "PrometheusServers": null + ] } --- @@ -5183,8 +4972,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5234,8 +5022,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5273,8 +5060,7 @@ "severity": "warning" } } - ], - "PrometheusServers": null + ] } --- @@ -5332,10 +5118,6 @@ "bytesPerSample": 4096 } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -5410,8 +5192,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5484,10 +5265,6 @@ "severity": "bug" } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -5537,8 +5314,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5588,8 +5364,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5639,8 +5414,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5690,8 +5464,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5741,8 +5514,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5792,8 +5564,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5843,8 +5614,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5894,8 +5664,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -5949,9 +5718,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -5984,9 +5750,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -6024,9 +5787,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -6069,9 +5829,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -6114,9 +5871,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -6161,8 +5915,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -6206,8 +5959,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -6251,8 +6003,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -6296,8 +6047,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -6341,8 +6091,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -6386,8 +6135,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -6416,8 +6164,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": null + } } --- @@ -6455,10 +6202,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -6500,10 +6244,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -6551,11 +6292,7 @@ "disabled": [ "alerts/template" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -6596,10 +6333,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -6640,10 +6374,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {} - ] + } } --- @@ -6694,11 +6425,7 @@ "rule/label", "rule/reject" ] - }, - "PrometheusServers": [ - {}, - {} - ] + } } --- @@ -6730,8 +6457,7 @@ }, "rules": [ {} - ], - "PrometheusServers": null + ] } --- @@ -6781,8 +6507,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -6832,8 +6557,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -6871,8 +6595,7 @@ "severity": "warning" } } - ], - "PrometheusServers": null + ] } --- @@ -6930,10 +6653,6 @@ "bytesPerSample": 4096 } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -7008,8 +6727,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7082,10 +6800,6 @@ "severity": "bug" } } - ], - "PrometheusServers": [ - {}, - {} ] } --- @@ -7135,8 +6849,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7186,8 +6899,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7237,8 +6949,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7288,8 +6999,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7339,8 +7049,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7390,8 +7099,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7441,8 +7149,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7492,8 +7199,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7547,9 +7253,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -7582,9 +7285,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -7622,9 +7322,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -7667,9 +7364,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -7712,9 +7406,6 @@ "resolve": "5m" } } - ], - "PrometheusServers": [ - {} ] } --- @@ -7759,8 +7450,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7804,8 +7494,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7849,8 +7538,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7894,8 +7582,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7939,8 +7626,7 @@ } ] } - ], - "PrometheusServers": null + ] } --- @@ -7984,7 +7670,6 @@ } ] } - ], - "PrometheusServers": null + ] } --- diff --git a/internal/config/check.go b/internal/config/check.go new file mode 100644 index 00000000..bfd97ab9 --- /dev/null +++ b/internal/config/check.go @@ -0,0 +1,53 @@ +package config + +import ( + "encoding/json" + "fmt" + + "github.com/cloudflare/pint/internal/checks" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" +) + +type Check struct { + Name string `hcl:",label" json:"name"` + Body hcl.Body `hcl:",remain" json:"-"` +} + +func (c Check) MarshalJSON() ([]byte, error) { + s, err := c.Decode() + if err != nil { + return nil, err + } + return json.MarshalIndent(s, "", " ") +} + +func (c Check) Decode() (s CheckSettings, err error) { + switch c.Name { + case checks.SeriesCheckName: + s = &checks.PromqlSeriesSettings{} + default: + return nil, fmt.Errorf("unknown check %q", c.Name) + } + + if diag := gohcl.DecodeBody(c.Body, nil, s); diag != nil && diag.HasErrors() { + return nil, diag + } + if err = s.Validate(); err != nil { + return nil, err + } + return s, nil +} + +func (c Check) validate() error { + s, err := c.Decode() + if err != nil { + return err + } + return s.Validate() +} + +type CheckSettings interface { + Validate() error +} diff --git a/internal/config/config.go b/internal/config/config.go index b24caef1..e03af3e6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,13 +17,14 @@ import ( ) type Config struct { - CI *CI `hcl:"ci,block" json:"ci,omitempty"` - Parser *Parser `hcl:"parser,block" json:"parser,omitempty"` - Repository *Repository `hcl:"repository,block" json:"repository,omitempty"` - Prometheus []PrometheusConfig `hcl:"prometheus,block" json:"prometheus,omitempty"` - Checks *Checks `hcl:"checks,block" json:"checks,omitempty"` - Rules []Rule `hcl:"rule,block" json:"rules,omitempty"` - PrometheusServers []*promapi.FailoverGroup + CI *CI `hcl:"ci,block" json:"ci,omitempty"` + Parser *Parser `hcl:"parser,block" json:"parser,omitempty"` + Repository *Repository `hcl:"repository,block" json:"repository,omitempty"` + Prometheus []PrometheusConfig `hcl:"prometheus,block" json:"prometheus,omitempty"` + Checks *Checks `hcl:"checks,block" json:"checks,omitempty"` + Check []Check `hcl:"check,block" json:"check,omitempty"` + Rules []Rule `hcl:"rule,block" json:"rules,omitempty"` + PrometheusServers []*promapi.FailoverGroup `json:"-"` } func (cfg *Config) DisableOnlineChecks() { @@ -216,6 +217,12 @@ func Load(path string, failOnMissing bool) (cfg Config, err error) { } } + for _, chk := range cfg.Check { + if err = chk.validate(); err != nil { + return cfg, err + } + } + for i, prom := range cfg.Prometheus { if err = prom.validate(); err != nil { return cfg, err diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 593cefdd..991ae10c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1282,6 +1282,18 @@ func TestConfigErrors(t *testing.T) { }`, err: "error parsing regexp: invalid nested repetition operator: `++`", }, + { + config: `check "bob" {}`, + err: `unknown check "bob"`, + }, + { + config: `check "promql/series " {}`, + err: `unknown check "promql/series "`, + }, + { + config: `check "promql/series" { ignoreMetrics = [".+++"] }`, + err: "error parsing regexp: invalid nested repetition operator: `++`", + }, } dir := t.TempDir()