Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/_output/
/mixtool
.idea/
27 changes: 27 additions & 0 deletions pkg/mixer/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -115,6 +136,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"]))
Expand Down
156 changes: 156 additions & 0 deletions pkg/mixer/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -265,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()
Expand Down