From bb26aa9b1fe7759f6b252e7971894225fc4b1a1f Mon Sep 17 00:00:00 2001 From: Roman Khavronenko Date: Tue, 7 Sep 2021 22:39:22 +0300 Subject: [PATCH] vmalert: add initial UI implementation (#1602) New UI pages: / - welcome page with API handlers list; /groups - list of all rules per group; /alerts - list of all active alerts; /groupID/alertID/status - status of the active alert; --- app/vmalert/Makefile | 5 +- app/vmalert/README.md | 2 +- app/vmalert/alerting.go | 3 +- app/vmalert/tpl/footer.qtpl | 36 ++ app/vmalert/tpl/footer.qtpl.go | 90 ++++ app/vmalert/tpl/header.qtpl | 43 ++ app/vmalert/tpl/header.qtpl.go | 107 ++++ app/vmalert/tpl/nav.qtpl | 25 + app/vmalert/tpl/nav.qtpl.go | 96 ++++ app/vmalert/web.go | 76 ++- app/vmalert/web.qtpl | 278 +++++++++++ app/vmalert/web.qtpl.go | 889 +++++++++++++++++++++++++++++++++ app/vmalert/web_types.go | 6 + 13 files changed, 1638 insertions(+), 18 deletions(-) create mode 100644 app/vmalert/tpl/footer.qtpl create mode 100644 app/vmalert/tpl/footer.qtpl.go create mode 100644 app/vmalert/tpl/header.qtpl create mode 100644 app/vmalert/tpl/header.qtpl.go create mode 100644 app/vmalert/tpl/nav.qtpl create mode 100644 app/vmalert/tpl/nav.qtpl.go create mode 100644 app/vmalert/web.qtpl create mode 100644 app/vmalert/web.qtpl.go diff --git a/app/vmalert/Makefile b/app/vmalert/Makefile index 8a3dbf24a1..e8da6673dd 100644 --- a/app/vmalert/Makefile +++ b/app/vmalert/Makefile @@ -1,6 +1,9 @@ # All these commands must run from repository root. -vmalert: +templates-gen: install-qtc + qtc -dir=./app/vmalert + +vmalert: templates-gen APP_NAME=vmalert $(MAKE) app-local vmalert-race: diff --git a/app/vmalert/README.md b/app/vmalert/README.md index c5cad17f35..acafc9adab 100644 --- a/app/vmalert/README.md +++ b/app/vmalert/README.md @@ -24,7 +24,6 @@ may fail; * by default, rules execution is sequential within one group, but persisting of execution results to remote storage is asynchronous. Hence, user shouldn't rely on recording rules chaining when result of previous recording rule is reused in next one; -* `vmalert` has no UI, just an API for getting groups and rules statuses. ## QuickStart @@ -243,6 +242,7 @@ tags at [Docker Hub](https://hub.docker.com/r/victoriametrics/vmalert/tags). ### WEB `vmalert` runs a web-server (`-httpListenAddr`) for serving metrics and alerts endpoints: +* `http://` - UI; * `http:///api/v1/groups` - list of all loaded groups and rules; * `http:///api/v1/alerts` - list of all active alerts; * `http:///api/v1///status" ` - get alert status by ID. diff --git a/app/vmalert/alerting.go b/app/vmalert/alerting.go index 05ac0ac079..4aa77adea4 100644 --- a/app/vmalert/alerting.go +++ b/app/vmalert/alerting.go @@ -419,6 +419,7 @@ func (ar *AlertingRule) newAlertAPI(a notifier.Alert) *APIAlert { // encode as strings to avoid rounding ID: fmt.Sprintf("%d", a.ID), GroupID: fmt.Sprintf("%d", a.GroupID), + RuleID: fmt.Sprintf("%d", ar.RuleID), Name: a.Name, Expression: ar.Expr, @@ -426,7 +427,7 @@ func (ar *AlertingRule) newAlertAPI(a notifier.Alert) *APIAlert { Annotations: a.Annotations, State: a.State.String(), ActiveAt: a.Start, - Value: strconv.FormatFloat(a.Value, 'e', -1, 64), + Value: strconv.FormatFloat(a.Value, 'f', -1, 32), } } diff --git a/app/vmalert/tpl/footer.qtpl b/app/vmalert/tpl/footer.qtpl new file mode 100644 index 0000000000..017092e2ad --- /dev/null +++ b/app/vmalert/tpl/footer.qtpl @@ -0,0 +1,36 @@ +{% func Footer() %} + + + + + + +{% endfunc %} diff --git a/app/vmalert/tpl/footer.qtpl.go b/app/vmalert/tpl/footer.qtpl.go new file mode 100644 index 0000000000..f61ee4ba19 --- /dev/null +++ b/app/vmalert/tpl/footer.qtpl.go @@ -0,0 +1,90 @@ +// Code generated by qtc from "footer.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line app/vmalert/tpl/footer.qtpl:1 +package tpl + +//line app/vmalert/tpl/footer.qtpl:1 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line app/vmalert/tpl/footer.qtpl:1 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line app/vmalert/tpl/footer.qtpl:1 +func StreamFooter(qw422016 *qt422016.Writer) { +//line app/vmalert/tpl/footer.qtpl:1 + qw422016.N().S(` + + + + + + +`) +//line app/vmalert/tpl/footer.qtpl:40 +} + +//line app/vmalert/tpl/footer.qtpl:40 +func WriteFooter(qq422016 qtio422016.Writer) { +//line app/vmalert/tpl/footer.qtpl:40 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmalert/tpl/footer.qtpl:40 + StreamFooter(qw422016) +//line app/vmalert/tpl/footer.qtpl:40 + qt422016.ReleaseWriter(qw422016) +//line app/vmalert/tpl/footer.qtpl:40 +} + +//line app/vmalert/tpl/footer.qtpl:40 +func Footer() string { +//line app/vmalert/tpl/footer.qtpl:40 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmalert/tpl/footer.qtpl:40 + WriteFooter(qb422016) +//line app/vmalert/tpl/footer.qtpl:40 + qs422016 := string(qb422016.B) +//line app/vmalert/tpl/footer.qtpl:40 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmalert/tpl/footer.qtpl:40 + return qs422016 +//line app/vmalert/tpl/footer.qtpl:40 +} diff --git a/app/vmalert/tpl/header.qtpl b/app/vmalert/tpl/header.qtpl new file mode 100644 index 0000000000..d5edf68e03 --- /dev/null +++ b/app/vmalert/tpl/header.qtpl @@ -0,0 +1,43 @@ +{% func Header(title string, pages []NavItem) %} + + + + vmalert{% if title != "" %} - {%s title %}{% endif %} + + + + + {%= PrintNavItems(title, pages) %} +
+{% endfunc %} diff --git a/app/vmalert/tpl/header.qtpl.go b/app/vmalert/tpl/header.qtpl.go new file mode 100644 index 0000000000..366a757271 --- /dev/null +++ b/app/vmalert/tpl/header.qtpl.go @@ -0,0 +1,107 @@ +// Code generated by qtc from "header.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line app/vmalert/tpl/header.qtpl:1 +package tpl + +//line app/vmalert/tpl/header.qtpl:1 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line app/vmalert/tpl/header.qtpl:1 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line app/vmalert/tpl/header.qtpl:1 +func StreamHeader(qw422016 *qt422016.Writer, title string, pages []NavItem) { +//line app/vmalert/tpl/header.qtpl:1 + qw422016.N().S(` + + + + vmalert`) +//line app/vmalert/tpl/header.qtpl:5 + if title != "" { +//line app/vmalert/tpl/header.qtpl:5 + qw422016.N().S(` - `) +//line app/vmalert/tpl/header.qtpl:5 + qw422016.E().S(title) +//line app/vmalert/tpl/header.qtpl:5 + } +//line app/vmalert/tpl/header.qtpl:5 + qw422016.N().S(` + + + + + `) +//line app/vmalert/tpl/header.qtpl:41 + StreamPrintNavItems(qw422016, title, pages) +//line app/vmalert/tpl/header.qtpl:41 + qw422016.N().S(` +
+`) +//line app/vmalert/tpl/header.qtpl:43 +} + +//line app/vmalert/tpl/header.qtpl:43 +func WriteHeader(qq422016 qtio422016.Writer, title string, pages []NavItem) { +//line app/vmalert/tpl/header.qtpl:43 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmalert/tpl/header.qtpl:43 + StreamHeader(qw422016, title, pages) +//line app/vmalert/tpl/header.qtpl:43 + qt422016.ReleaseWriter(qw422016) +//line app/vmalert/tpl/header.qtpl:43 +} + +//line app/vmalert/tpl/header.qtpl:43 +func Header(title string, pages []NavItem) string { +//line app/vmalert/tpl/header.qtpl:43 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmalert/tpl/header.qtpl:43 + WriteHeader(qb422016, title, pages) +//line app/vmalert/tpl/header.qtpl:43 + qs422016 := string(qb422016.B) +//line app/vmalert/tpl/header.qtpl:43 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmalert/tpl/header.qtpl:43 + return qs422016 +//line app/vmalert/tpl/header.qtpl:43 +} diff --git a/app/vmalert/tpl/nav.qtpl b/app/vmalert/tpl/nav.qtpl new file mode 100644 index 0000000000..f5442f7f17 --- /dev/null +++ b/app/vmalert/tpl/nav.qtpl @@ -0,0 +1,25 @@ +{% code +type NavItem struct { + Name string + Url string +} +%} + +{% func PrintNavItems(current string, items []NavItem) %} + +{% endfunc %} + + diff --git a/app/vmalert/tpl/nav.qtpl.go b/app/vmalert/tpl/nav.qtpl.go new file mode 100644 index 0000000000..85b56266f7 --- /dev/null +++ b/app/vmalert/tpl/nav.qtpl.go @@ -0,0 +1,96 @@ +// Code generated by qtc from "nav.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line app/vmalert/tpl/nav.qtpl:1 +package tpl + +//line app/vmalert/tpl/nav.qtpl:1 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line app/vmalert/tpl/nav.qtpl:1 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line app/vmalert/tpl/nav.qtpl:2 +type NavItem struct { + Name string + Url string +} + +//line app/vmalert/tpl/nav.qtpl:8 +func StreamPrintNavItems(qw422016 *qt422016.Writer, current string, items []NavItem) { +//line app/vmalert/tpl/nav.qtpl:8 + qw422016.N().S(` + +`) +//line app/vmalert/tpl/nav.qtpl:23 +} + +//line app/vmalert/tpl/nav.qtpl:23 +func WritePrintNavItems(qq422016 qtio422016.Writer, current string, items []NavItem) { +//line app/vmalert/tpl/nav.qtpl:23 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmalert/tpl/nav.qtpl:23 + StreamPrintNavItems(qw422016, current, items) +//line app/vmalert/tpl/nav.qtpl:23 + qt422016.ReleaseWriter(qw422016) +//line app/vmalert/tpl/nav.qtpl:23 +} + +//line app/vmalert/tpl/nav.qtpl:23 +func PrintNavItems(current string, items []NavItem) string { +//line app/vmalert/tpl/nav.qtpl:23 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmalert/tpl/nav.qtpl:23 + WritePrintNavItems(qb422016, current, items) +//line app/vmalert/tpl/nav.qtpl:23 + qs422016 := string(qb422016.B) +//line app/vmalert/tpl/nav.qtpl:23 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmalert/tpl/nav.qtpl:23 + return qs422016 +//line app/vmalert/tpl/nav.qtpl:23 +} diff --git a/app/vmalert/web.go b/app/vmalert/web.go index 8e8a6e2f4e..a82d052c71 100644 --- a/app/vmalert/web.go +++ b/app/vmalert/web.go @@ -23,7 +23,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool { if r.Method != "GET" { return false } - httpserver.WriteAPIHelp(w, [][2]string{ + WriteWelcome(w, [][2]string{ {"/api/v1/groups", "list all loaded groups and rules"}, {"/api/v1/alerts", "list all active alerts"}, {"/api/v1/groupID/alertID/status", "get alert status by ID"}, @@ -31,6 +31,12 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool { {"/-/reload", "reload configuration"}, }) return true + case "/alerts": + WriteListAlerts(w, rh.groupAlerts()) + return true + case "/groups": + WriteListGroups(w, rh.groups()) + return true case "/api/v1/groups": data, err := rh.listGroups() if err != nil { @@ -58,14 +64,26 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool { if !strings.HasSuffix(r.URL.Path, "/status") { return false } - // /api/v1///status - data, err := rh.alert(r.URL.Path) + alert, err := rh.alertByPath(strings.TrimPrefix(r.URL.Path, "/api/v1/")) if err != nil { httpserver.Errorf(w, r, "%s", err) return true } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write(data) + + // /api/v1///status + if strings.HasPrefix(r.URL.Path, "/api/v1/") { + data, err := json.Marshal(alert) + if err != nil { + httpserver.Errorf(w, r, "failed to marshal alert: %s", err) + return true + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write(data) + return true + } + + // //status + WriteAlert(w, alert) return true } } @@ -77,20 +95,25 @@ type listGroupsResponse struct { Status string `json:"status"` } -func (rh *requestHandler) listGroups() ([]byte, error) { +func (rh *requestHandler) groups() []APIGroup { rh.m.groupsMu.RLock() defer rh.m.groupsMu.RUnlock() - lr := listGroupsResponse{Status: "success"} + var groups []APIGroup for _, g := range rh.m.groups { - lr.Data.Groups = append(lr.Data.Groups, g.toAPI()) + groups = append(groups, g.toAPI()) } // sort list of alerts for deterministic output - sort.Slice(lr.Data.Groups, func(i, j int) bool { - return lr.Data.Groups[i].Name < lr.Data.Groups[j].Name + sort.Slice(groups, func(i, j int) bool { + return groups[i].Name < groups[j].Name }) + return groups +} +func (rh *requestHandler) listGroups() ([]byte, error) { + lr := listGroupsResponse{Status: "success"} + lr.Data.Groups = rh.groups() b, err := json.Marshal(lr) if err != nil { return nil, &httpserver.ErrorWithStatusCode{ @@ -108,6 +131,30 @@ type listAlertsResponse struct { Status string `json:"status"` } +func (rh *requestHandler) groupAlerts() []GroupAlerts { + rh.m.groupsMu.RLock() + defer rh.m.groupsMu.RUnlock() + + var groupAlerts []GroupAlerts + for _, g := range rh.m.groups { + var alerts []*APIAlert + for _, r := range g.Rules { + a, ok := r.(*AlertingRule) + if !ok { + continue + } + alerts = append(alerts, a.AlertsAPI()...) + } + if len(alerts) > 0 { + groupAlerts = append(groupAlerts, GroupAlerts{ + Group: g.toAPI(), + Alerts: alerts, + }) + } + } + return groupAlerts +} + func (rh *requestHandler) listAlerts() ([]byte, error) { rh.m.groupsMu.RLock() defer rh.m.groupsMu.RUnlock() @@ -138,18 +185,17 @@ func (rh *requestHandler) listAlerts() ([]byte, error) { return b, nil } -func (rh *requestHandler) alert(path string) ([]byte, error) { +func (rh *requestHandler) alertByPath(path string) (*APIAlert, error) { rh.m.groupsMu.RLock() defer rh.m.groupsMu.RUnlock() - parts := strings.SplitN(strings.TrimPrefix(path, "/api/v1/"), "/", 3) + parts := strings.SplitN(strings.TrimLeft(path, "/"), "/", 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), + Err: fmt.Errorf(`path %q cointains /status suffix but doesn't match pattern "/groupID/alertID/status"`, path), StatusCode: http.StatusBadRequest, } } - groupID, err := uint64FromPath(parts[0]) if err != nil { return nil, badRequest(fmt.Errorf(`cannot parse groupID: %w`, err)) @@ -162,7 +208,7 @@ func (rh *requestHandler) alert(path string) ([]byte, error) { if err != nil { return nil, errResponse(err, http.StatusNotFound) } - return json.Marshal(resp) + return resp, nil } func uint64FromPath(path string) (uint64, error) { diff --git a/app/vmalert/web.qtpl b/app/vmalert/web.qtpl new file mode 100644 index 0000000000..229f5af9f5 --- /dev/null +++ b/app/vmalert/web.qtpl @@ -0,0 +1,278 @@ +{% package main %} + +{% import ( + "time" + "sort" + + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl" +) %} + + +{% code +var navItems = []tpl.NavItem{ + {Name: "vmalert", Url: "/"}, + {Name: "Groups", Url: "/groups"}, + {Name: "Alerts", Url: "/alerts"}, + {Name: "Docs", Url: "https://docs.victoriametrics.com/vmalert.html"}, +} +%} + +{% func Welcome(pathList [][2]string) %} + {%= tpl.Header("vmalert", navItems) %} +

+ API:
+ {% for _, p := range pathList %} + {%code + p, doc := p[0], p[1] + %} + {%s p %} - {%s doc %}
+ {% endfor %} +

+ {%= tpl.Footer() %} +{% endfunc %} + +{% func ListGroups(groups []APIGroup) %} + {%= tpl.Header("Groups", navItems) %} + {% if len(groups) > 0 %} + {%code + rOk := make(map[string]int) + rNotOk := make(map[string]int) + for _, g := range groups { + for _, r := range g.AlertingRules{ + if r.LastError != "" { + rNotOk[g.Name]++ + } else { + rOk[g.Name]++ + } + } + for _, r := range g.RecordingRules{ + if r.LastError != "" { + rNotOk[g.Name]++ + } else { + rOk[g.Name]++ + } + } + } + %} + Collapse All + Expand All + {% for _, g := range groups %} +
+ + {%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%s g.Interval %}) + {% if rNotOk[g.Name] > 0 %}{%d rNotOk[g.Name] %} {% endif %} + {%d rOk[g.Name] %} +

{%s g.File %}

+
+
+ + + + + + + + + + + {% for _, ar := range g.AlertingRules %} + + + + + + + {% endfor %} + {% for _, rr := range g.RecordingRules %} + + + + + + + {% endfor %} + +
RuleErrorSamplesUpdated
+ alert: {%s ar.Name %} (for: {%v ar.For %})
+
{%s ar.Expression %}

+ {% if len(ar.Labels) > 0 %} Labels:{% endif %} + {% for k, v := range ar.Labels %} + {%s k %}={%s v %} + {% endfor %} +
{%s ar.LastError %}
{%d ar.LastSamples %}{%f.3 time.Since(ar.LastExec).Seconds() %}s ago
+ record: {%s rr.Name %}
+
{%s rr.Expression %}
+ {% if len(rr.Labels) > 0 %} Labels:{% endif %} + {% for k, v := range rr.Labels %} + {%s k %}={%s v %} + {% endfor %} +
{%s rr.LastError %}
{%d rr.LastSamples %}{%f.3 time.Since(rr.LastExec).Seconds() %}s ago
+
+ {% endfor %} + + {% else %} +
+

No items...

+
+ {% endif %} + + {%= tpl.Footer() %} + +{% endfunc %} + + +{% func ListAlerts(groupAlerts []GroupAlerts) %} + {%= tpl.Header("Alerts", navItems) %} + {% if len(groupAlerts) > 0 %} + Collapse All + Expand All + {% for _, ga := range groupAlerts %} + {%code g := ga.Group %} +
+ + {%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} + {%d len(ga.Alerts) %} +
+

{%s g.File %}

+
+ {%code + var keys []string + alertsByRule := make(map[string][]*APIAlert) + for _, alert := range ga.Alerts { + if len(alertsByRule[alert.RuleID]) < 1 { + keys = append(keys, alert.RuleID) + } + alertsByRule[alert.RuleID] = append(alertsByRule[alert.RuleID], alert) + } + sort.Strings(keys) + %} +
+ {% for _, ruleID := range keys %} + {%code + defaultAR := alertsByRule[ruleID][0] + var labelKeys []string + for k := range defaultAR.Labels { + labelKeys = append(labelKeys, k) + } + sort.Strings(labelKeys) + %} +
+ alert: {%s defaultAR.Name %} ({%d len(alertsByRule[ruleID]) %})
+ expr:
{%s defaultAR.Expression %}
+ + + + + + + + + + + + {% for _, ar := range alertsByRule[ruleID] %} + + + + + + + + {% endfor %} + +
LabelsStateActive atValueLink
+ {% for _, k := range labelKeys %} + {%s k %}={%s ar.Labels[k] %} + {% endfor %} + {%s ar.State %}{%s ar.ActiveAt.Format("2006-01-02T15:04:05Z07:00") %}{%s ar.Value %} + Details +
+ {% endfor %} +
+
+ {% endfor %} + + {% else %} +
+

No items...

+
+ {% endif %} + + {%= tpl.Footer() %} + +{% endfunc %} + +{% func Alert(alert *APIAlert) %} + {%= tpl.Header("", navItems) %} + {%code + var labelKeys []string + for k := range alert.Labels { + labelKeys = append(labelKeys, k) + } + sort.Strings(labelKeys) + + var annotationKeys []string + for k := range alert.Annotations { + annotationKeys = append(annotationKeys, k) + } + sort.Strings(annotationKeys) + %} +
{%s alert.Name %}{%s alert.State %}
+
+
+
+ Active at +
+
+ {%s alert.ActiveAt.Format("2006-01-02T15:04:05Z07:00") %} +
+
+
+
+
+
+ Expr +
+
+
{%s alert.Expression %}
+
+
+
+
+
+
+ Labels +
+
+ {% for _, k := range labelKeys %} + {%s k %}={%s alert.Labels[k] %} + {% endfor %} +
+
+
+
+
+
+ Annotations +
+
+ {% for _, k := range annotationKeys %} + {%s k %}:
+

{%s alert.Annotations[k] %}

+ {% endfor %} +
+
+
+
+
+
+ Group +
+ +
+
+ {%= tpl.Footer() %} + +{% endfunc %} \ No newline at end of file diff --git a/app/vmalert/web.qtpl.go b/app/vmalert/web.qtpl.go new file mode 100644 index 0000000000..363c0cc8fc --- /dev/null +++ b/app/vmalert/web.qtpl.go @@ -0,0 +1,889 @@ +// Code generated by qtc from "web.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line app/vmalert/web.qtpl:1 +package main + +//line app/vmalert/web.qtpl:3 +import ( + "sort" + "time" + + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl" +) + +//line app/vmalert/web.qtpl:11 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line app/vmalert/web.qtpl:11 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line app/vmalert/web.qtpl:12 +var navItems = []tpl.NavItem{ + {Name: "vmalert", Url: "/"}, + {Name: "Groups", Url: "/groups"}, + {Name: "Alerts", Url: "/alerts"}, + {Name: "Docs", Url: "https://docs.victoriametrics.com/vmalert.html"}, +} + +//line app/vmalert/web.qtpl:20 +func StreamWelcome(qw422016 *qt422016.Writer, pathList [][2]string) { +//line app/vmalert/web.qtpl:20 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:21 + tpl.StreamHeader(qw422016, "vmalert", navItems) +//line app/vmalert/web.qtpl:21 + qw422016.N().S(` +

+ API:
+ `) +//line app/vmalert/web.qtpl:24 + for _, p := range pathList { +//line app/vmalert/web.qtpl:24 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:26 + p, doc := p[0], p[1] + +//line app/vmalert/web.qtpl:27 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:28 + qw422016.E().S(p) +//line app/vmalert/web.qtpl:28 + qw422016.N().S(` - `) +//line app/vmalert/web.qtpl:28 + qw422016.E().S(doc) +//line app/vmalert/web.qtpl:28 + qw422016.N().S(`
+ `) +//line app/vmalert/web.qtpl:29 + } +//line app/vmalert/web.qtpl:29 + qw422016.N().S(` +

+ `) +//line app/vmalert/web.qtpl:31 + tpl.StreamFooter(qw422016) +//line app/vmalert/web.qtpl:31 + qw422016.N().S(` +`) +//line app/vmalert/web.qtpl:32 +} + +//line app/vmalert/web.qtpl:32 +func WriteWelcome(qq422016 qtio422016.Writer, pathList [][2]string) { +//line app/vmalert/web.qtpl:32 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmalert/web.qtpl:32 + StreamWelcome(qw422016, pathList) +//line app/vmalert/web.qtpl:32 + qt422016.ReleaseWriter(qw422016) +//line app/vmalert/web.qtpl:32 +} + +//line app/vmalert/web.qtpl:32 +func Welcome(pathList [][2]string) string { +//line app/vmalert/web.qtpl:32 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmalert/web.qtpl:32 + WriteWelcome(qb422016, pathList) +//line app/vmalert/web.qtpl:32 + qs422016 := string(qb422016.B) +//line app/vmalert/web.qtpl:32 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmalert/web.qtpl:32 + return qs422016 +//line app/vmalert/web.qtpl:32 +} + +//line app/vmalert/web.qtpl:34 +func StreamListGroups(qw422016 *qt422016.Writer, groups []APIGroup) { +//line app/vmalert/web.qtpl:34 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:35 + tpl.StreamHeader(qw422016, "Groups", navItems) +//line app/vmalert/web.qtpl:35 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:36 + if len(groups) > 0 { +//line app/vmalert/web.qtpl:36 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:38 + rOk := make(map[string]int) + rNotOk := make(map[string]int) + for _, g := range groups { + for _, r := range g.AlertingRules { + if r.LastError != "" { + rNotOk[g.Name]++ + } else { + rOk[g.Name]++ + } + } + for _, r := range g.RecordingRules { + if r.LastError != "" { + rNotOk[g.Name]++ + } else { + rOk[g.Name]++ + } + } + } + +//line app/vmalert/web.qtpl:56 + qw422016.N().S(` + Collapse All + Expand All + `) +//line app/vmalert/web.qtpl:59 + for _, g := range groups { +//line app/vmalert/web.qtpl:59 + qw422016.N().S(` +
+ + `) +//line app/vmalert/web.qtpl:62 + qw422016.E().S(g.Name) +//line app/vmalert/web.qtpl:62 + if g.Type != "prometheus" { +//line app/vmalert/web.qtpl:62 + qw422016.N().S(` (`) +//line app/vmalert/web.qtpl:62 + qw422016.E().S(g.Type) +//line app/vmalert/web.qtpl:62 + qw422016.N().S(`)`) +//line app/vmalert/web.qtpl:62 + } +//line app/vmalert/web.qtpl:62 + qw422016.N().S(` (every `) +//line app/vmalert/web.qtpl:62 + qw422016.E().S(g.Interval) +//line app/vmalert/web.qtpl:62 + qw422016.N().S(`) + `) +//line app/vmalert/web.qtpl:63 + if rNotOk[g.Name] > 0 { +//line app/vmalert/web.qtpl:63 + qw422016.N().S(``) +//line app/vmalert/web.qtpl:63 + qw422016.N().D(rNotOk[g.Name]) +//line app/vmalert/web.qtpl:63 + qw422016.N().S(` `) +//line app/vmalert/web.qtpl:63 + } +//line app/vmalert/web.qtpl:63 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:64 + qw422016.N().D(rOk[g.Name]) +//line app/vmalert/web.qtpl:64 + qw422016.N().S(` +

`) +//line app/vmalert/web.qtpl:65 + qw422016.E().S(g.File) +//line app/vmalert/web.qtpl:65 + qw422016.N().S(`

+
+
+ + + + + + + + + + + `) +//line app/vmalert/web.qtpl:78 + for _, ar := range g.AlertingRules { +//line app/vmalert/web.qtpl:78 + qw422016.N().S(` + + + + + + + `) +//line app/vmalert/web.qtpl:92 + } +//line app/vmalert/web.qtpl:92 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:93 + for _, rr := range g.RecordingRules { +//line app/vmalert/web.qtpl:93 + qw422016.N().S(` + + + + + + + `) +//line app/vmalert/web.qtpl:107 + } +//line app/vmalert/web.qtpl:107 + qw422016.N().S(` + +
RuleErrorSamplesUpdated
+ alert: `) +//line app/vmalert/web.qtpl:81 + qw422016.E().S(ar.Name) +//line app/vmalert/web.qtpl:81 + qw422016.N().S(` (for: `) +//line app/vmalert/web.qtpl:81 + qw422016.E().V(ar.For) +//line app/vmalert/web.qtpl:81 + qw422016.N().S(`)
+
`)
+//line app/vmalert/web.qtpl:82
+				qw422016.E().S(ar.Expression)
+//line app/vmalert/web.qtpl:82
+				qw422016.N().S(`

+ `) +//line app/vmalert/web.qtpl:83 + if len(ar.Labels) > 0 { +//line app/vmalert/web.qtpl:83 + qw422016.N().S(` Labels:`) +//line app/vmalert/web.qtpl:83 + } +//line app/vmalert/web.qtpl:83 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:84 + for k, v := range ar.Labels { +//line app/vmalert/web.qtpl:84 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:85 + qw422016.E().S(k) +//line app/vmalert/web.qtpl:85 + qw422016.N().S(`=`) +//line app/vmalert/web.qtpl:85 + qw422016.E().S(v) +//line app/vmalert/web.qtpl:85 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:86 + } +//line app/vmalert/web.qtpl:86 + qw422016.N().S(` +
`) +//line app/vmalert/web.qtpl:88 + qw422016.E().S(ar.LastError) +//line app/vmalert/web.qtpl:88 + qw422016.N().S(`
`) +//line app/vmalert/web.qtpl:89 + qw422016.N().D(ar.LastSamples) +//line app/vmalert/web.qtpl:89 + qw422016.N().S(``) +//line app/vmalert/web.qtpl:90 + qw422016.N().FPrec(time.Since(ar.LastExec).Seconds(), 3) +//line app/vmalert/web.qtpl:90 + qw422016.N().S(`s ago
+ record: `) +//line app/vmalert/web.qtpl:96 + qw422016.E().S(rr.Name) +//line app/vmalert/web.qtpl:96 + qw422016.N().S(`
+
`)
+//line app/vmalert/web.qtpl:97
+				qw422016.E().S(rr.Expression)
+//line app/vmalert/web.qtpl:97
+				qw422016.N().S(`
+ `) +//line app/vmalert/web.qtpl:98 + if len(rr.Labels) > 0 { +//line app/vmalert/web.qtpl:98 + qw422016.N().S(` Labels:`) +//line app/vmalert/web.qtpl:98 + } +//line app/vmalert/web.qtpl:98 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:99 + for k, v := range rr.Labels { +//line app/vmalert/web.qtpl:99 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:100 + qw422016.E().S(k) +//line app/vmalert/web.qtpl:100 + qw422016.N().S(`=`) +//line app/vmalert/web.qtpl:100 + qw422016.E().S(v) +//line app/vmalert/web.qtpl:100 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:101 + } +//line app/vmalert/web.qtpl:101 + qw422016.N().S(` +
`) +//line app/vmalert/web.qtpl:103 + qw422016.E().S(rr.LastError) +//line app/vmalert/web.qtpl:103 + qw422016.N().S(`
`) +//line app/vmalert/web.qtpl:104 + qw422016.N().D(rr.LastSamples) +//line app/vmalert/web.qtpl:104 + qw422016.N().S(``) +//line app/vmalert/web.qtpl:105 + qw422016.N().FPrec(time.Since(rr.LastExec).Seconds(), 3) +//line app/vmalert/web.qtpl:105 + qw422016.N().S(`s ago
+
+ `) +//line app/vmalert/web.qtpl:111 + } +//line app/vmalert/web.qtpl:111 + qw422016.N().S(` + + `) +//line app/vmalert/web.qtpl:113 + } else { +//line app/vmalert/web.qtpl:113 + qw422016.N().S(` +
+

No items...

+
+ `) +//line app/vmalert/web.qtpl:117 + } +//line app/vmalert/web.qtpl:117 + qw422016.N().S(` + + `) +//line app/vmalert/web.qtpl:119 + tpl.StreamFooter(qw422016) +//line app/vmalert/web.qtpl:119 + qw422016.N().S(` + +`) +//line app/vmalert/web.qtpl:121 +} + +//line app/vmalert/web.qtpl:121 +func WriteListGroups(qq422016 qtio422016.Writer, groups []APIGroup) { +//line app/vmalert/web.qtpl:121 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmalert/web.qtpl:121 + StreamListGroups(qw422016, groups) +//line app/vmalert/web.qtpl:121 + qt422016.ReleaseWriter(qw422016) +//line app/vmalert/web.qtpl:121 +} + +//line app/vmalert/web.qtpl:121 +func ListGroups(groups []APIGroup) string { +//line app/vmalert/web.qtpl:121 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmalert/web.qtpl:121 + WriteListGroups(qb422016, groups) +//line app/vmalert/web.qtpl:121 + qs422016 := string(qb422016.B) +//line app/vmalert/web.qtpl:121 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmalert/web.qtpl:121 + return qs422016 +//line app/vmalert/web.qtpl:121 +} + +//line app/vmalert/web.qtpl:124 +func StreamListAlerts(qw422016 *qt422016.Writer, groupAlerts []GroupAlerts) { +//line app/vmalert/web.qtpl:124 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:125 + tpl.StreamHeader(qw422016, "Alerts", navItems) +//line app/vmalert/web.qtpl:125 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:126 + if len(groupAlerts) > 0 { +//line app/vmalert/web.qtpl:126 + qw422016.N().S(` + Collapse All + Expand All + `) +//line app/vmalert/web.qtpl:129 + for _, ga := range groupAlerts { +//line app/vmalert/web.qtpl:129 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:130 + g := ga.Group + +//line app/vmalert/web.qtpl:130 + qw422016.N().S(` + + `) +//line app/vmalert/web.qtpl:139 + var keys []string + alertsByRule := make(map[string][]*APIAlert) + for _, alert := range ga.Alerts { + if len(alertsByRule[alert.RuleID]) < 1 { + keys = append(keys, alert.RuleID) + } + alertsByRule[alert.RuleID] = append(alertsByRule[alert.RuleID], alert) + } + sort.Strings(keys) + +//line app/vmalert/web.qtpl:148 + qw422016.N().S(` +
+ `) +//line app/vmalert/web.qtpl:150 + for _, ruleID := range keys { +//line app/vmalert/web.qtpl:150 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:152 + defaultAR := alertsByRule[ruleID][0] + var labelKeys []string + for k := range defaultAR.Labels { + labelKeys = append(labelKeys, k) + } + sort.Strings(labelKeys) + +//line app/vmalert/web.qtpl:158 + qw422016.N().S(` +
+ alert: `) +//line app/vmalert/web.qtpl:160 + qw422016.E().S(defaultAR.Name) +//line app/vmalert/web.qtpl:160 + qw422016.N().S(` (`) +//line app/vmalert/web.qtpl:160 + qw422016.N().D(len(alertsByRule[ruleID])) +//line app/vmalert/web.qtpl:160 + qw422016.N().S(`)
+ expr:
`)
+//line app/vmalert/web.qtpl:161
+				qw422016.E().S(defaultAR.Expression)
+//line app/vmalert/web.qtpl:161
+				qw422016.N().S(`
+ + + + + + + + + + + + `) +//line app/vmalert/web.qtpl:173 + for _, ar := range alertsByRule[ruleID] { +//line app/vmalert/web.qtpl:173 + qw422016.N().S(` + + + + + + + + `) +//line app/vmalert/web.qtpl:187 + } +//line app/vmalert/web.qtpl:187 + qw422016.N().S(` + +
LabelsStateActive atValueLink
+ `) +//line app/vmalert/web.qtpl:176 + for _, k := range labelKeys { +//line app/vmalert/web.qtpl:176 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:177 + qw422016.E().S(k) +//line app/vmalert/web.qtpl:177 + qw422016.N().S(`=`) +//line app/vmalert/web.qtpl:177 + qw422016.E().S(ar.Labels[k]) +//line app/vmalert/web.qtpl:177 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:178 + } +//line app/vmalert/web.qtpl:178 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:180 + qw422016.E().S(ar.State) +//line app/vmalert/web.qtpl:180 + qw422016.N().S(``) +//line app/vmalert/web.qtpl:181 + qw422016.E().S(ar.ActiveAt.Format("2006-01-02T15:04:05Z07:00")) +//line app/vmalert/web.qtpl:181 + qw422016.N().S(``) +//line app/vmalert/web.qtpl:182 + qw422016.E().S(ar.Value) +//line app/vmalert/web.qtpl:182 + qw422016.N().S(` + Details +
+ `) +//line app/vmalert/web.qtpl:190 + } +//line app/vmalert/web.qtpl:190 + qw422016.N().S(` +
+
+ `) +//line app/vmalert/web.qtpl:193 + } +//line app/vmalert/web.qtpl:193 + qw422016.N().S(` + + `) +//line app/vmalert/web.qtpl:195 + } else { +//line app/vmalert/web.qtpl:195 + qw422016.N().S(` +
+

No items...

+
+ `) +//line app/vmalert/web.qtpl:199 + } +//line app/vmalert/web.qtpl:199 + qw422016.N().S(` + + `) +//line app/vmalert/web.qtpl:201 + tpl.StreamFooter(qw422016) +//line app/vmalert/web.qtpl:201 + qw422016.N().S(` + +`) +//line app/vmalert/web.qtpl:203 +} + +//line app/vmalert/web.qtpl:203 +func WriteListAlerts(qq422016 qtio422016.Writer, groupAlerts []GroupAlerts) { +//line app/vmalert/web.qtpl:203 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmalert/web.qtpl:203 + StreamListAlerts(qw422016, groupAlerts) +//line app/vmalert/web.qtpl:203 + qt422016.ReleaseWriter(qw422016) +//line app/vmalert/web.qtpl:203 +} + +//line app/vmalert/web.qtpl:203 +func ListAlerts(groupAlerts []GroupAlerts) string { +//line app/vmalert/web.qtpl:203 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmalert/web.qtpl:203 + WriteListAlerts(qb422016, groupAlerts) +//line app/vmalert/web.qtpl:203 + qs422016 := string(qb422016.B) +//line app/vmalert/web.qtpl:203 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmalert/web.qtpl:203 + return qs422016 +//line app/vmalert/web.qtpl:203 +} + +//line app/vmalert/web.qtpl:205 +func StreamAlert(qw422016 *qt422016.Writer, alert *APIAlert) { +//line app/vmalert/web.qtpl:205 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:206 + tpl.StreamHeader(qw422016, "", navItems) +//line app/vmalert/web.qtpl:206 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:208 + var labelKeys []string + for k := range alert.Labels { + labelKeys = append(labelKeys, k) + } + sort.Strings(labelKeys) + + var annotationKeys []string + for k := range alert.Annotations { + annotationKeys = append(annotationKeys, k) + } + sort.Strings(annotationKeys) + +//line app/vmalert/web.qtpl:219 + qw422016.N().S(` +
`) +//line app/vmalert/web.qtpl:220 + qw422016.E().S(alert.Name) +//line app/vmalert/web.qtpl:220 + qw422016.N().S(``) +//line app/vmalert/web.qtpl:220 + qw422016.E().S(alert.State) +//line app/vmalert/web.qtpl:220 + qw422016.N().S(`
+
+
+
+ Active at +
+
+ `) +//line app/vmalert/web.qtpl:227 + qw422016.E().S(alert.ActiveAt.Format("2006-01-02T15:04:05Z07:00")) +//line app/vmalert/web.qtpl:227 + qw422016.N().S(` +
+
+
+
+
+
+ Expr +
+
+
`)
+//line app/vmalert/web.qtpl:237
+	qw422016.E().S(alert.Expression)
+//line app/vmalert/web.qtpl:237
+	qw422016.N().S(`
+
+
+
+
+
+
+ Labels +
+
+ `) +//line app/vmalert/web.qtpl:247 + for _, k := range labelKeys { +//line app/vmalert/web.qtpl:247 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:248 + qw422016.E().S(k) +//line app/vmalert/web.qtpl:248 + qw422016.N().S(`=`) +//line app/vmalert/web.qtpl:248 + qw422016.E().S(alert.Labels[k]) +//line app/vmalert/web.qtpl:248 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:249 + } +//line app/vmalert/web.qtpl:249 + qw422016.N().S(` +
+
+
+
+
+
+ Annotations +
+
+ `) +//line app/vmalert/web.qtpl:259 + for _, k := range annotationKeys { +//line app/vmalert/web.qtpl:259 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:260 + qw422016.E().S(k) +//line app/vmalert/web.qtpl:260 + qw422016.N().S(`:
+

`) +//line app/vmalert/web.qtpl:261 + qw422016.E().S(alert.Annotations[k]) +//line app/vmalert/web.qtpl:261 + qw422016.N().S(`

+ `) +//line app/vmalert/web.qtpl:262 + } +//line app/vmalert/web.qtpl:262 + qw422016.N().S(` +
+
+
+ + `) +//line app/vmalert/web.qtpl:276 + tpl.StreamFooter(qw422016) +//line app/vmalert/web.qtpl:276 + qw422016.N().S(` + +`) +//line app/vmalert/web.qtpl:278 +} + +//line app/vmalert/web.qtpl:278 +func WriteAlert(qq422016 qtio422016.Writer, alert *APIAlert) { +//line app/vmalert/web.qtpl:278 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmalert/web.qtpl:278 + StreamAlert(qw422016, alert) +//line app/vmalert/web.qtpl:278 + qt422016.ReleaseWriter(qw422016) +//line app/vmalert/web.qtpl:278 +} + +//line app/vmalert/web.qtpl:278 +func Alert(alert *APIAlert) string { +//line app/vmalert/web.qtpl:278 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmalert/web.qtpl:278 + WriteAlert(qb422016, alert) +//line app/vmalert/web.qtpl:278 + qs422016 := string(qb422016.B) +//line app/vmalert/web.qtpl:278 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmalert/web.qtpl:278 + return qs422016 +//line app/vmalert/web.qtpl:278 +} diff --git a/app/vmalert/web_types.go b/app/vmalert/web_types.go index bcf6732222..e2ad7357ee 100644 --- a/app/vmalert/web_types.go +++ b/app/vmalert/web_types.go @@ -9,6 +9,7 @@ import ( type APIAlert struct { ID string `json:"id"` Name string `json:"name"` + RuleID string `json:"rule_id"` GroupID string `json:"group_id"` Expression string `json:"expression"` State string `json:"state"` @@ -59,3 +60,8 @@ type APIRecordingRule struct { LastExec time.Time `json:"last_exec"` Labels map[string]string `json:"labels"` } + +type GroupAlerts struct { + Group APIGroup + Alerts []*APIAlert +}