diff --git a/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager.go b/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager.go index 1207772..64d74c2 100644 --- a/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager.go +++ b/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager.go @@ -12,6 +12,7 @@ import ( html "html/template" "net/http" text "text/template" + "strings" ) // ServiceType of the Alertmanager service. @@ -69,6 +70,7 @@ type WebhookNotification struct { StartsAt string `json:"startsAt"` EndsAt string `json:"endsAt"` GeneratorURL string `json:"generatorURL"` + SilenceURL string } `json:"alerts"` } @@ -82,6 +84,18 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli return } + // add the silence link for each alert + // see 'newSilenceFromAlertLabels' in + // https://github.com/prometheus/alertmanager/blob/master/ui/app/src/Views/SilenceForm/Parsing.elm + for i := range notif.Alerts { + alert := ¬if.Alerts[i] + filters := []string{} + for label, val := range alert.Labels { + filters = append(filters, fmt.Sprintf("%s%%3D\"%s\"", label, val)) + } + alert.SilenceURL = fmt.Sprintf("%s#silences/new?filter={%s}", notif.ExternalURL, strings.Join(filters, ",") ) + } + for roomID, templates := range s.Rooms { var msg interface{} // we don't check whether the templates parse because we already did when storing them in the db diff --git a/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager_test.go b/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager_test.go new file mode 100644 index 0000000..b8ca7f3 --- /dev/null +++ b/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager_test.go @@ -0,0 +1,144 @@ +package alertmanager + +import ( + "github.com/matrix-org/go-neb/database" + "github.com/matrix-org/go-neb/testutils" + "github.com/matrix-org/gomatrix" + "net/http" + "strings" + "fmt" + "encoding/json" + "io/ioutil" + "bytes" + "testing" + "github.com/matrix-org/go-neb/types" + "net/http/httptest" + "regexp" +) + +func TestNotify(t *testing.T) { + database.SetServiceDB(&database.NopStorage{}) + + // Intercept message sending to Matrix and mock responses + msgs := []gomatrix.HTMLMessage{} + matrixTrans := struct{ testutils.MockTransport }{} + matrixTrans.RT = func(req *http.Request) (*http.Response, error) { + if !strings.Contains(req.URL.String(), "/send/m.room.message") { + return nil, fmt.Errorf("Unhandled URL: %s", req.URL.String()) + } + var msg gomatrix.HTMLMessage + if err := json.NewDecoder(req.Body).Decode(&msg); err != nil { + return nil, fmt.Errorf("Failed to decode request JSON: %s", err) + } + msgs = append(msgs, msg) + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString(`{"event_id":"$yup:event"}`)), + }, nil + } + matrixCli, _ := gomatrix.NewClient("https://hs", "@neb:hs", "its_a_secret") + matrixCli.Client = &http.Client{Transport: matrixTrans} + + // create the service + html_template, err := json.Marshal( + `{{range .Alerts}} + {{index .Labels "severity" }} : {{- index .Labels "alertname" -}} + source + silence + {{- end }} + `, + ) + + if err != nil { + t.Fatal(err) + } + + text_template, err := json.Marshal(`{{range .Alerts}}{{index .Labels "alertname"}} {{end}}`) + if err != nil { + t.Fatal(err) + } + + config := fmt.Sprintf(`{ + "rooms":{ "!testroom:id" : { + "text_template":%s, + "html_template":%s, + "msg_type":"m.text" + }} + }`, text_template, html_template, + ) + + srv, err := types.CreateService("id", "alertmanager", "@neb:hs", []byte(config)) + if err != nil { + t.Fatal(err) + } + + // send a notification + req, err := http.NewRequest( + "POST", "", bytes.NewBufferString(` + { + "externalURL": "http://alertmanager", + "alerts": [ + { + "labels": { + "alertname": "alert 1", + "severity": "huge" + }, + "generatorURL": "http://x" + }, + { + "labels": { + "alertname": "alert 2", + "severity": "tiny" + }, + "generatorURL": "http://y" + } + ] + } + `), + ) + if err != nil { + t.Fatalf("Failed to create webhook request: %s", err) + } + mockWriter := httptest.NewRecorder() + srv.OnReceiveWebhook(mockWriter, req, matrixCli) + + // check response + if mockWriter.Code != 200 { + t.Fatalf("Expected response 200 OK, got %d", mockWriter.Code) + } + if len(msgs) != 1 { + t.Fatalf("Expected sent 1 msgs, sent %d", len(msgs)) + } + msg := msgs[0] + if msg.MsgType != "m.text" { + t.Errorf("Wrong msgtype: got %s want m.text", msg.MsgType) + } + + lines := strings.Split(msg.FormattedBody, "\n" ) + + // silence + silenceRegexp := regexp.MustCompile(`silence`) + matchedSilence := 0 + for _, line := range lines { + if ! strings.Contains(line, "silence") { + continue + } + + matchedSilence += 1 + m := silenceRegexp.FindStringSubmatch(line) + if m == nil { + t.Errorf("silence line %s had bad format", line) + } else { + url := m[1] + expected := "http://alertmanager#silences/new?filter=%7balertname%3D%22alert%201%22,severity%3D%22huge%22%7d" + if url != expected { + t.Errorf("silence url: got %s, want %s", url, expected) + } + } + break + } + + if matchedSilence == 0 { + t.Errorf("Did not find any silence lines") + } +}