Skip to content

Commit 3a8bcbe

Browse files
authored
Feat: Add guidelines for alert/alert group name & array size (#233)
* Add IDEA exclusion to .gitignore * Add `alert-name-length` test ensuring names don't exceed 40 characters Add tests for `alert-name-length` * 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 * Trailing newline gitignore
1 parent 1abe34c commit 3a8bcbe

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/_output/
22
/mixtool
3+
.idea/

pkg/mixer/lint.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ func lintPrometheus(filename string, vm *jsonnet.VM, errsOut chan<- error) {
8686
}
8787

8888
for _, g := range groups.Groups {
89+
errs = lintPrometheusAlertGroups(&g, config)
90+
for _, err := range errs {
91+
errsOut <- err
92+
}
8993
for _, r := range g.Rules {
9094
errs = lintPrometheusAlertsGuidelines(&r, config)
9195
for _, err := range errs {
@@ -106,6 +110,23 @@ var camelCaseRegexp = regexp.MustCompile(`^([A-Z]+[a-z0-9]+)+$`)
106110
var goTemplateRegexp = regexp.MustCompile(`\{\{.+}\}`)
107111
var sentenceRegexp = regexp.MustCompile(`^[A-Z].+\.$`)
108112

113+
// Enforces alert group guidelines.
114+
func lintPrometheusAlertGroups(group *rulefmt.RuleGroup, cf *lint.ConfigurationFile) (errs []error) {
115+
if !isLintExcluded("alert-group-rule-count", group.Name, cf) {
116+
if len(group.Rules) > 20 {
117+
errs = append(errs, fmt.Errorf("[alert-group-rule-count] Group '%s' contains more than 20 rules (%d)", group.Name, len(group.Rules)))
118+
}
119+
}
120+
121+
if !isLintExcluded("alert-group-name-length", group.Name, cf) {
122+
if len(group.Name) > 40 {
123+
errs = append(errs, fmt.Errorf("[alert-group-name-length] Alert Group '%s' name exceeds 40 characters", group.Name))
124+
}
125+
}
126+
return errs
127+
128+
}
129+
109130
// Enforces alerting guidelines.
110131
// https://monitoring.mixins.dev/#guidelines-for-alert-names-labels-and-annotations
111132
func lintPrometheusAlertsGuidelines(rule *rulefmt.RuleNode, cf *lint.ConfigurationFile) (errs []error) {
@@ -115,6 +136,12 @@ func lintPrometheusAlertsGuidelines(rule *rulefmt.RuleNode, cf *lint.Configurati
115136
}
116137
}
117138

139+
if !isLintExcluded("alert-name-length", rule.Alert.Value, cf) {
140+
if len(rule.Alert.Value) > 40 {
141+
errs = append(errs, fmt.Errorf("[alert-name-length] Alert '%s' name exceeds 40 characters", rule.Alert.Value))
142+
}
143+
}
144+
118145
if !isLintExcluded("alert-severity-rule", rule.Alert.Value, cf) {
119146
if rule.Labels["severity"] != "warning" && rule.Labels["severity"] != "critical" && rule.Labels["severity"] != "info" {
120147
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"]))

pkg/mixer/lint_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,21 @@ var alertTests = []struct {
121121
}`,
122122
`[alert-name-camelcase] Alert 'test Alert' name is not in camel case`,
123123
},
124+
{
125+
`{
126+
alert: 'TestAlertNameIsLongerThan40CharactersLong',
127+
expr: 'up == 0',
128+
labels: {
129+
severity: 'warning',
130+
},
131+
annotations: {
132+
description: '{{ $labels.instance }} has been unready for more than 15 minutes.',
133+
summary: 'Instance is not ready.',
134+
},
135+
'for': '15m',
136+
}`,
137+
`[alert-name-length] Alert 'TestAlertNameIsLongerThan40CharactersLong' name exceeds 40 characters`,
138+
},
124139

125140
// severity
126141
{
@@ -265,6 +280,147 @@ func TestLintPrometheusAlertsGuidelines(t *testing.T) {
265280

266281
}
267282

283+
func TestLintPrometheusAlertGroupsGuidelines_ValidGroup(t *testing.T) {
284+
expectedLintErr := ""
285+
var validAlert = `{
286+
alert: 'TestAlert',
287+
expr: 'up == 0',
288+
labels: {
289+
severity: 'warning',
290+
},
291+
annotations: {
292+
description: '{{ $labels.instance }} has been unready for more than 15 minutes.',
293+
summary: 'Instance is not ready.',
294+
},
295+
'for': '15m',
296+
}`
297+
298+
alertsStr := fmt.Sprintf(`
299+
{
300+
_config+:: {},
301+
prometheusAlerts+: {
302+
groups+: [
303+
{
304+
name: 'test',
305+
rules: [
306+
%s,
307+
],
308+
},
309+
],
310+
},
311+
}
312+
`, validAlert)
313+
314+
filename, delete := writeTempFile(t, "alerts.jsonnet", alertsStr)
315+
defer delete()
316+
317+
vm := jsonnet.MakeVM()
318+
errs := make(chan error)
319+
go lintPrometheus(filename, vm, errs)
320+
for err := range errs {
321+
if err.Error() != expectedLintErr {
322+
t.Errorf("linting wrote unexpected output, expected '%s', got: %v", expectedLintErr, err)
323+
}
324+
}
325+
}
326+
327+
func TestLintPrometheusAlertGroupsGuidelines_InvalidGroupName(t *testing.T) {
328+
expectedLintErr := "[alert-group-name-length] Alert Group 'InvalidAlertGroupNameHasMoreThanFourtyCharacters' name exceeds 40 characters"
329+
var validAlert = `{
330+
alert: 'TestAlert',
331+
expr: 'up == 0',
332+
labels: {
333+
severity: 'warning',
334+
},
335+
annotations: {
336+
description: '{{ $labels.instance }} has been unready for more than 15 minutes.',
337+
summary: 'Instance is not ready.',
338+
},
339+
'for': '15m',
340+
}`
341+
342+
alertsStr := fmt.Sprintf(`
343+
{
344+
_config+:: {},
345+
prometheusAlerts+: {
346+
groups+: [
347+
{
348+
name: 'InvalidAlertGroupNameHasMoreThanFourtyCharacters',
349+
rules: [
350+
%s,
351+
],
352+
},
353+
],
354+
},
355+
}
356+
`, validAlert)
357+
358+
filename, delete := writeTempFile(t, "alerts.jsonnet", alertsStr)
359+
defer delete()
360+
361+
vm := jsonnet.MakeVM()
362+
errs := make(chan error)
363+
go lintPrometheus(filename, vm, errs)
364+
for err := range errs {
365+
if err.Error() != expectedLintErr {
366+
t.Errorf("linting wrote unexpected output, expected '%s', got: %v", expectedLintErr, err)
367+
}
368+
}
369+
}
370+
371+
func TestLintPrometheusAlertGroupsGuidelines_TooManyAlerts(t *testing.T) {
372+
invalidNumAlerts := 21
373+
expectedLintErr := "[alert-group-rule-count] Group 'test' contains more than 20 rules (21)"
374+
var validAlert = `{
375+
alert: 'TestAlert',
376+
expr: 'up == 0',
377+
labels: {
378+
severity: 'warning',
379+
},
380+
annotations: {
381+
description: '{{ $labels.instance }} has been unready for more than 15 minutes.',
382+
summary: 'Instance is not ready.',
383+
},
384+
'for': '15m',
385+
}`
386+
387+
var joinedAlerts = ""
388+
for i := 0; i < invalidNumAlerts; i++ {
389+
if i > 0 {
390+
joinedAlerts += ","
391+
}
392+
joinedAlerts += validAlert
393+
}
394+
395+
alertsStr := fmt.Sprintf(`
396+
{
397+
_config+:: {},
398+
prometheusAlerts+: {
399+
groups+: [
400+
{
401+
name: 'test',
402+
rules: [
403+
%s,
404+
],
405+
},
406+
],
407+
},
408+
}
409+
`, joinedAlerts)
410+
411+
filename, delete := writeTempFile(t, "alerts.jsonnet", alertsStr)
412+
defer delete()
413+
414+
vm := jsonnet.MakeVM()
415+
errs := make(chan error)
416+
go lintPrometheus(filename, vm, errs)
417+
for err := range errs {
418+
if err.Error() != expectedLintErr {
419+
t.Errorf("linting wrote unexpected output, expected '%s', got: %v", expectedLintErr, err)
420+
}
421+
}
422+
}
423+
268424
func TestLintPrometheusRules(t *testing.T) {
269425
filename, delete := writeTempFile(t, "rules.jsonnet", rules)
270426
defer delete()

0 commit comments

Comments
 (0)