From 90de3086b39c5643f71e20b1aa1bff77b8d32b03 Mon Sep 17 00:00:00 2001
From: kreedom <60944649+kreedom@users.noreply.github.com>
Date: Sat, 11 Apr 2020 12:40:24 +0300
Subject: [PATCH] [vmalert] add webserver (#410)

* [vmalert] add webserver
---
 app/vmalert/main.go                           |   6 +-
 app/vmalert/notifier/alert.go                 |  12 ++
 app/vmalert/notifier/alertmanager.go          |   2 +-
 .../notifier/alertmanager_request.qtpl        |   3 +-
 .../notifier/alertmanager_request.qtpl.go     | 113 ++++++++--------
 app/vmalert/notifier/alertmanager_test.go     |   4 +-
 app/vmalert/rule.go                           |  33 ++++-
 app/vmalert/testdata/rules0-good.rules        |   2 +-
 app/vmalert/web.go                            | 122 ++++++++++++++++++
 9 files changed, 230 insertions(+), 67 deletions(-)
 create mode 100644 app/vmalert/web.go

diff --git a/app/vmalert/main.go b/app/vmalert/main.go
index ef6490da28..afc03af09d 100644
--- a/app/vmalert/main.go
+++ b/app/vmalert/main.go
@@ -61,7 +61,7 @@ func main() {
 	w := &watchdog{
 		storage: datasource.NewVMStorage(*datasourceURL, *basicAuthUsername, *basicAuthPassword, &http.Client{}),
 		alertProvider: notifier.NewAlertManager(*providerURL, func(group, name string) string {
-			return strings.Replace(fmt.Sprintf("%s/%s/%s/status", eu, group, name), "//", "/", -1)
+			return fmt.Sprintf("%s/api/v1/%s/%s/status", eu, group, name)
 		}, &http.Client{}),
 	}
 	wg := sync.WaitGroup{}
@@ -73,9 +73,7 @@ func main() {
 		}(groups[i])
 	}
 
-	go httpserver.Serve(*httpListenAddr, func(w http.ResponseWriter, r *http.Request) bool {
-		panic("not implemented")
-	})
+	go httpserver.Serve(*httpListenAddr, (&requestHandler{groups: groups}).handler)
 
 	sig := procutil.WaitForSigterm()
 	logger.Infof("service received signal %s", sig)
diff --git a/app/vmalert/notifier/alert.go b/app/vmalert/notifier/alert.go
index d1f027afee..51887532ba 100644
--- a/app/vmalert/notifier/alert.go
+++ b/app/vmalert/notifier/alert.go
@@ -21,6 +21,7 @@ type Alert struct {
 	Start time.Time
 	End   time.Time
 	Value float64
+	ID    uint64
 }
 
 // AlertState type indicates the Alert state
@@ -37,6 +38,17 @@ const (
 	StateFiring
 )
 
+// String stringer for AlertState
+func (as AlertState) String() string {
+	switch as {
+	case StateFiring:
+		return "firing"
+	case StatePending:
+		return "pending"
+	}
+	return "inactive"
+}
+
 type alertTplData struct {
 	Labels map[string]string
 	Value  float64
diff --git a/app/vmalert/notifier/alertmanager.go b/app/vmalert/notifier/alertmanager.go
index d7e02b7573..ab1ba6cc63 100644
--- a/app/vmalert/notifier/alertmanager.go
+++ b/app/vmalert/notifier/alertmanager.go
@@ -37,7 +37,7 @@ func (am *AlertManager) Send(alerts []Alert) error {
 }
 
 // AlertURLGenerator returns URL to single alert by given name
-type AlertURLGenerator func(group, name string) string
+type AlertURLGenerator func(group, id string) string
 
 const alertManagerPath = "/api/v2/alerts"
 
diff --git a/app/vmalert/notifier/alertmanager_request.qtpl b/app/vmalert/notifier/alertmanager_request.qtpl
index b831bcf7c5..1c64f1f373 100644
--- a/app/vmalert/notifier/alertmanager_request.qtpl
+++ b/app/vmalert/notifier/alertmanager_request.qtpl
@@ -1,4 +1,5 @@
 {% import (
+    "strconv"
     "time"
 ) %}
 {% stripspace %}
@@ -8,7 +9,7 @@
 {% for i, alert := range alerts %}
 {
 	"startsAt":{%q= alert.Start.Format(time.RFC3339Nano) %},
-	"generatorURL": {%q= generatorURL(alert.Group, alert.Name) %},
+	"generatorURL": {%q= generatorURL(alert.Group, strconv.FormatUint(alert.ID, 10)) %},
     {% if !alert.End.IsZero() %}
     "endsAt":{%q= alert.End.Format(time.RFC3339Nano) %},
     {% endif %}
diff --git a/app/vmalert/notifier/alertmanager_request.qtpl.go b/app/vmalert/notifier/alertmanager_request.qtpl.go
index 16880aab25..26aebf5848 100644
--- a/app/vmalert/notifier/alertmanager_request.qtpl.go
+++ b/app/vmalert/notifier/alertmanager_request.qtpl.go
@@ -1,130 +1,131 @@
 // Code generated by qtc from "alertmanager_request.qtpl". DO NOT EDIT.
 // See https://github.com/valyala/quicktemplate for details.
 
-//line notifier/alertmanager_request.qtpl:1
+//line app/vmalert/notifier/alertmanager_request.qtpl:1
 package notifier
 
-//line notifier/alertmanager_request.qtpl:1
+//line app/vmalert/notifier/alertmanager_request.qtpl:1
 import (
+	"strconv"
 	"time"
 )
 
-//line notifier/alertmanager_request.qtpl:6
+//line app/vmalert/notifier/alertmanager_request.qtpl:7
 import (
 	qtio422016 "io"
 
 	qt422016 "github.com/valyala/quicktemplate"
 )
 
-//line notifier/alertmanager_request.qtpl:6
+//line app/vmalert/notifier/alertmanager_request.qtpl:7
 var (
 	_ = qtio422016.Copy
 	_ = qt422016.AcquireByteBuffer
 )
 
-//line notifier/alertmanager_request.qtpl:6
+//line app/vmalert/notifier/alertmanager_request.qtpl:7
 func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(string, string) string) {
-//line notifier/alertmanager_request.qtpl:6
+//line app/vmalert/notifier/alertmanager_request.qtpl:7
 	qw422016.N().S(`[`)
-//line notifier/alertmanager_request.qtpl:8
+//line app/vmalert/notifier/alertmanager_request.qtpl:9
 	for i, alert := range alerts {
-//line notifier/alertmanager_request.qtpl:8
+//line app/vmalert/notifier/alertmanager_request.qtpl:9
 		qw422016.N().S(`{"startsAt":`)
-//line notifier/alertmanager_request.qtpl:10
+//line app/vmalert/notifier/alertmanager_request.qtpl:11
 		qw422016.N().Q(alert.Start.Format(time.RFC3339Nano))
-//line notifier/alertmanager_request.qtpl:10
+//line app/vmalert/notifier/alertmanager_request.qtpl:11
 		qw422016.N().S(`,"generatorURL":`)
-//line notifier/alertmanager_request.qtpl:11
-		qw422016.N().Q(generatorURL(alert.Group, alert.Name))
-//line notifier/alertmanager_request.qtpl:11
+//line app/vmalert/notifier/alertmanager_request.qtpl:12
+		qw422016.N().Q(generatorURL(alert.Group, strconv.FormatUint(alert.ID, 10)))
+//line app/vmalert/notifier/alertmanager_request.qtpl:12
 		qw422016.N().S(`,`)
-//line notifier/alertmanager_request.qtpl:12
+//line app/vmalert/notifier/alertmanager_request.qtpl:13
 		if !alert.End.IsZero() {
-//line notifier/alertmanager_request.qtpl:12
+//line app/vmalert/notifier/alertmanager_request.qtpl:13
 			qw422016.N().S(`"endsAt":`)
-//line notifier/alertmanager_request.qtpl:13
+//line app/vmalert/notifier/alertmanager_request.qtpl:14
 			qw422016.N().Q(alert.End.Format(time.RFC3339Nano))
-//line notifier/alertmanager_request.qtpl:13
+//line app/vmalert/notifier/alertmanager_request.qtpl:14
 			qw422016.N().S(`,`)
-//line notifier/alertmanager_request.qtpl:14
+//line app/vmalert/notifier/alertmanager_request.qtpl:15
 		}
-//line notifier/alertmanager_request.qtpl:14
+//line app/vmalert/notifier/alertmanager_request.qtpl:15
 		qw422016.N().S(`"labels": {"alertname":`)
-//line notifier/alertmanager_request.qtpl:16
+//line app/vmalert/notifier/alertmanager_request.qtpl:17
 		qw422016.N().Q(alert.Name)
-//line notifier/alertmanager_request.qtpl:17
+//line app/vmalert/notifier/alertmanager_request.qtpl:18
 		for k, v := range alert.Labels {
-//line notifier/alertmanager_request.qtpl:17
+//line app/vmalert/notifier/alertmanager_request.qtpl:18
 			qw422016.N().S(`,`)
-//line notifier/alertmanager_request.qtpl:18
+//line app/vmalert/notifier/alertmanager_request.qtpl:19
 			qw422016.N().Q(k)
-//line notifier/alertmanager_request.qtpl:18
+//line app/vmalert/notifier/alertmanager_request.qtpl:19
 			qw422016.N().S(`:`)
-//line notifier/alertmanager_request.qtpl:18
+//line app/vmalert/notifier/alertmanager_request.qtpl:19
 			qw422016.N().Q(v)
-//line notifier/alertmanager_request.qtpl:19
+//line app/vmalert/notifier/alertmanager_request.qtpl:20
 		}
-//line notifier/alertmanager_request.qtpl:19
+//line app/vmalert/notifier/alertmanager_request.qtpl:20
 		qw422016.N().S(`},"annotations": {`)
-//line notifier/alertmanager_request.qtpl:22
+//line app/vmalert/notifier/alertmanager_request.qtpl:23
 		c := len(alert.Annotations)
 
-//line notifier/alertmanager_request.qtpl:23
+//line app/vmalert/notifier/alertmanager_request.qtpl:24
 		for k, v := range alert.Annotations {
-//line notifier/alertmanager_request.qtpl:24
+//line app/vmalert/notifier/alertmanager_request.qtpl:25
 			c = c - 1
 
-//line notifier/alertmanager_request.qtpl:25
+//line app/vmalert/notifier/alertmanager_request.qtpl:26
 			qw422016.N().Q(k)
-//line notifier/alertmanager_request.qtpl:25
+//line app/vmalert/notifier/alertmanager_request.qtpl:26
 			qw422016.N().S(`:`)
-//line notifier/alertmanager_request.qtpl:25
+//line app/vmalert/notifier/alertmanager_request.qtpl:26
 			qw422016.N().Q(v)
-//line notifier/alertmanager_request.qtpl:25
+//line app/vmalert/notifier/alertmanager_request.qtpl:26
 			if c > 0 {
-//line notifier/alertmanager_request.qtpl:25
+//line app/vmalert/notifier/alertmanager_request.qtpl:26
 				qw422016.N().S(`,`)
-//line notifier/alertmanager_request.qtpl:25
+//line app/vmalert/notifier/alertmanager_request.qtpl:26
 			}
-//line notifier/alertmanager_request.qtpl:26
+//line app/vmalert/notifier/alertmanager_request.qtpl:27
 		}
-//line notifier/alertmanager_request.qtpl:26
+//line app/vmalert/notifier/alertmanager_request.qtpl:27
 		qw422016.N().S(`}}`)
-//line notifier/alertmanager_request.qtpl:29
+//line app/vmalert/notifier/alertmanager_request.qtpl:30
 		if i != len(alerts)-1 {
-//line notifier/alertmanager_request.qtpl:29
+//line app/vmalert/notifier/alertmanager_request.qtpl:30
 			qw422016.N().S(`,`)
-//line notifier/alertmanager_request.qtpl:29
+//line app/vmalert/notifier/alertmanager_request.qtpl:30
 		}
-//line notifier/alertmanager_request.qtpl:30
+//line app/vmalert/notifier/alertmanager_request.qtpl:31
 	}
-//line notifier/alertmanager_request.qtpl:30
+//line app/vmalert/notifier/alertmanager_request.qtpl:31
 	qw422016.N().S(`]`)
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 }
 
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(string, string) string) {
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 	streamamRequest(qw422016, alerts, generatorURL)
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 	qt422016.ReleaseWriter(qw422016)
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 }
 
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 func amRequest(alerts []Alert, generatorURL func(string, string) string) string {
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 	qb422016 := qt422016.AcquireByteBuffer()
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 	writeamRequest(qb422016, alerts, generatorURL)
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 	qs422016 := string(qb422016.B)
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 	qt422016.ReleaseByteBuffer(qb422016)
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 	return qs422016
-//line notifier/alertmanager_request.qtpl:32
+//line app/vmalert/notifier/alertmanager_request.qtpl:33
 }
diff --git a/app/vmalert/notifier/alertmanager_test.go b/app/vmalert/notifier/alertmanager_test.go
index b701271835..710d147bc9 100644
--- a/app/vmalert/notifier/alertmanager_test.go
+++ b/app/vmalert/notifier/alertmanager_test.go
@@ -40,7 +40,7 @@ func TestAlertManager_Send(t *testing.T) {
 			if len(a) != 1 {
 				t.Errorf("expected 1 alert in array got %d", len(a))
 			}
-			if a[0].GeneratorURL != "group0alert0" {
+			if a[0].GeneratorURL != "group0" {
 				t.Errorf("exptected alert0 as generatorURL got %s", a[0].GeneratorURL)
 			}
 			if a[0].Labels["alertname"] != "alert0" {
@@ -66,7 +66,7 @@ func TestAlertManager_Send(t *testing.T) {
 		t.Error("expected wrong http code error got nil")
 	}
 	if err := am.Send([]Alert{{
-		Group:       "group0",
+		Group:       "group",
 		Name:        "alert0",
 		Start:       time.Now().UTC(),
 		End:         time.Now().UTC(),
diff --git a/app/vmalert/rule.go b/app/vmalert/rule.go
index 16f8546d0c..5cf2298140 100644
--- a/app/vmalert/rule.go
+++ b/app/vmalert/rule.go
@@ -21,6 +21,15 @@ type Group struct {
 	Rules []*Rule
 }
 
+// ActiveAlerts returns list of active alert for all rules
+func (g *Group) ActiveAlerts() []*notifier.Alert {
+	var list []*notifier.Alert
+	for i := range g.Rules {
+		list = append(list, g.Rules[i].listActiveAlerts()...)
+	}
+	return list
+}
+
 // Rule is basic alert entity
 type Rule struct {
 	Name        string            `yaml:"alert"`
@@ -61,7 +70,6 @@ func (r *Rule) Validate() error {
 // Based on the Querier results Rule maintains notifier.Alerts
 func (r *Rule) Exec(ctx context.Context, q datasource.Querier) error {
 	metrics, err := q.Query(ctx, r.Expr)
-
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
@@ -91,6 +99,7 @@ func (r *Rule) Exec(ctx context.Context, q datasource.Querier) error {
 			r.lastExecError = err
 			return fmt.Errorf("failed to create alert: %s", err)
 		}
+		a.ID = h
 		a.State = notifier.StatePending
 		r.alerts[h] = a
 	}
@@ -119,7 +128,7 @@ func (r *Rule) Exec(ctx context.Context, q datasource.Querier) error {
 // notifier.Notifier.
 // See for reference https://prometheus.io/docs/alerting/clients/
 // TODO: add tests for endAt value
-func (r *Rule) Send(ctx context.Context, ap notifier.Notifier) error {
+func (r *Rule) Send(_ context.Context, ap notifier.Notifier) error {
 	// copy alerts to new list to avoid locks
 	var alertsCopy []notifier.Alert
 	r.mu.Lock()
@@ -178,3 +187,23 @@ func (r *Rule) newAlert(m datasource.Metric) (*notifier.Alert, error) {
 	a.Annotations, err = a.ExecTemplate(r.Annotations)
 	return a, err
 }
+
+func (r *Rule) listActiveAlerts() []*notifier.Alert {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	var list []*notifier.Alert
+	for _, a := range r.alerts {
+		a := a
+		if a.State == notifier.StateFiring {
+			list = append(list, a)
+		}
+	}
+	return list
+}
+
+// Alert returns single alert by its id(hash)
+func (r *Rule) Alert(id uint64) *notifier.Alert {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	return r.alerts[id]
+}
diff --git a/app/vmalert/testdata/rules0-good.rules b/app/vmalert/testdata/rules0-good.rules
index d8ae2f8f5c..5b4a01cb81 100644
--- a/app/vmalert/testdata/rules0-good.rules
+++ b/app/vmalert/testdata/rules0-good.rules
@@ -2,7 +2,7 @@ groups:
   - name: groupGorSingleAlert
     rules:
       - alert: VMRows
-        for: 5m
+        for: 10s
         expr: vm_rows > 0
         labels:
           label: bar
diff --git a/app/vmalert/web.go b/app/vmalert/web.go
new file mode 100644
index 0000000000..ad383f7373
--- /dev/null
+++ b/app/vmalert/web.go
@@ -0,0 +1,122 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
+)
+
+// apiAlert has info for an alert.
+type apiAlert struct {
+	Labels      map[string]string `json:"labels"`
+	Annotations map[string]string `json:"annotations"`
+	State       string            `json:"state"`
+	ActiveAt    time.Time         `json:"activeAt"`
+	Value       string            `json:"value"`
+}
+
+type requestHandler struct {
+	groups []Group
+}
+
+func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
+	resph := responseHandler{w}
+	switch r.URL.Path {
+	default:
+		if strings.HasSuffix(r.URL.Path, "/status") {
+			resph.handle(rh.alert(r.URL.Path))
+			return true
+		}
+		return false
+	case "/api/v1/alerts":
+		resph.handle(rh.listActiveAlerts())
+		return true
+	}
+}
+
+func (rh *requestHandler) listActiveAlerts() ([]byte, error) {
+	type listAlertsResponse struct {
+		Data struct {
+			Alerts []apiAlert `json:"alerts"`
+		} `json:"data"`
+		Status string `json:"status"`
+	}
+	lr := listAlertsResponse{Status: "success"}
+	for _, g := range rh.groups {
+		alerts := g.ActiveAlerts()
+		for i := range alerts {
+			alert := alerts[i]
+			lr.Data.Alerts = append(lr.Data.Alerts, apiAlert{
+				Labels:      alert.Labels,
+				Annotations: alert.Annotations,
+				State:       alert.State.String(),
+				ActiveAt:    alert.Start,
+				Value:       strconv.FormatFloat(alert.Value, 'e', -1, 64),
+			})
+		}
+	}
+
+	b, err := json.Marshal(lr)
+	if err != nil {
+		return nil, &httpserver.ErrorWithStatusCode{
+			Err:        fmt.Errorf(`error encoding list of active alerts: %s`, err),
+			StatusCode: http.StatusInternalServerError,
+		}
+	}
+	return b, nil
+}
+
+func (rh *requestHandler) alert(path string) ([]byte, error) {
+	parts := strings.SplitN(strings.TrimPrefix(path, "/api/v1/"), "/", 3)
+	if len(parts) != 3 {
+		return nil, &httpserver.ErrorWithStatusCode{
+			Err:        fmt.Errorf(`path %q cointains /status suffix but doesn't match pattern "/group/alert/status"`, path),
+			StatusCode: http.StatusBadRequest,
+		}
+	}
+	group := strings.TrimRight(parts[0], "/")
+	idStr := strings.TrimRight(parts[1], "/")
+	id, err := strconv.ParseUint(idStr, 10, 0)
+	if err != nil {
+		return nil, &httpserver.ErrorWithStatusCode{
+			Err:        fmt.Errorf(`cannot parse int from %s"`, idStr),
+			StatusCode: http.StatusBadRequest,
+		}
+	}
+	for _, g := range rh.groups {
+		if g.Name == group {
+			for i := range g.Rules {
+				if alert := g.Rules[i].Alert(id); alert != nil {
+					return json.Marshal(apiAlert{
+						Labels:      alert.Labels,
+						Annotations: alert.Annotations,
+						State:       alert.State.String(),
+						ActiveAt:    alert.Start,
+						Value:       strconv.FormatFloat(alert.Value, 'e', -1, 64),
+					})
+				}
+			}
+		}
+	}
+	return nil, &httpserver.ErrorWithStatusCode{
+		Err:        fmt.Errorf(`cannot find alert %s in %s"`, idStr, group),
+		StatusCode: http.StatusNotFound,
+	}
+}
+
+// responseHandler wrapper on http.ResponseWriter with sugar
+type responseHandler struct{ http.ResponseWriter }
+
+func (w responseHandler) handle(b []byte, err error) {
+	if err != nil {
+		httpserver.Errorf(w, "%s", err)
+		return
+	}
+	w.Header().Set("Content-Type", "application/json")
+	w.Write(b)
+}