From b3cc91a62ed7782b9f537d1f7336a0e33cb8dedb Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 1 Aug 2025 15:08:45 +0200 Subject: [PATCH 1/4] Add IDEA exclusion to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1306ab9..c61a877 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /_output/ /mixtool +.idea/ \ No newline at end of file From cac9c46b47b415bbf00e60d080b496e52db8ee38 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 1 Aug 2025 15:10:43 +0200 Subject: [PATCH 2/4] Add `alert-name-length` test ensuring names don't exceed 40 characters Add tests for `alert-name-length` --- pkg/mixer/lint.go | 6 ++++++ pkg/mixer/lint_test.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/pkg/mixer/lint.go b/pkg/mixer/lint.go index 686c4d5..f34f1c4 100644 --- a/pkg/mixer/lint.go +++ b/pkg/mixer/lint.go @@ -115,6 +115,12 @@ func lintPrometheusAlertsGuidelines(rule *rulefmt.RuleNode, cf *lint.Configurati } } + if !isLintExcluded("alert-name-length", rule.Alert.Value, cf) { + if len(rule.Alert.Value) > 40 { + errs = append(errs, fmt.Errorf("[alert-name-length] Alert '%s' name exceeds 40 characters", rule.Alert.Value)) + } + } + if !isLintExcluded("alert-severity-rule", rule.Alert.Value, cf) { if rule.Labels["severity"] != "warning" && rule.Labels["severity"] != "critical" && rule.Labels["severity"] != "info" { errs = append(errs, fmt.Errorf("[alert-severity-rule] Alert '%s' severity must be 'warning', 'critical' or 'info', is currently '%s'", rule.Alert.Value, rule.Labels["severity"])) diff --git a/pkg/mixer/lint_test.go b/pkg/mixer/lint_test.go index d496f93..8acb637 100644 --- a/pkg/mixer/lint_test.go +++ b/pkg/mixer/lint_test.go @@ -121,6 +121,21 @@ var alertTests = []struct { }`, `[alert-name-camelcase] Alert 'test Alert' name is not in camel case`, }, + { + `{ + alert: 'TestAlertNameIsLongerThan40CharactersLong', + expr: 'up == 0', + labels: { + severity: 'warning', + }, + annotations: { + description: '{{ $labels.instance }} has been unready for more than 15 minutes.', + summary: 'Instance is not ready.', + }, + 'for': '15m', + }`, + `[alert-name-length] Alert 'TestAlertNameIsLongerThan40CharactersLong' name exceeds 40 characters`, + }, // severity { From 1b4a8772450a211a8406e0c089abc0d4935911b2 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 15 Aug 2025 15:28:06 +0200 Subject: [PATCH 3/4] Add lint tests for new AlertGroup guidelines Tests the following: - Alert group name does not exceed 40 characters - Alert group array of alerts does not exceed 20 alerts --- pkg/mixer/lint.go | 21 ++++++ pkg/mixer/lint_test.go | 141 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/pkg/mixer/lint.go b/pkg/mixer/lint.go index f34f1c4..3113243 100644 --- a/pkg/mixer/lint.go +++ b/pkg/mixer/lint.go @@ -86,6 +86,10 @@ func lintPrometheus(filename string, vm *jsonnet.VM, errsOut chan<- error) { } for _, g := range groups.Groups { + errs = lintPrometheusAlertGroups(&g, config) + for _, err := range errs { + errsOut <- err + } for _, r := range g.Rules { errs = lintPrometheusAlertsGuidelines(&r, config) for _, err := range errs { @@ -106,6 +110,23 @@ var camelCaseRegexp = regexp.MustCompile(`^([A-Z]+[a-z0-9]+)+$`) var goTemplateRegexp = regexp.MustCompile(`\{\{.+}\}`) var sentenceRegexp = regexp.MustCompile(`^[A-Z].+\.$`) +// Enforces alert group guidelines. +func lintPrometheusAlertGroups(group *rulefmt.RuleGroup, cf *lint.ConfigurationFile) (errs []error) { + if !isLintExcluded("alert-group-rule-count", group.Name, cf) { + if len(group.Rules) > 20 { + errs = append(errs, fmt.Errorf("[alert-group-rule-count] Group '%s' contains more than 20 rules (%d)", group.Name, len(group.Rules))) + } + } + + if !isLintExcluded("alert-group-name-length", group.Name, cf) { + if len(group.Name) > 40 { + errs = append(errs, fmt.Errorf("[alert-group-name-length] Alert Group '%s' name exceeds 40 characters", group.Name)) + } + } + return errs + +} + // Enforces alerting guidelines. // https://monitoring.mixins.dev/#guidelines-for-alert-names-labels-and-annotations func lintPrometheusAlertsGuidelines(rule *rulefmt.RuleNode, cf *lint.ConfigurationFile) (errs []error) { diff --git a/pkg/mixer/lint_test.go b/pkg/mixer/lint_test.go index 8acb637..4bc3b4e 100644 --- a/pkg/mixer/lint_test.go +++ b/pkg/mixer/lint_test.go @@ -280,6 +280,147 @@ func TestLintPrometheusAlertsGuidelines(t *testing.T) { } +func TestLintPrometheusAlertGroupsGuidelines_ValidGroup(t *testing.T) { + expectedLintErr := "" + var validAlert = `{ + alert: 'TestAlert', + expr: 'up == 0', + labels: { + severity: 'warning', + }, + annotations: { + description: '{{ $labels.instance }} has been unready for more than 15 minutes.', + summary: 'Instance is not ready.', + }, + 'for': '15m', + }` + + alertsStr := fmt.Sprintf(` + { + _config+:: {}, + prometheusAlerts+: { + groups+: [ + { + name: 'test', + rules: [ + %s, + ], + }, + ], + }, + } + `, validAlert) + + filename, delete := writeTempFile(t, "alerts.jsonnet", alertsStr) + defer delete() + + vm := jsonnet.MakeVM() + errs := make(chan error) + go lintPrometheus(filename, vm, errs) + for err := range errs { + if err.Error() != expectedLintErr { + t.Errorf("linting wrote unexpected output, expected '%s', got: %v", expectedLintErr, err) + } + } +} + +func TestLintPrometheusAlertGroupsGuidelines_InvalidGroupName(t *testing.T) { + expectedLintErr := "[alert-group-name-length] Alert Group 'InvalidAlertGroupNameHasMoreThanFourtyCharacters' name exceeds 40 characters" + var validAlert = `{ + alert: 'TestAlert', + expr: 'up == 0', + labels: { + severity: 'warning', + }, + annotations: { + description: '{{ $labels.instance }} has been unready for more than 15 minutes.', + summary: 'Instance is not ready.', + }, + 'for': '15m', + }` + + alertsStr := fmt.Sprintf(` + { + _config+:: {}, + prometheusAlerts+: { + groups+: [ + { + name: 'InvalidAlertGroupNameHasMoreThanFourtyCharacters', + rules: [ + %s, + ], + }, + ], + }, + } + `, validAlert) + + filename, delete := writeTempFile(t, "alerts.jsonnet", alertsStr) + defer delete() + + vm := jsonnet.MakeVM() + errs := make(chan error) + go lintPrometheus(filename, vm, errs) + for err := range errs { + if err.Error() != expectedLintErr { + t.Errorf("linting wrote unexpected output, expected '%s', got: %v", expectedLintErr, err) + } + } +} + +func TestLintPrometheusAlertGroupsGuidelines_TooManyAlerts(t *testing.T) { + invalidNumAlerts := 21 + expectedLintErr := "[alert-group-rule-count] Group 'test' contains more than 20 rules (21)" + var validAlert = `{ + alert: 'TestAlert', + expr: 'up == 0', + labels: { + severity: 'warning', + }, + annotations: { + description: '{{ $labels.instance }} has been unready for more than 15 minutes.', + summary: 'Instance is not ready.', + }, + 'for': '15m', + }` + + var joinedAlerts = "" + for i := 0; i < invalidNumAlerts; i++ { + if i > 0 { + joinedAlerts += "," + } + joinedAlerts += validAlert + } + + alertsStr := fmt.Sprintf(` + { + _config+:: {}, + prometheusAlerts+: { + groups+: [ + { + name: 'test', + rules: [ + %s, + ], + }, + ], + }, + } + `, joinedAlerts) + + filename, delete := writeTempFile(t, "alerts.jsonnet", alertsStr) + defer delete() + + vm := jsonnet.MakeVM() + errs := make(chan error) + go lintPrometheus(filename, vm, errs) + for err := range errs { + if err.Error() != expectedLintErr { + t.Errorf("linting wrote unexpected output, expected '%s', got: %v", expectedLintErr, err) + } + } +} + func TestLintPrometheusRules(t *testing.T) { filename, delete := writeTempFile(t, "rules.jsonnet", rules) defer delete() From 5506210052344712dca8647deb5ca3f8d913fe55 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 15 Aug 2025 15:50:58 +0200 Subject: [PATCH 4/4] Trailing newline gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c61a877..ba4b7b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /_output/ /mixtool -.idea/ \ No newline at end of file +.idea/