From e562d2927dd09fe3a5633029653031f2e4b52cd8 Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Wed, 11 Dec 2024 08:22:23 +0100 Subject: [PATCH 1/2] Add tests for empty API reponse --- cmd/alert_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cmd/alert_test.go b/cmd/alert_test.go index c636351..ef17c02 100644 --- a/cmd/alert_test.go +++ b/cmd/alert_test.go @@ -31,6 +31,24 @@ type AlertTest struct { func TestAlertCmd(t *testing.T) { tests := []AlertTest{ + { + name: "alert-none", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"success","data":{"groups":[]}}`)) + })), + args: []string{"run", "../main.go", "alert"}, + expected: "[UNKNOWN] - 0 Alerts: 0 Firing - 0 Pending - 0 Inactive\n\nexit status 3\n", + }, + { + name: "alert-none-with-problems", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"success","data":{"groups":[]}}`)) + })), + args: []string{"run", "../main.go", "alert", "--problems"}, + expected: "[UNKNOWN] - 0 Alerts: 0 Firing - 0 Pending - 0 Inactive\n\nexit status 3\n", + }, { name: "alert-default", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From c4706c16d9685bf2972294907d6e249fa27a5113 Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Mon, 16 Dec 2024 14:47:07 +0100 Subject: [PATCH 2/2] Rework logic when there are no alerts defined Changes default to OK, instead of UNKNOWN. Added a check to see if the API response contains alert rules. If not, we exit early. To add extra flexibility this also adds a --no-alerts-state flag that can be used to set the desired exit state when no alerts are found. --- README.md | 11 +++++----- cmd/alert.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++- cmd/alert_test.go | 22 ++++++++++++++++++-- cmd/config.go | 11 +--------- 4 files changed, 78 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 96af02a..56989ff 100644 --- a/README.md +++ b/README.md @@ -150,11 +150,12 @@ Examples: | total=2 firing=1 pending=0 inactive=1 Flags: - -h, --help help for alert - -n, --name strings The name of one or more specific alerts to check. - This parameter can be repeated e.G.: '--name alert1 --name alert2' - If no name is given, all alerts will be evaluated - -P, --problems Display only alerts which status is not inactive/OK + -h, --help help for alert + -n, --name strings The name of one or more specific alerts to check. + This parameter can be repeated e.G.: '--name alert1 --name alert2' + If no name is given, all alerts will be evaluated + -T, --no-alerts-state string State to assign when no alerts are found (0, 1, 2, 3, OK, WARNING, CRITICAL, UNKNOWN). If not set this defaults to OK (default "OK") + -P, --problems 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 ``` #### Checking all defined alerts diff --git a/cmd/alert.go b/cmd/alert.go index f5bff86..00cd8f7 100644 --- a/cmd/alert.go +++ b/cmd/alert.go @@ -1,7 +1,9 @@ package cmd import ( + "errors" "fmt" + "strings" "github.com/NETWAYS/check_prometheus/internal/alert" "github.com/NETWAYS/go-check" @@ -10,6 +12,15 @@ import ( "github.com/spf13/cobra" ) +type AlertConfig struct { + AlertName []string + Group []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 { @@ -40,6 +51,12 @@ inactive = 0`, \_[CRITICAL] [PrometheusAlertmanagerJobMissing] - Job: [alertmanager] is firing - value: 1.00 | total=2 firing=1 pending=0 inactive=1`, Run: func(_ *cobra.Command, _ []string) { + // Convert --no-alerts-state to integer and validate input + noAlertsState, err := convertStateToInt(cliAlertConfig.NoAlertsState) + if err != nil { + check.ExitError(fmt.Errorf("invalid value for --no-alerts-state: %s", cliAlertConfig.NoAlertsState)) + } + var ( counterFiring int counterPending int @@ -47,7 +64,8 @@ inactive = 0`, ) c := cliConfig.NewClient() - err := c.Connect() + err = c.Connect() + if err != nil { check.ExitError(err) } @@ -65,6 +83,16 @@ inactive = 0`, // Get all rules from all groups into a single list rules := alert.FlattenRules(alerts.Groups) + // If there are no rules we can exit early + if len(rules) == 0 { + // Since the user is expecting the state of a certain alert and + // it that is not present it might be noteworthy. + if cliAlertConfig.AlertName != nil { + check.ExitRaw(check.Unknown, "No such alert defined") + } + check.ExitRaw(noAlertsState, "No alerts defined") + } + // Set initial capacity to reduce memory allocations var l int for _, rl := range rules { @@ -164,11 +192,33 @@ inactive = 0`, func init() { rootCmd.AddCommand(alertCmd) + fs := alertCmd.Flags() + + 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.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'"+ "\nIf no name is given, all alerts will be evaluated") + 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") } + +// Function to convert state to integer. +func convertStateToInt(state string) (int, error) { + state = strings.ToUpper(state) + switch state { + case "OK", "0": + return check.OK, nil + case "WARNING", "1": + return check.Warning, nil + case "CRITICAL", "2": + return check.Critical, nil + case "UNKNOWN", "3": + return check.Unknown, nil + default: + return check.Unknown, errors.New("invalid state") + } +} diff --git a/cmd/alert_test.go b/cmd/alert_test.go index ef17c02..049a9ad 100644 --- a/cmd/alert_test.go +++ b/cmd/alert_test.go @@ -38,7 +38,7 @@ func TestAlertCmd(t *testing.T) { w.Write([]byte(`{"status":"success","data":{"groups":[]}}`)) })), args: []string{"run", "../main.go", "alert"}, - expected: "[UNKNOWN] - 0 Alerts: 0 Firing - 0 Pending - 0 Inactive\n\nexit status 3\n", + expected: "[OK] - No alerts defined\n", }, { name: "alert-none-with-problems", @@ -47,7 +47,25 @@ func TestAlertCmd(t *testing.T) { w.Write([]byte(`{"status":"success","data":{"groups":[]}}`)) })), args: []string{"run", "../main.go", "alert", "--problems"}, - expected: "[UNKNOWN] - 0 Alerts: 0 Firing - 0 Pending - 0 Inactive\n\nexit status 3\n", + expected: "[OK] - No alerts defined\n", + }, + { + name: "alert-none-with-no-state", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"success","data":{"groups":[]}}`)) + })), + args: []string{"run", "../main.go", "alert", "--no-alerts-state", "3"}, + expected: "[UNKNOWN] - No alerts defined\nexit status 3\n", + }, + { + name: "alert-none-with-name", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"success","data":{"groups":[]}}`)) + })), + args: []string{"run", "../main.go", "alert", "--name", "MyPreciousAlert"}, + expected: "[UNKNOWN] - No such alert defined\nexit status 3\n", }, { name: "alert-default", diff --git a/cmd/config.go b/cmd/config.go index dd7ad57..c48483d 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -15,12 +15,6 @@ import ( "github.com/prometheus/common/config" ) -type AlertConfig struct { - AlertName []string - Group []string - ProblemsOnly bool -} - type Config struct { BasicAuth string `env:"CHECK_PROMETHEUS_BASICAUTH"` Bearer string `env:"CHECK_PROMETHEUS_BEARER"` @@ -57,10 +51,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/. ` -var ( - cliConfig Config - cliAlertConfig AlertConfig -) +var cliConfig Config func (c *Config) NewClient() *client.Client { u := url.URL{