// Copyright 2018 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package v1 import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/httptest" "regexp" "testing" "time" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" ) // fakeAlerts is a struct implementing the provider.Alerts interface for tests. type fakeAlerts struct { fps map[model.Fingerprint]int alerts []*types.Alert err error } func newFakeAlerts(alerts []*types.Alert, withErr bool) *fakeAlerts { fps := make(map[model.Fingerprint]int) for i, a := range alerts { fps[a.Fingerprint()] = i } f := &fakeAlerts{ alerts: alerts, fps: fps, } if withErr { f.err = errors.New("error occurred") } return f } func (f *fakeAlerts) Subscribe() provider.AlertIterator { return nil } func (f *fakeAlerts) Get(model.Fingerprint) (*types.Alert, error) { return nil, nil } func (f *fakeAlerts) Put(alerts ...*types.Alert) error { return f.err } func (f *fakeAlerts) GetPending() provider.AlertIterator { ch := make(chan *types.Alert) done := make(chan struct{}) go func() { defer close(ch) for _, a := range f.alerts { ch <- a } }() return provider.NewAlertIterator(ch, done, f.err) } func newGetAlertStatus(f *fakeAlerts) func(model.Fingerprint) types.AlertStatus { return func(fp model.Fingerprint) types.AlertStatus { status := types.AlertStatus{SilencedBy: []string{}, InhibitedBy: []string{}} i, ok := f.fps[fp] if !ok { return status } alert := f.alerts[i] switch alert.Labels["state"] { case "active": status.State = types.AlertStateActive case "unprocessed": status.State = types.AlertStateUnprocessed case "suppressed": status.State = types.AlertStateSuppressed } if alert.Labels["silenced_by"] != "" { status.SilencedBy = append(status.SilencedBy, string(alert.Labels["silenced_by"])) } if alert.Labels["inhibited_by"] != "" { status.InhibitedBy = append(status.InhibitedBy, string(alert.Labels["inhibited_by"])) } return status } } func TestAddAlerts(t *testing.T) { now := func(offset int) time.Time { return time.Now().Add(time.Duration(offset) * time.Second) } for i, tc := range []struct { start, end time.Time err bool code int }{ {time.Time{}, time.Time{}, false, 200}, {now(0), time.Time{}, false, 200}, {time.Time{}, now(-1), false, 200}, {time.Time{}, now(0), false, 200}, {time.Time{}, now(1), false, 200}, {now(-2), now(-1), false, 200}, {now(1), now(2), false, 200}, {now(1), now(0), false, 400}, {now(0), time.Time{}, true, 500}, } { alerts := []model.Alert{{ StartsAt: tc.start, EndsAt: tc.end, Labels: model.LabelSet{"label1": "test1"}, Annotations: model.LabelSet{"annotation1": "some text"}, }} b, err := json.Marshal(&alerts) if err != nil { t.Errorf("Unexpected error %v", err) } alertsProvider := newFakeAlerts([]*types.Alert{}, tc.err) api := New(alertsProvider, nil, newGetAlertStatus(alertsProvider), nil, nil, nil) defaultGlobalConfig := config.DefaultGlobalConfig() route := config.Route{} api.Update(&config.Config{ Global: &defaultGlobalConfig, Route: &route, }) r, err := http.NewRequest("POST", "/api/v1/alerts", bytes.NewReader(b)) w := httptest.NewRecorder() if err != nil { t.Errorf("Unexpected error %v", err) } api.addAlerts(w, r) res := w.Result() body, _ := io.ReadAll(res.Body) require.Equal(t, tc.code, w.Code, fmt.Sprintf("test case: %d, StartsAt %v, EndsAt %v, Response: %s", i, tc.start, tc.end, string(body))) } } func TestListAlerts(t *testing.T) { now := time.Now() alerts := []*types.Alert{ { Alert: model.Alert{ Labels: model.LabelSet{"state": "active", "alertname": "alert1"}, StartsAt: now.Add(-time.Minute), }, }, { Alert: model.Alert{ Labels: model.LabelSet{"state": "unprocessed", "alertname": "alert2"}, StartsAt: now.Add(-time.Minute), }, }, { Alert: model.Alert{ Labels: model.LabelSet{"state": "suppressed", "silenced_by": "abc", "alertname": "alert3"}, StartsAt: now.Add(-time.Minute), }, }, { Alert: model.Alert{ Labels: model.LabelSet{"state": "suppressed", "inhibited_by": "abc", "alertname": "alert4"}, StartsAt: now.Add(-time.Minute), }, }, { Alert: model.Alert{ Labels: model.LabelSet{"alertname": "alert5"}, StartsAt: now.Add(-2 * time.Minute), EndsAt: now.Add(-time.Minute), }, }, } for i, tc := range []struct { err bool params map[string]string code int anames []string }{ { false, map[string]string{}, 200, []string{"alert1", "alert2", "alert3", "alert4"}, }, { false, map[string]string{"active": "true", "unprocessed": "true", "silenced": "true", "inhibited": "true"}, 200, []string{"alert1", "alert2", "alert3", "alert4"}, }, { false, map[string]string{"active": "false", "unprocessed": "true", "silenced": "true", "inhibited": "true"}, 200, []string{"alert2", "alert3", "alert4"}, }, { false, map[string]string{"active": "true", "unprocessed": "false", "silenced": "true", "inhibited": "true"}, 200, []string{"alert1", "alert3", "alert4"}, }, { false, map[string]string{"active": "true", "unprocessed": "true", "silenced": "false", "inhibited": "true"}, 200, []string{"alert1", "alert2", "alert4"}, }, { false, map[string]string{"active": "true", "unprocessed": "true", "silenced": "true", "inhibited": "false"}, 200, []string{"alert1", "alert2", "alert3"}, }, { false, map[string]string{"filter": "{alertname=\"alert3\""}, 200, []string{"alert3"}, }, { false, map[string]string{"filter": "{alertname"}, 400, []string{}, }, { false, map[string]string{"receiver": "other"}, 200, []string{}, }, { false, map[string]string{"active": "invalid"}, 400, []string{}, }, { true, map[string]string{}, 500, []string{}, }, } { alertsProvider := newFakeAlerts(alerts, tc.err) api := New(alertsProvider, nil, newGetAlertStatus(alertsProvider), nil, nil, nil) api.route = dispatch.NewRoute(&config.Route{Receiver: "def-receiver"}, nil) r, err := http.NewRequest("GET", "/api/v1/alerts", nil) if err != nil { t.Fatalf("Unexpected error %v", err) } q := r.URL.Query() for k, v := range tc.params { q.Add(k, v) } r.URL.RawQuery = q.Encode() w := httptest.NewRecorder() api.listAlerts(w, r) body, _ := io.ReadAll(w.Result().Body) var res response err = json.Unmarshal(body, &res) if err != nil { t.Fatalf("Unexpected error %v", err) } require.Equal(t, tc.code, w.Code, fmt.Sprintf("test case: %d, response: %s", i, string(body))) if w.Code != 200 { continue } // Data needs to be serialized/deserialized to be converted to the real type. b, err := json.Marshal(res.Data) if err != nil { t.Fatalf("Unexpected error %v", err) } retAlerts := []*Alert{} err = json.Unmarshal(b, &retAlerts) if err != nil { t.Fatalf("Unexpected error %v", err) } anames := []string{} for _, a := range retAlerts { name, ok := a.Labels["alertname"] if ok { anames = append(anames, string(name)) } } require.Equal(t, tc.anames, anames, fmt.Sprintf("test case: %d, alert names are not equal", i)) } } func TestAlertFiltering(t *testing.T) { type test struct { alert *model.Alert msg string expected bool } // Equal equal, err := labels.NewMatcher(labels.MatchEqual, "label1", "test1") if err != nil { t.Errorf("Unexpected error %v", err) } tests := []test{ {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1=test1", true}, {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1=test2", false}, {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2=test2", false}, } for _, test := range tests { actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{equal}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Not Equal notEqual, err := labels.NewMatcher(labels.MatchNotEqual, "label1", "test1") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1!=test1", false}, {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1!=test2", true}, {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2!=test2", true}, } for _, test := range tests { actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{notEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Regexp Equal regexpEqual, err := labels.NewMatcher(labels.MatchRegexp, "label1", "tes.*") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1=~test1", true}, {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1=~test2", true}, {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2=~test2", false}, } for _, test := range tests { actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{regexpEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Regexp Not Equal regexpNotEqual, err := labels.NewMatcher(labels.MatchNotRegexp, "label1", "tes.*") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1!~test1", false}, {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1!~test2", false}, {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2!~test2", true}, } for _, test := range tests { actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{regexpNotEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } } func TestSilenceFiltering(t *testing.T) { type test struct { silence *types.Silence msg string expected bool } // Equal equal, err := labels.NewMatcher(labels.MatchEqual, "label1", "test1") if err != nil { t.Errorf("Unexpected error %v", err) } tests := []test{ { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, "label1=test1", true, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, "label1=test2", false, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, "label2=test2", false, }, } for _, test := range tests { actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{equal}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Not Equal notEqual, err := labels.NewMatcher(labels.MatchNotEqual, "label1", "test1") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, "label1!=test1", false, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, "label1!=test2", true, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, "label2!=test2", true, }, } for _, test := range tests { actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{notEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Regexp Equal regexpEqual, err := labels.NewMatcher(labels.MatchRegexp, "label1", "tes.*") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, "label1=~test1", true, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, "label1=~test2", true, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, "label2=~test2", false, }, } for _, test := range tests { actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{regexpEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } // Regexp Not Equal regexpNotEqual, err := labels.NewMatcher(labels.MatchNotRegexp, "label1", "tes.*") if err != nil { t.Errorf("Unexpected error %v", err) } tests = []test{ { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, "label1!~test1", false, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, "label1!~test2", false, }, { &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, "label2!~test2", true, }, } for _, test := range tests { actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{regexpNotEqual}) msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) require.Equal(t, test.expected, actual, msg) } } func TestReceiversMatchFilter(t *testing.T) { receivers := []string{"pagerduty", "slack", "pushover"} filter, err := regexp.Compile(fmt.Sprintf("^(?:%s)$", "push.*")) if err != nil { t.Errorf("Unexpected error %v", err) } require.True(t, receiversMatchFilter(receivers, filter)) filter, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", "push")) if err != nil { t.Errorf("Unexpected error %v", err) } require.False(t, receiversMatchFilter(receivers, filter)) } func TestMatchFilterLabels(t *testing.T) { testCases := []struct { matcher labels.MatchType expected bool }{ {labels.MatchEqual, true}, {labels.MatchRegexp, true}, {labels.MatchNotEqual, false}, {labels.MatchNotRegexp, false}, } for _, tc := range testCases { l, err := labels.NewMatcher(tc.matcher, "foo", "") require.NoError(t, err) sms := map[string]string{ "baz": "bar", } ls := []*labels.Matcher{l} require.Equal(t, tc.expected, matchFilterLabels(ls, sms)) l, err = labels.NewMatcher(tc.matcher, "foo", "") require.NoError(t, err) sms = map[string]string{ "baz": "bar", "foo": "quux", } ls = []*labels.Matcher{l} require.NotEqual(t, tc.expected, matchFilterLabels(ls, sms)) } } func newMatcher(labelSet model.LabelSet) labels.Matchers { matchers := make([]*labels.Matcher, 0, len(labelSet)) for key, val := range labelSet { matchers = append(matchers, &labels.Matcher{ Type: labels.MatchEqual, Name: string(key), Value: string(val), }) } return matchers }