diff --git a/cmd/alert.go b/cmd/alert.go index 4e2bbd3..542532b 100644 --- a/cmd/alert.go +++ b/cmd/alert.go @@ -4,12 +4,14 @@ import ( "errors" "fmt" "regexp" + "slices" "strings" "github.com/NETWAYS/check_prometheus/internal/alert" "github.com/NETWAYS/go-check" "github.com/NETWAYS/go-check/perfdata" "github.com/NETWAYS/go-check/result" + "github.com/prometheus/common/model" "github.com/spf13/cobra" ) @@ -17,23 +19,14 @@ type AlertConfig struct { AlertName []string Group []string ExcludeAlerts []string + ExcludeLabels []string + IncludeLabels []string ProblemsOnly bool NoAlertsState string } var cliAlertConfig AlertConfig -func contains(s string, list []string) bool { - // Tiny helper to see if a string is in a list of strings - for _, elem := range list { - if s == elem { - return true - } - } - - return false -} - var alertCmd = &cobra.Command{ Use: "alert", Short: "Checks the status of a Prometheus alert", @@ -115,7 +108,7 @@ inactive = 0`, // If it's not the Alert we're looking for, Skip! if cliAlertConfig.AlertName != nil { - if !contains(rl.AlertingRule.Name, cliAlertConfig.AlertName) { + if !slices.Contains(cliAlertConfig.AlertName, rl.AlertingRule.Name) { continue } } @@ -125,17 +118,31 @@ inactive = 0`, continue } - alertMatched, regexErr := matches(rl.AlertingRule.Name, cliAlertConfig.ExcludeAlerts) + alertMatchedExclude, regexErr := matches(rl.AlertingRule.Name, cliAlertConfig.ExcludeAlerts) if regexErr != nil { check.ExitRaw(check.Unknown, "Invalid regular expression provided:", regexErr.Error()) } - if alertMatched { + if alertMatchedExclude { // If the alert matches a regex from the list we can skip it. continue } + labelsMatchedInclude := matchesLabel(rl.AlertingRule.Labels, cliAlertConfig.IncludeLabels) + + if !labelsMatchedInclude { + // If the alert labels don't match here we can skip it. + continue + } + + labelsMatchedExclude := matchesLabel(rl.AlertingRule.Labels, cliAlertConfig.ExcludeLabels) + + if len(cliAlertConfig.ExcludeLabels) > 0 && labelsMatchedExclude { + // If the alert labels matches here we can skip it. + continue + } + // Handle Inactive Alerts if len(rl.AlertingRule.Alerts) == 0 { // Counting states for perfdata @@ -218,18 +225,27 @@ func init() { fs.StringVarP(&cliAlertConfig.NoAlertsState, "no-alerts-state", "T", "OK", "State to assign when no alerts are found (0, 1, 2, 3, OK, WARNING, CRITICAL, UNKNOWN). If not set this defaults to OK") - fs.StringArrayVar(&cliAlertConfig.ExcludeAlerts, "exclude-alert", []string{}, "Alerts to ignore. Can be used multiple times and supports regex.") + fs.StringArrayVar(&cliAlertConfig.ExcludeAlerts, "exclude-alert", []string{}, + "Alerts to ignore. Can be used multiple times and supports regex.") fs.StringSliceVarP(&cliAlertConfig.AlertName, "name", "n", nil, "The name of one or more specific alerts to check."+ - "\nThis parameter can be repeated e.G.: '--name alert1 --name alert2'"+ + "\nThis parameter can be repeated e.g.: '--name alert1 --name alert2'"+ "\nIf no name is given, all alerts will be evaluated") fs.StringSliceVarP(&cliAlertConfig.Group, "group", "g", nil, "The name of one or more specific groups to check for alerts."+ - "\nThis parameter can be repeated e.G.: '--group group1 --group group2'"+ + "\nThis parameter can be repeated e.g.: '--group group1 --group group2'"+ "\nIf no group is given, all groups will be scanned for alerts") + fs.StringArrayVar(&cliAlertConfig.IncludeLabels, "include-label", []string{}, + "The label of one or more specific alerts to include."+ + "\nThis parameter can be repeated e.g.: '--include-label prio=high --include-label another=example'") + + fs.StringArrayVar(&cliAlertConfig.ExcludeLabels, "exclude-label", []string{}, + "The label of one or more specific alerts to exclude."+ + "\nThis parameter can be repeated e.g.: '--exclude-label prio=high --exclude-label another=example'") + fs.BoolVarP(&cliAlertConfig.ProblemsOnly, "problems", "P", false, "Display only alerts which status is not inactive/OK. Note that in combination with the --name flag this might result in no alerts being displayed") } @@ -267,3 +283,39 @@ func matches(input string, regexToExclude []string) (bool, error) { return false, nil } + +// Matches a list of regular expressions against a string. +func matchesLabel(labels model.LabelSet, labelsToExclude []string) bool { + kv := sliceToMap(labelsToExclude) + + for k, v := range kv { + lname := model.LabelName(k) + + lv, ok := labels[lname] + + if !ok { + return false + } + + if string(lv) != v { + return false + } + } + + return true +} + +func sliceToMap(labels []string) map[string]string { + m := make(map[string]string, len(labels)) + + for _, s := range labels { + kv := strings.SplitN(s, "=", 2) + if len(kv) != 2 { + continue + } + + m[kv[0]] = kv[1] + } + + return m +}