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 64c8000..ecdbef9 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
@@ -11,6 +11,7 @@ import (
"github.com/matrix-org/gomatrix"
html "html/template"
"net/http"
+ "strings"
text "text/template"
)
@@ -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..b39c9fa
--- /dev/null
+++ b/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager_test.go
@@ -0,0 +1,183 @@
+package alertmanager
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "github.com/matrix-org/go-neb/database"
+ "github.com/matrix-org/go-neb/testutils"
+ "github.com/matrix-org/go-neb/types"
+ "github.com/matrix-org/gomatrix"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "strings"
+ "testing"
+)
+
+func TestNotify(t *testing.T) {
+ database.SetServiceDB(&database.NopStorage{})
+
+ // Intercept message sending to Matrix and mock responses
+ msgs := []gomatrix.HTMLMessage{}
+ matrixCli := buildTestClient(&msgs)
+
+ // create the service
+ srv := buildTestService(t)
+
+ // 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
+ matchedSilence := 0
+ for _, line := range lines {
+ if !strings.Contains(line, "silence") {
+ continue
+ }
+
+ matchedSilence++
+ checkSilenceLine(t, line, map[string]string{
+ "alertname": "\"alert 1\"",
+ "severity": "\"huge\"",
+ })
+ break
+ }
+
+ if matchedSilence == 0 {
+ t.Errorf("Did not find any silence lines")
+ }
+}
+
+func buildTestClient(msgs *[]gomatrix.HTMLMessage) *gomatrix.Client {
+ 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}
+ return matrixCli
+}
+
+func buildTestService(t *testing.T) types.Service {
+ htmlTemplate, err := json.Marshal(
+ `{{range .Alerts}}
+ {{index .Labels "severity" }} : {{- index .Labels "alertname" -}}
+ source
+ silence
+ {{- end }}
+ `,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ textTemplate, 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"
+ }}
+ }`, textTemplate, htmlTemplate,
+ )
+
+ srv, err := types.CreateService("id", "alertmanager", "@neb:hs", []byte(config))
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return srv
+}
+
+func checkSilenceLine(t *testing.T, line string, expectedKeys map[string]string) {
+ silenceRegexp := regexp.MustCompile(`silence`)
+ m := silenceRegexp.FindStringSubmatch(line)
+ if m == nil {
+ t.Errorf("silence line %s had bad format", line)
+ return
+ }
+
+ unesc, err := url.QueryUnescape(m[1])
+ if err != nil {
+ t.Errorf("Unable to decode filter, %v", err)
+ return
+ }
+
+ matched := 0
+ for _, f := range strings.Split(unesc, ",") {
+ splits := strings.SplitN(f, "=", 2)
+ key := splits[0]
+ exp, ok := expectedKeys[key]
+ if !ok {
+ t.Errorf("unexpected key in filter: %v", key)
+ } else if exp != splits[1] {
+ t.Errorf("bad value for filter key %v: got %q, want %q", key, splits[1], exp)
+ } else {
+ matched++
+ }
+ }
+
+ if matched != len(expectedKeys) {
+ t.Errorf("number of filter fields got %i, want %i", matched, len(expectedKeys))
+ }
+}