From 44e27a876ec278f0e06ae297f81116bda5295d12 Mon Sep 17 00:00:00 2001 From: Subhramit Basu Date: Mon, 26 May 2025 16:29:34 +0000 Subject: [PATCH] Add parse alerting for rules files (#16601) Builds over https://github.com/prometheus/prometheus/pull/16462 Addresses comments, adds invalid rules file Signed-off-by: subhramit Co-authored-by: marcodebba --- cmd/prometheus/main.go | 11 +++++++++++ rules/fixtures/invalid_rules.yaml | 6 ++++++ rules/manager.go | 30 ++++++++++++++++++++++++++++++ rules/manager_test.go | 12 ++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 rules/fixtures/invalid_rules.yaml diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index 6bf9778f0c..3e2bee26e4 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -648,6 +648,17 @@ func main() { logger.Error(fmt.Sprintf("Error loading dynamic scrape config files from config (--config.file=%q)", cfg.configFile), "file", absPath, "err", err) os.Exit(2) } + + // Parse rule files to verify they exist and contain valid rules. + if err := rules.ParseFiles(cfgFile.RuleFiles); err != nil { + absPath, pathErr := filepath.Abs(cfg.configFile) + if pathErr != nil { + absPath = cfg.configFile + } + logger.Error(fmt.Sprintf("Error loading rule file patterns from config (--config.file=%q)", cfg.configFile), "file", absPath, "err", err) + os.Exit(2) + } + if cfg.tsdb.EnableExemplarStorage { if cfgFile.StorageConfig.ExemplarsConfig == nil { cfgFile.StorageConfig.ExemplarsConfig = &config.DefaultExemplarsConfig diff --git a/rules/fixtures/invalid_rules.yaml b/rules/fixtures/invalid_rules.yaml new file mode 100644 index 0000000000..a531033c73 --- /dev/null +++ b/rules/fixtures/invalid_rules.yaml @@ -0,0 +1,6 @@ +groups: + - name: invalid + rules: + - record: job:http_requests:rate5m + expr: sum by (job)(rate(http_requests_total[5m])) + unexpected_field: this_field_should_not_be_here diff --git a/rules/manager.go b/rules/manager.go index a38be82ebe..fafbe2dc43 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -19,6 +19,7 @@ import ( "fmt" "log/slog" "net/url" + "path/filepath" "slices" "strings" "sync" @@ -579,3 +580,32 @@ func FromMaps(maps ...map[string]string) labels.Labels { return labels.FromMap(mLables) } + +// ParseFiles parses the rule files corresponding to glob patterns. +func ParseFiles(patterns []string) error { + files := map[string]string{} + for _, pat := range patterns { + fns, err := filepath.Glob(pat) + if err != nil { + return fmt.Errorf("failed retrieving rule files for %q: %w", pat, err) + } + for _, fn := range fns { + absPath, err := filepath.Abs(fn) + if err != nil { + absPath = fn + } + cleanPath, err := filepath.EvalSymlinks(absPath) + if err != nil { + return fmt.Errorf("failed evaluating rule file path %q (pattern: %q): %w", absPath, pat, err) + } + files[cleanPath] = pat + } + } + for fn, pat := range files { + _, errs := rulefmt.ParseFile(fn, false) + if len(errs) > 0 { + return fmt.Errorf("parse rules from file %q (pattern: %q): %w", fn, pat, errors.Join(errs...)) + } + } + return nil +} diff --git a/rules/manager_test.go b/rules/manager_test.go index 54ca8ebfb3..aa8fed59f8 100644 --- a/rules/manager_test.go +++ b/rules/manager_test.go @@ -20,6 +20,7 @@ import ( "math" "os" "path" + "path/filepath" "slices" "sort" "strconv" @@ -2549,6 +2550,17 @@ func TestLabels_FromMaps(t *testing.T) { require.Equal(t, expected, mLabels, "unexpected labelset") } +func TestParseFiles(t *testing.T) { + t.Run("good files", func(t *testing.T) { + err := ParseFiles([]string{filepath.Join("fixtures", "rules.y*ml")}) + require.NoError(t, err) + }) + t.Run("bad files", func(t *testing.T) { + err := ParseFiles([]string{filepath.Join("fixtures", "invalid_rules.y*ml")}) + require.ErrorContains(t, err, "field unexpected_field not found in type rulefmt.Rule") + }) +} + func TestRuleDependencyController_AnalyseRules(t *testing.T) { type expectedDependencies struct { noDependentRules bool