From b29fafa86b3cfc3a6caf14b7f30a55e49cbcb339 Mon Sep 17 00:00:00 2001 From: Roman Khavronenko <roman@victoriametrics.com> Date: Fri, 8 Jul 2022 10:26:13 +0200 Subject: [PATCH] vmalert: deprecate alert's status link (#2840) * vmalert: deprecate alert's status link Deprecate alert's status link `/api/v1/<groupID>/<alertID>/status` in favour of `api/v1/alerts?group_id=<group_id>&alert_id=<alert_id>"`. The change was needed for simplifying logic in vmselect for proxying vmalert's requests. The old alert's status link will be still supported for a few versions but will be removed in the future. https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2825 Signed-off-by: hagen1778 <roman@victoriametrics.com> * vmalert: fix review comments Signed-off-by: hagen1778 <roman@victoriametrics.com> --- app/vmalert/README.md | 4 +- app/vmalert/main.go | 7 +- app/vmalert/main_test.go | 3 +- app/vmalert/tpl/footer.qtpl | 10 +- app/vmalert/tpl/footer.qtpl.go | 54 ++-- app/vmalert/tpl/header.qtpl | 9 +- app/vmalert/tpl/header.qtpl.go | 127 +++++---- app/vmalert/utils/links.go | 12 + app/vmalert/web.go | 78 ++++-- app/vmalert/web.qtpl | 8 +- app/vmalert/web.qtpl.go | 498 +++++++++++++++++---------------- app/vmalert/web_test.go | 59 +++- app/vmalert/web_types.go | 13 + docs/CHANGELOG.md | 6 + docs/vmalert.md | 4 +- 15 files changed, 511 insertions(+), 381 deletions(-) create mode 100644 app/vmalert/utils/links.go diff --git a/app/vmalert/README.md b/app/vmalert/README.md index 3b65798e7b..56722c72d6 100644 --- a/app/vmalert/README.md +++ b/app/vmalert/README.md @@ -481,7 +481,7 @@ or time series modification via [relabeling](https://docs.victoriametrics.com/vm * `http://<vmalert-addr>` - UI; * `http://<vmalert-addr>/api/v1/rules` - list of all loaded groups and rules; * `http://<vmalert-addr>/api/v1/alerts` - list of all active alerts; -* `http://<vmalert-addr>/api/v1/<groupID>/<alertID>/status"` - get alert status by ID. +* `http://<vmalert-addr>/vmalert/api/v1/alert?group_id=<group_id>&alert_id=<alert_id>"` - get alert status by ID. Used as alert source in AlertManager. * `http://<vmalert-addr>/metrics` - application metrics. * `http://<vmalert-addr>/-/reload` - hot configuration reload. @@ -681,7 +681,7 @@ The shortlist of configuration flags is the following: How often to evaluate the rules (default 1m0s) -external.alert.source string External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. - eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used + eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/vmalert/api/v1/alert?group_id=&alert_id=' is used -external.label array Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets. Supports an array of values separated by comma or specified via multiple flags. diff --git a/app/vmalert/main.go b/app/vmalert/main.go index eccf4ef3e4..feef1691c0 100644 --- a/app/vmalert/main.go +++ b/app/vmalert/main.go @@ -59,7 +59,7 @@ absolute path to all .tpl files in root.`) externalURL = flag.String("external.url", "", "External URL is used as alert's source for sent alerts to the notifier") externalAlertSource = flag.String("external.alert.source", "", `External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. -eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used`) +eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/vmalert/api/v1/alert?group_id=&alert_id=' is used`) externalLabels = flagutil.NewArray("external.label", "Optional label in the form 'Name=value' to add to all generated recording rules and alerts. "+ "Pass multiple -label flags in order to add multiple label sets.") @@ -236,8 +236,9 @@ func getExternalURL(externalURL, httpListenAddr string, isSecure bool) (*url.URL func getAlertURLGenerator(externalURL *url.URL, externalAlertSource string, validateTemplate bool) (notifier.AlertURLGenerator, error) { if externalAlertSource == "" { - return func(alert notifier.Alert) string { - return fmt.Sprintf("%s/api/v1/%s/%s/status", externalURL, strconv.FormatUint(alert.GroupID, 10), strconv.FormatUint(alert.ID, 10)) + return func(a notifier.Alert) string { + gID, aID := strconv.FormatUint(a.GroupID, 10), strconv.FormatUint(a.ID, 10) + return fmt.Sprintf("%s/vmalert/api/v1/alert?%s=%s&%s=%s", externalURL, paramGroupID, gID, paramAlertID, aID) }, nil } if validateTemplate { diff --git a/app/vmalert/main_test.go b/app/vmalert/main_test.go index 4999723ea8..30595d2f02 100644 --- a/app/vmalert/main_test.go +++ b/app/vmalert/main_test.go @@ -41,7 +41,8 @@ func TestGetAlertURLGenerator(t *testing.T) { if err != nil { t.Errorf("unexpected error %s", err) } - if exp := "https://victoriametrics.com/path/api/v1/42/2/status"; exp != fn(testAlert) { + exp := fmt.Sprintf("https://victoriametrics.com/path/vmalert/api/v1/alert?%s=42&%s=2", paramGroupID, paramAlertID) + if exp != fn(testAlert) { t.Errorf("unexpected url want %s, got %s", exp, fn(testAlert)) } _, err = getAlertURLGenerator(nil, "foo?{{invalid}}", true) diff --git a/app/vmalert/tpl/footer.qtpl b/app/vmalert/tpl/footer.qtpl index eef27785d4..df1e3a4ba2 100644 --- a/app/vmalert/tpl/footer.qtpl +++ b/app/vmalert/tpl/footer.qtpl @@ -1,16 +1,12 @@ {% import ( "net/http" - "strings" + + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils" ) %} {% func Footer(r *http.Request) %} -{%code - prefix := "/vmalert/" - if strings.HasPrefix(r.URL.Path, prefix) { - prefix = "" - } -%} + {%code prefix := utils.Prefix(r.URL.Path) %} </main> <script src="{%s prefix %}static/js/jquery-3.6.0.min.js" type="text/javascript"></script> <script src="{%s prefix %}static/js/bootstrap.bundle.min.js" type="text/javascript"></script> diff --git a/app/vmalert/tpl/footer.qtpl.go b/app/vmalert/tpl/footer.qtpl.go index 65eea9fa4b..3f8b098012 100644 --- a/app/vmalert/tpl/footer.qtpl.go +++ b/app/vmalert/tpl/footer.qtpl.go @@ -7,45 +7,43 @@ package tpl //line app/vmalert/tpl/footer.qtpl:1 import ( "net/http" - "strings" + + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils" ) -//line app/vmalert/tpl/footer.qtpl:7 +//line app/vmalert/tpl/footer.qtpl:8 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) -//line app/vmalert/tpl/footer.qtpl:7 +//line app/vmalert/tpl/footer.qtpl:8 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) -//line app/vmalert/tpl/footer.qtpl:7 +//line app/vmalert/tpl/footer.qtpl:8 func StreamFooter(qw422016 *qt422016.Writer, r *http.Request) { -//line app/vmalert/tpl/footer.qtpl:7 +//line app/vmalert/tpl/footer.qtpl:8 qw422016.N().S(` -`) + `) //line app/vmalert/tpl/footer.qtpl:9 - prefix := "/vmalert/" - if strings.HasPrefix(r.URL.Path, prefix) { - prefix = "" - } + prefix := utils.Prefix(r.URL.Path) -//line app/vmalert/tpl/footer.qtpl:13 +//line app/vmalert/tpl/footer.qtpl:9 qw422016.N().S(` </main> <script src="`) -//line app/vmalert/tpl/footer.qtpl:15 +//line app/vmalert/tpl/footer.qtpl:11 qw422016.E().S(prefix) -//line app/vmalert/tpl/footer.qtpl:15 +//line app/vmalert/tpl/footer.qtpl:11 qw422016.N().S(`static/js/jquery-3.6.0.min.js" type="text/javascript"></script> <script src="`) -//line app/vmalert/tpl/footer.qtpl:16 +//line app/vmalert/tpl/footer.qtpl:12 qw422016.E().S(prefix) -//line app/vmalert/tpl/footer.qtpl:16 +//line app/vmalert/tpl/footer.qtpl:12 qw422016.N().S(`static/js/bootstrap.bundle.min.js" type="text/javascript"></script> <script type="text/javascript"> function expandAll() { @@ -79,31 +77,31 @@ func StreamFooter(qw422016 *qt422016.Writer, r *http.Request) { </body> </html> `) -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 } -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 func WriteFooter(qq422016 qtio422016.Writer, r *http.Request) { -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 StreamFooter(qw422016, r) -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 qt422016.ReleaseWriter(qw422016) -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 } -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 func Footer(r *http.Request) string { -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 WriteFooter(qb422016, r) -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 qs422016 := string(qb422016.B) -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 return qs422016 -//line app/vmalert/tpl/footer.qtpl:48 +//line app/vmalert/tpl/footer.qtpl:44 } diff --git a/app/vmalert/tpl/header.qtpl b/app/vmalert/tpl/header.qtpl index c999333db4..b5714da293 100644 --- a/app/vmalert/tpl/header.qtpl +++ b/app/vmalert/tpl/header.qtpl @@ -2,15 +2,12 @@ "strings" "net/http" "path" + + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils" ) %} {% func Header(r *http.Request, navItems []NavItem, title string) %} -{%code - prefix := "/vmalert/" - if strings.HasPrefix(r.URL.Path, prefix) { - prefix = "" - } -%} + {%code prefix := utils.Prefix(r.URL.Path) %} <!DOCTYPE html> <html lang="en"> <head> diff --git a/app/vmalert/tpl/header.qtpl.go b/app/vmalert/tpl/header.qtpl.go index 1c2c55d858..6fb6b0483d 100644 --- a/app/vmalert/tpl/header.qtpl.go +++ b/app/vmalert/tpl/header.qtpl.go @@ -9,52 +9,51 @@ import ( "net/http" "path" "strings" + + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils" ) -//line app/vmalert/tpl/header.qtpl:7 +//line app/vmalert/tpl/header.qtpl:9 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) -//line app/vmalert/tpl/header.qtpl:7 +//line app/vmalert/tpl/header.qtpl:9 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) -//line app/vmalert/tpl/header.qtpl:7 -func StreamHeader(qw422016 *qt422016.Writer, r *http.Request, navItems []NavItem, title string) { -//line app/vmalert/tpl/header.qtpl:7 - qw422016.N().S(` -`) //line app/vmalert/tpl/header.qtpl:9 - prefix := "/vmalert/" - if strings.HasPrefix(r.URL.Path, prefix) { - prefix = "" - } +func StreamHeader(qw422016 *qt422016.Writer, r *http.Request, navItems []NavItem, title string) { +//line app/vmalert/tpl/header.qtpl:9 + qw422016.N().S(` + `) +//line app/vmalert/tpl/header.qtpl:10 + prefix := utils.Prefix(r.URL.Path) -//line app/vmalert/tpl/header.qtpl:13 +//line app/vmalert/tpl/header.qtpl:10 qw422016.N().S(` <!DOCTYPE html> <html lang="en"> <head> <title>vmalert`) -//line app/vmalert/tpl/header.qtpl:17 +//line app/vmalert/tpl/header.qtpl:14 if title != "" { -//line app/vmalert/tpl/header.qtpl:17 +//line app/vmalert/tpl/header.qtpl:14 qw422016.N().S(` - `) -//line app/vmalert/tpl/header.qtpl:17 +//line app/vmalert/tpl/header.qtpl:14 qw422016.E().S(title) -//line app/vmalert/tpl/header.qtpl:17 +//line app/vmalert/tpl/header.qtpl:14 } -//line app/vmalert/tpl/header.qtpl:17 +//line app/vmalert/tpl/header.qtpl:14 qw422016.N().S(`</title> <link href="`) -//line app/vmalert/tpl/header.qtpl:18 +//line app/vmalert/tpl/header.qtpl:15 qw422016.E().S(prefix) -//line app/vmalert/tpl/header.qtpl:18 +//line app/vmalert/tpl/header.qtpl:15 qw422016.N().S(`static/css/bootstrap.min.css" rel="stylesheet" /> <style> body{ @@ -105,124 +104,124 @@ func StreamHeader(qw422016 *qt422016.Writer, r *http.Request, navItems []NavItem </head> <body> `) -//line app/vmalert/tpl/header.qtpl:67 +//line app/vmalert/tpl/header.qtpl:64 streamprintNavItems(qw422016, r, title, navItems) -//line app/vmalert/tpl/header.qtpl:67 +//line app/vmalert/tpl/header.qtpl:64 qw422016.N().S(` <main class="px-2"> `) -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 } -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 func WriteHeader(qq422016 qtio422016.Writer, r *http.Request, navItems []NavItem, title string) { -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 StreamHeader(qw422016, r, navItems, title) -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 qt422016.ReleaseWriter(qw422016) -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 } -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 func Header(r *http.Request, navItems []NavItem, title string) string { -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 WriteHeader(qb422016, r, navItems, title) -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 qs422016 := string(qb422016.B) -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 return qs422016 -//line app/vmalert/tpl/header.qtpl:69 +//line app/vmalert/tpl/header.qtpl:66 } -//line app/vmalert/tpl/header.qtpl:73 +//line app/vmalert/tpl/header.qtpl:70 type NavItem struct { Name string Url string } -//line app/vmalert/tpl/header.qtpl:79 +//line app/vmalert/tpl/header.qtpl:76 func streamprintNavItems(qw422016 *qt422016.Writer, r *http.Request, current string, items []NavItem) { -//line app/vmalert/tpl/header.qtpl:79 +//line app/vmalert/tpl/header.qtpl:76 qw422016.N().S(` `) -//line app/vmalert/tpl/header.qtpl:81 +//line app/vmalert/tpl/header.qtpl:78 prefix := "/vmalert/" if strings.HasPrefix(r.URL.Path, prefix) { prefix = "" } -//line app/vmalert/tpl/header.qtpl:85 +//line app/vmalert/tpl/header.qtpl:82 qw422016.N().S(` <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <div class="container-fluid"> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav me-auto mb-2 mb-md-0"> `) -//line app/vmalert/tpl/header.qtpl:90 +//line app/vmalert/tpl/header.qtpl:87 for _, item := range items { -//line app/vmalert/tpl/header.qtpl:90 +//line app/vmalert/tpl/header.qtpl:87 qw422016.N().S(` <li class="nav-item"> <a class="nav-link`) -//line app/vmalert/tpl/header.qtpl:92 +//line app/vmalert/tpl/header.qtpl:89 if current == item.Name { -//line app/vmalert/tpl/header.qtpl:92 +//line app/vmalert/tpl/header.qtpl:89 qw422016.N().S(` active`) -//line app/vmalert/tpl/header.qtpl:92 +//line app/vmalert/tpl/header.qtpl:89 } -//line app/vmalert/tpl/header.qtpl:92 +//line app/vmalert/tpl/header.qtpl:89 qw422016.N().S(`" href="`) -//line app/vmalert/tpl/header.qtpl:92 +//line app/vmalert/tpl/header.qtpl:89 qw422016.E().S(path.Join(prefix, item.Url)) -//line app/vmalert/tpl/header.qtpl:92 +//line app/vmalert/tpl/header.qtpl:89 qw422016.N().S(`"> `) -//line app/vmalert/tpl/header.qtpl:93 +//line app/vmalert/tpl/header.qtpl:90 qw422016.E().S(item.Name) -//line app/vmalert/tpl/header.qtpl:93 +//line app/vmalert/tpl/header.qtpl:90 qw422016.N().S(` </a> </li> `) -//line app/vmalert/tpl/header.qtpl:96 +//line app/vmalert/tpl/header.qtpl:93 } -//line app/vmalert/tpl/header.qtpl:96 +//line app/vmalert/tpl/header.qtpl:93 qw422016.N().S(` </ul> </div> </nav> `) -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 } -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 func writeprintNavItems(qq422016 qtio422016.Writer, r *http.Request, current string, items []NavItem) { -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 streamprintNavItems(qw422016, r, current, items) -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 qt422016.ReleaseWriter(qw422016) -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 } -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 func printNavItems(r *http.Request, current string, items []NavItem) string { -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 writeprintNavItems(qb422016, r, current, items) -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 qs422016 := string(qb422016.B) -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 return qs422016 -//line app/vmalert/tpl/header.qtpl:100 +//line app/vmalert/tpl/header.qtpl:97 } diff --git a/app/vmalert/utils/links.go b/app/vmalert/utils/links.go new file mode 100644 index 0000000000..00a9d3a62e --- /dev/null +++ b/app/vmalert/utils/links.go @@ -0,0 +1,12 @@ +package utils + +import "strings" + +const prefix = "/vmalert/" + +func Prefix(path string) string { + if strings.HasPrefix(path, prefix) { + return "" + } + return prefix +} diff --git a/app/vmalert/web.go b/app/vmalert/web.go index 1969db7507..ae9c156167 100644 --- a/app/vmalert/web.go +++ b/app/vmalert/web.go @@ -25,11 +25,11 @@ var ( func initLinks() { apiLinks = [][2]string{ - // api links are relative since they can be used by external clients - // such as Grafana and proxied via vmselect. + // api links are relative since they can be used by external clients, + // such as Grafana, and proxied via vmselect. {"api/v1/rules", "list all loaded groups and rules"}, {"api/v1/alerts", "list all active alerts"}, - {"api/v1/groupID/alertID/status", "get alert status by ID"}, + {fmt.Sprintf("api/v1/alert?%s=<int>&%s=<int>", paramGroupID, paramAlertID), "get alert status by group and alert ID"}, // system links {"/flags", "command-line flags"}, @@ -76,6 +76,14 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool { case "/vmalert/alerts": WriteListAlerts(w, r, rh.groupAlerts()) return true + case "/vmalert/alert": + alert, err := rh.getAlert(r) + if err != nil { + httpserver.Errorf(w, r, "%s", err) + return true + } + WriteAlert(w, r, alert) + return true case "/vmalert/groups": WriteListGroups(w, r, rh.groups()) return true @@ -111,7 +119,20 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool { w.Header().Set("Content-Type", "application/json") w.Write(data) return true - + case "/vmalert/api/v1/alert", "/api/v1/alert": + alert, err := rh.getAlert(r) + if err != nil { + httpserver.Errorf(w, r, "%s", err) + return true + } + 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") + w.Write(data) + return true case "/-/reload": logger.Infof("api config reload was called, sending sighup") procutil.SelfSIGHUP() @@ -119,6 +140,11 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool { return true default: + // Support of deprecated links: + // * /api/v1/<groupID>/<alertID>/status + // * <groupID>/<alertID>/status + // TODO: to remove in next versions + if !strings.HasSuffix(r.URL.Path, "/status") { return false } @@ -128,24 +154,36 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool { return true } - // /api/v1/<groupID>/<alertID>/status + redirectURL := alert.WebLink() 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") - w.Write(data) - return true + redirectURL = alert.APILink() } - - // <groupID>/<alertID>/status - WriteAlert(w, r, alert) + http.Redirect(w, r, "/"+redirectURL, http.StatusPermanentRedirect) return true } } +const ( + paramGroupID = "group_id" + paramAlertID = "alert_id" +) + +func (rh *requestHandler) getAlert(r *http.Request) (*APIAlert, error) { + groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 0) + if err != nil { + return nil, fmt.Errorf("failed to read %q param: %s", paramGroupID, err) + } + alertID, err := strconv.ParseUint(r.FormValue(paramAlertID), 10, 0) + if err != nil { + return nil, fmt.Errorf("failed to read %q param: %s", paramAlertID, err) + } + a, err := rh.m.AlertAPI(groupID, alertID) + if err != nil { + return nil, errResponse(err, http.StatusNotFound) + } + return a, nil +} + type listGroupsResponse struct { Status string `json:"status"` Data struct { @@ -245,10 +283,10 @@ func (rh *requestHandler) listAlerts() ([]byte, error) { } func (rh *requestHandler) alertByPath(path string) (*APIAlert, error) { - rh.m.groupsMu.RLock() - defer rh.m.groupsMu.RUnlock() - - parts := strings.SplitN(strings.TrimLeft(path, "/"), "/", 3) + if strings.HasPrefix(path, "/vmalert") { + path = strings.TrimLeft(path, "/vmalert") + } + parts := strings.SplitN(strings.TrimLeft(path, "/"), "/", -1) if len(parts) != 3 { return nil, &httpserver.ErrorWithStatusCode{ Err: fmt.Errorf(`path %q cointains /status suffix but doesn't match pattern "/groupID/alertID/status"`, path), diff --git a/app/vmalert/web.qtpl b/app/vmalert/web.qtpl index d5e5c316f4..7e5db0a5c9 100644 --- a/app/vmalert/web.qtpl +++ b/app/vmalert/web.qtpl @@ -3,10 +3,10 @@ {% import ( "time" "sort" - "path" "net/http" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl" + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier" ) %} @@ -119,6 +119,7 @@ {% func ListAlerts(r *http.Request, groupAlerts []GroupAlerts) %} + {%code prefix := utils.Prefix(r.URL.Path) %} {%= tpl.Header(r, navItems, "Alerts") %} {% if len(groupAlerts) > 0 %} <a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a> @@ -183,7 +184,7 @@ </td> <td>{%s ar.Value %}</td> <td> - <a href="{%s path.Join(g.ID, ar.ID, "status") %}">Details</a> + <a href="{%s prefix+ar.WebLink() %}">Details</a> </td> </tr> {% endfor %} @@ -261,6 +262,7 @@ {% endfunc %} {% func Alert(r *http.Request, alert *APIAlert) %} + {%code prefix := utils.Prefix(r.URL.Path) %} {%= tpl.Header(r, navItems, "") %} {%code var labelKeys []string @@ -327,7 +329,7 @@ Group </div> <div class="col"> - <a target="_blank" href="/groups#group-{%s alert.GroupID %}">{%s alert.GroupID %}</a> + <a target="_blank" href="{%s prefix %}groups#group-{%s alert.GroupID %}">{%s alert.GroupID %}</a> </div> </div> </div> diff --git a/app/vmalert/web.qtpl.go b/app/vmalert/web.qtpl.go index d37e9d2e30..aa73307c6f 100644 --- a/app/vmalert/web.qtpl.go +++ b/app/vmalert/web.qtpl.go @@ -7,12 +7,12 @@ package main //line app/vmalert/web.qtpl:3 import ( "net/http" - "path" "sort" "time" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl" + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils" ) //line app/vmalert/web.qtpl:14 @@ -434,70 +434,76 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts [] qw422016.N().S(` `) //line app/vmalert/web.qtpl:122 - tpl.StreamHeader(qw422016, r, navItems, "Alerts") + prefix := utils.Prefix(r.URL.Path) + //line app/vmalert/web.qtpl:122 qw422016.N().S(` `) //line app/vmalert/web.qtpl:123 - if len(groupAlerts) > 0 { + tpl.StreamHeader(qw422016, r, navItems, "Alerts") //line app/vmalert/web.qtpl:123 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:124 + if len(groupAlerts) > 0 { +//line app/vmalert/web.qtpl:124 qw422016.N().S(` <a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a> <a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a> `) -//line app/vmalert/web.qtpl:126 +//line app/vmalert/web.qtpl:127 for _, ga := range groupAlerts { -//line app/vmalert/web.qtpl:126 +//line app/vmalert/web.qtpl:127 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:127 +//line app/vmalert/web.qtpl:128 g := ga.Group -//line app/vmalert/web.qtpl:127 +//line app/vmalert/web.qtpl:128 qw422016.N().S(` <div class="group-heading alert-danger" data-bs-target="rules-`) -//line app/vmalert/web.qtpl:128 +//line app/vmalert/web.qtpl:129 qw422016.E().S(g.ID) -//line app/vmalert/web.qtpl:128 +//line app/vmalert/web.qtpl:129 qw422016.N().S(`"> <span class="anchor" id="group-`) -//line app/vmalert/web.qtpl:129 +//line app/vmalert/web.qtpl:130 qw422016.E().S(g.ID) -//line app/vmalert/web.qtpl:129 +//line app/vmalert/web.qtpl:130 qw422016.N().S(`"></span> <a href="#group-`) -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 qw422016.E().S(g.ID) -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 qw422016.N().S(`">`) -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 qw422016.E().S(g.Name) -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 if g.Type != "prometheus" { -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 qw422016.N().S(` (`) -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 qw422016.E().S(g.Type) -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 qw422016.N().S(`)`) -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 } -//line app/vmalert/web.qtpl:130 +//line app/vmalert/web.qtpl:131 qw422016.N().S(`</a> <span class="badge bg-danger" title="Number of active alerts">`) -//line app/vmalert/web.qtpl:131 +//line app/vmalert/web.qtpl:132 qw422016.N().D(len(ga.Alerts)) -//line app/vmalert/web.qtpl:131 +//line app/vmalert/web.qtpl:132 qw422016.N().S(`</span> <br> <p class="fs-6 fw-lighter">`) -//line app/vmalert/web.qtpl:133 +//line app/vmalert/web.qtpl:134 qw422016.E().S(g.File) -//line app/vmalert/web.qtpl:133 +//line app/vmalert/web.qtpl:134 qw422016.N().S(`</p> </div> `) -//line app/vmalert/web.qtpl:136 +//line app/vmalert/web.qtpl:137 var keys []string alertsByRule := make(map[string][]*APIAlert) for _, alert := range ga.Alerts { @@ -508,20 +514,20 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts [] } sort.Strings(keys) -//line app/vmalert/web.qtpl:145 +//line app/vmalert/web.qtpl:146 qw422016.N().S(` <div class="collapse" id="rules-`) -//line app/vmalert/web.qtpl:146 +//line app/vmalert/web.qtpl:147 qw422016.E().S(g.ID) -//line app/vmalert/web.qtpl:146 +//line app/vmalert/web.qtpl:147 qw422016.N().S(`"> `) -//line app/vmalert/web.qtpl:147 +//line app/vmalert/web.qtpl:148 for _, ruleID := range keys { -//line app/vmalert/web.qtpl:147 +//line app/vmalert/web.qtpl:148 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:149 +//line app/vmalert/web.qtpl:150 defaultAR := alertsByRule[ruleID][0] var labelKeys []string for k := range defaultAR.Labels { @@ -529,28 +535,28 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts [] } sort.Strings(labelKeys) -//line app/vmalert/web.qtpl:155 +//line app/vmalert/web.qtpl:156 qw422016.N().S(` <br> <b>alert:</b> `) -//line app/vmalert/web.qtpl:157 +//line app/vmalert/web.qtpl:158 qw422016.E().S(defaultAR.Name) -//line app/vmalert/web.qtpl:157 +//line app/vmalert/web.qtpl:158 qw422016.N().S(` (`) -//line app/vmalert/web.qtpl:157 +//line app/vmalert/web.qtpl:158 qw422016.N().D(len(alertsByRule[ruleID])) -//line app/vmalert/web.qtpl:157 +//line app/vmalert/web.qtpl:158 qw422016.N().S(`) | <span><a target="_blank" href="`) -//line app/vmalert/web.qtpl:158 +//line app/vmalert/web.qtpl:159 qw422016.E().S(defaultAR.SourceLink) -//line app/vmalert/web.qtpl:158 +//line app/vmalert/web.qtpl:159 qw422016.N().S(`">Source</a></span> <br> <b>expr:</b><code><pre>`) -//line app/vmalert/web.qtpl:160 +//line app/vmalert/web.qtpl:161 qw422016.E().S(defaultAR.Expression) -//line app/vmalert/web.qtpl:160 +//line app/vmalert/web.qtpl:161 qw422016.N().S(`</pre></code> <table class="table table-striped table-hover table-sm"> <thead> @@ -564,204 +570,204 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts [] </thead> <tbody> `) -//line app/vmalert/web.qtpl:172 +//line app/vmalert/web.qtpl:173 for _, ar := range alertsByRule[ruleID] { -//line app/vmalert/web.qtpl:172 +//line app/vmalert/web.qtpl:173 qw422016.N().S(` <tr> <td> `) -//line app/vmalert/web.qtpl:175 +//line app/vmalert/web.qtpl:176 for _, k := range labelKeys { -//line app/vmalert/web.qtpl:175 +//line app/vmalert/web.qtpl:176 qw422016.N().S(` <span class="ms-1 badge bg-primary">`) -//line app/vmalert/web.qtpl:176 +//line app/vmalert/web.qtpl:177 qw422016.E().S(k) -//line app/vmalert/web.qtpl:176 +//line app/vmalert/web.qtpl:177 qw422016.N().S(`=`) -//line app/vmalert/web.qtpl:176 +//line app/vmalert/web.qtpl:177 qw422016.E().S(ar.Labels[k]) -//line app/vmalert/web.qtpl:176 +//line app/vmalert/web.qtpl:177 qw422016.N().S(`</span> `) -//line app/vmalert/web.qtpl:177 +//line app/vmalert/web.qtpl:178 } -//line app/vmalert/web.qtpl:177 +//line app/vmalert/web.qtpl:178 qw422016.N().S(` </td> <td>`) -//line app/vmalert/web.qtpl:179 +//line app/vmalert/web.qtpl:180 streambadgeState(qw422016, ar.State) -//line app/vmalert/web.qtpl:179 +//line app/vmalert/web.qtpl:180 qw422016.N().S(`</td> <td> `) -//line app/vmalert/web.qtpl:181 +//line app/vmalert/web.qtpl:182 qw422016.E().S(ar.ActiveAt.Format("2006-01-02T15:04:05Z07:00")) -//line app/vmalert/web.qtpl:181 +//line app/vmalert/web.qtpl:182 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:182 +//line app/vmalert/web.qtpl:183 if ar.Restored { -//line app/vmalert/web.qtpl:182 +//line app/vmalert/web.qtpl:183 streambadgeRestored(qw422016) -//line app/vmalert/web.qtpl:182 +//line app/vmalert/web.qtpl:183 } -//line app/vmalert/web.qtpl:182 +//line app/vmalert/web.qtpl:183 qw422016.N().S(` </td> <td>`) -//line app/vmalert/web.qtpl:184 +//line app/vmalert/web.qtpl:185 qw422016.E().S(ar.Value) -//line app/vmalert/web.qtpl:184 +//line app/vmalert/web.qtpl:185 qw422016.N().S(`</td> <td> <a href="`) -//line app/vmalert/web.qtpl:186 - qw422016.E().S(path.Join(g.ID, ar.ID, "status")) -//line app/vmalert/web.qtpl:186 +//line app/vmalert/web.qtpl:187 + qw422016.E().S(prefix + ar.WebLink()) +//line app/vmalert/web.qtpl:187 qw422016.N().S(`">Details</a> </td> </tr> `) -//line app/vmalert/web.qtpl:189 +//line app/vmalert/web.qtpl:190 } -//line app/vmalert/web.qtpl:189 +//line app/vmalert/web.qtpl:190 qw422016.N().S(` </tbody> </table> `) -//line app/vmalert/web.qtpl:192 +//line app/vmalert/web.qtpl:193 } -//line app/vmalert/web.qtpl:192 +//line app/vmalert/web.qtpl:193 qw422016.N().S(` </div> <br> `) -//line app/vmalert/web.qtpl:195 +//line app/vmalert/web.qtpl:196 } -//line app/vmalert/web.qtpl:195 +//line app/vmalert/web.qtpl:196 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:197 +//line app/vmalert/web.qtpl:198 } else { -//line app/vmalert/web.qtpl:197 +//line app/vmalert/web.qtpl:198 qw422016.N().S(` <div> <p>No items...</p> </div> `) -//line app/vmalert/web.qtpl:201 +//line app/vmalert/web.qtpl:202 } -//line app/vmalert/web.qtpl:201 +//line app/vmalert/web.qtpl:202 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:203 +//line app/vmalert/web.qtpl:204 tpl.StreamFooter(qw422016, r) -//line app/vmalert/web.qtpl:203 +//line app/vmalert/web.qtpl:204 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 } -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 func WriteListAlerts(qq422016 qtio422016.Writer, r *http.Request, groupAlerts []GroupAlerts) { -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 StreamListAlerts(qw422016, r, groupAlerts) -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 qt422016.ReleaseWriter(qw422016) -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 } -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 func ListAlerts(r *http.Request, groupAlerts []GroupAlerts) string { -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 WriteListAlerts(qb422016, r, groupAlerts) -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 qs422016 := string(qb422016.B) -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 return qs422016 -//line app/vmalert/web.qtpl:205 +//line app/vmalert/web.qtpl:206 } -//line app/vmalert/web.qtpl:207 +//line app/vmalert/web.qtpl:208 func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[notifier.TargetType][]notifier.Target) { -//line app/vmalert/web.qtpl:207 +//line app/vmalert/web.qtpl:208 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:208 +//line app/vmalert/web.qtpl:209 tpl.StreamHeader(qw422016, r, navItems, "Notifiers") -//line app/vmalert/web.qtpl:208 +//line app/vmalert/web.qtpl:209 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:209 +//line app/vmalert/web.qtpl:210 if len(targets) > 0 { -//line app/vmalert/web.qtpl:209 +//line app/vmalert/web.qtpl:210 qw422016.N().S(` <a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a> <a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a> `) -//line app/vmalert/web.qtpl:214 +//line app/vmalert/web.qtpl:215 var keys []string for key := range targets { keys = append(keys, string(key)) } sort.Strings(keys) -//line app/vmalert/web.qtpl:219 +//line app/vmalert/web.qtpl:220 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:221 +//line app/vmalert/web.qtpl:222 for i := range keys { -//line app/vmalert/web.qtpl:221 +//line app/vmalert/web.qtpl:222 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:222 +//line app/vmalert/web.qtpl:223 typeK, ns := keys[i], targets[notifier.TargetType(keys[i])] count := len(ns) -//line app/vmalert/web.qtpl:224 +//line app/vmalert/web.qtpl:225 qw422016.N().S(` <div class="group-heading data-bs-target="rules-`) -//line app/vmalert/web.qtpl:225 +//line app/vmalert/web.qtpl:226 qw422016.E().S(typeK) -//line app/vmalert/web.qtpl:225 +//line app/vmalert/web.qtpl:226 qw422016.N().S(`"> <span class="anchor" id="notifiers-`) -//line app/vmalert/web.qtpl:226 +//line app/vmalert/web.qtpl:227 qw422016.E().S(typeK) -//line app/vmalert/web.qtpl:226 +//line app/vmalert/web.qtpl:227 qw422016.N().S(`"></span> <a href="#notifiers-`) -//line app/vmalert/web.qtpl:227 +//line app/vmalert/web.qtpl:228 qw422016.E().S(typeK) -//line app/vmalert/web.qtpl:227 +//line app/vmalert/web.qtpl:228 qw422016.N().S(`">`) -//line app/vmalert/web.qtpl:227 +//line app/vmalert/web.qtpl:228 qw422016.E().S(typeK) -//line app/vmalert/web.qtpl:227 +//line app/vmalert/web.qtpl:228 qw422016.N().S(` (`) -//line app/vmalert/web.qtpl:227 +//line app/vmalert/web.qtpl:228 qw422016.N().D(count) -//line app/vmalert/web.qtpl:227 +//line app/vmalert/web.qtpl:228 qw422016.N().S(`)</a> </div> <div class="collapse show" id="notifiers-`) -//line app/vmalert/web.qtpl:229 +//line app/vmalert/web.qtpl:230 qw422016.E().S(typeK) -//line app/vmalert/web.qtpl:229 +//line app/vmalert/web.qtpl:230 qw422016.N().S(`"> <table class="table table-striped table-hover table-sm"> <thead> @@ -772,113 +778,119 @@ func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[n </thead> <tbody> `) -//line app/vmalert/web.qtpl:238 +//line app/vmalert/web.qtpl:239 for _, n := range ns { -//line app/vmalert/web.qtpl:238 +//line app/vmalert/web.qtpl:239 qw422016.N().S(` <tr> <td> `) -//line app/vmalert/web.qtpl:241 +//line app/vmalert/web.qtpl:242 for _, l := range n.Labels { -//line app/vmalert/web.qtpl:241 +//line app/vmalert/web.qtpl:242 qw422016.N().S(` <span class="ms-1 badge bg-primary">`) -//line app/vmalert/web.qtpl:242 +//line app/vmalert/web.qtpl:243 qw422016.E().S(l.Name) -//line app/vmalert/web.qtpl:242 +//line app/vmalert/web.qtpl:243 qw422016.N().S(`=`) -//line app/vmalert/web.qtpl:242 +//line app/vmalert/web.qtpl:243 qw422016.E().S(l.Value) -//line app/vmalert/web.qtpl:242 +//line app/vmalert/web.qtpl:243 qw422016.N().S(`</span> `) -//line app/vmalert/web.qtpl:243 +//line app/vmalert/web.qtpl:244 } -//line app/vmalert/web.qtpl:243 +//line app/vmalert/web.qtpl:244 qw422016.N().S(` </td> <td>`) -//line app/vmalert/web.qtpl:245 +//line app/vmalert/web.qtpl:246 qw422016.E().S(n.Notifier.Addr()) -//line app/vmalert/web.qtpl:245 +//line app/vmalert/web.qtpl:246 qw422016.N().S(`</td> </tr> `) -//line app/vmalert/web.qtpl:247 +//line app/vmalert/web.qtpl:248 } -//line app/vmalert/web.qtpl:247 +//line app/vmalert/web.qtpl:248 qw422016.N().S(` </tbody> </table> </div> `) -//line app/vmalert/web.qtpl:251 +//line app/vmalert/web.qtpl:252 } -//line app/vmalert/web.qtpl:251 +//line app/vmalert/web.qtpl:252 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:253 +//line app/vmalert/web.qtpl:254 } else { -//line app/vmalert/web.qtpl:253 +//line app/vmalert/web.qtpl:254 qw422016.N().S(` <div> <p>No items...</p> </div> `) -//line app/vmalert/web.qtpl:257 +//line app/vmalert/web.qtpl:258 } -//line app/vmalert/web.qtpl:257 +//line app/vmalert/web.qtpl:258 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:259 +//line app/vmalert/web.qtpl:260 tpl.StreamFooter(qw422016, r) -//line app/vmalert/web.qtpl:259 +//line app/vmalert/web.qtpl:260 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 } -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 func WriteListTargets(qq422016 qtio422016.Writer, r *http.Request, targets map[notifier.TargetType][]notifier.Target) { -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 StreamListTargets(qw422016, r, targets) -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 qt422016.ReleaseWriter(qw422016) -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 } -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 func ListTargets(r *http.Request, targets map[notifier.TargetType][]notifier.Target) string { -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 WriteListTargets(qb422016, r, targets) -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 qs422016 := string(qb422016.B) -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 return qs422016 -//line app/vmalert/web.qtpl:261 +//line app/vmalert/web.qtpl:262 } -//line app/vmalert/web.qtpl:263 +//line app/vmalert/web.qtpl:264 func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) { -//line app/vmalert/web.qtpl:263 +//line app/vmalert/web.qtpl:264 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:264 - tpl.StreamHeader(qw422016, r, navItems, "") -//line app/vmalert/web.qtpl:264 +//line app/vmalert/web.qtpl:265 + prefix := utils.Prefix(r.URL.Path) + +//line app/vmalert/web.qtpl:265 qw422016.N().S(` `) //line app/vmalert/web.qtpl:266 + tpl.StreamHeader(qw422016, r, navItems, "") +//line app/vmalert/web.qtpl:266 + qw422016.N().S(` + `) +//line app/vmalert/web.qtpl:268 var labelKeys []string for k := range alert.Labels { labelKeys = append(labelKeys, k) @@ -891,28 +903,28 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) { } sort.Strings(annotationKeys) -//line app/vmalert/web.qtpl:277 +//line app/vmalert/web.qtpl:279 qw422016.N().S(` <div class="display-6 pb-3 mb-3">`) -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 qw422016.E().S(alert.Name) -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 qw422016.N().S(`<span class="ms-2 badge `) -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 if alert.State == "firing" { -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 qw422016.N().S(`bg-danger`) -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 } else { -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 qw422016.N().S(` bg-warning text-dark`) -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 } -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 qw422016.N().S(`">`) -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 qw422016.E().S(alert.State) -//line app/vmalert/web.qtpl:278 +//line app/vmalert/web.qtpl:280 qw422016.N().S(`</span></div> <div class="container border-bottom p-2"> <div class="row"> @@ -921,9 +933,9 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) { </div> <div class="col"> `) -//line app/vmalert/web.qtpl:285 +//line app/vmalert/web.qtpl:287 qw422016.E().S(alert.ActiveAt.Format("2006-01-02T15:04:05Z07:00")) -//line app/vmalert/web.qtpl:285 +//line app/vmalert/web.qtpl:287 qw422016.N().S(` </div> </div> @@ -935,9 +947,9 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) { </div> <div class="col"> <code><pre>`) -//line app/vmalert/web.qtpl:295 +//line app/vmalert/web.qtpl:297 qw422016.E().S(alert.Expression) -//line app/vmalert/web.qtpl:295 +//line app/vmalert/web.qtpl:297 qw422016.N().S(`</pre></code> </div> </div> @@ -949,23 +961,23 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) { </div> <div class="col"> `) -//line app/vmalert/web.qtpl:305 +//line app/vmalert/web.qtpl:307 for _, k := range labelKeys { -//line app/vmalert/web.qtpl:305 +//line app/vmalert/web.qtpl:307 qw422016.N().S(` <span class="m-1 badge bg-primary">`) -//line app/vmalert/web.qtpl:306 +//line app/vmalert/web.qtpl:308 qw422016.E().S(k) -//line app/vmalert/web.qtpl:306 +//line app/vmalert/web.qtpl:308 qw422016.N().S(`=`) -//line app/vmalert/web.qtpl:306 +//line app/vmalert/web.qtpl:308 qw422016.E().S(alert.Labels[k]) -//line app/vmalert/web.qtpl:306 +//line app/vmalert/web.qtpl:308 qw422016.N().S(`</span> `) -//line app/vmalert/web.qtpl:307 +//line app/vmalert/web.qtpl:309 } -//line app/vmalert/web.qtpl:307 +//line app/vmalert/web.qtpl:309 qw422016.N().S(` </div> </div> @@ -977,24 +989,24 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) { </div> <div class="col"> `) -//line app/vmalert/web.qtpl:317 +//line app/vmalert/web.qtpl:319 for _, k := range annotationKeys { -//line app/vmalert/web.qtpl:317 +//line app/vmalert/web.qtpl:319 qw422016.N().S(` <b>`) -//line app/vmalert/web.qtpl:318 +//line app/vmalert/web.qtpl:320 qw422016.E().S(k) -//line app/vmalert/web.qtpl:318 +//line app/vmalert/web.qtpl:320 qw422016.N().S(`:</b><br> <p>`) -//line app/vmalert/web.qtpl:319 +//line app/vmalert/web.qtpl:321 qw422016.E().S(alert.Annotations[k]) -//line app/vmalert/web.qtpl:319 +//line app/vmalert/web.qtpl:321 qw422016.N().S(`</p> `) -//line app/vmalert/web.qtpl:320 +//line app/vmalert/web.qtpl:322 } -//line app/vmalert/web.qtpl:320 +//line app/vmalert/web.qtpl:322 qw422016.N().S(` </div> </div> @@ -1005,14 +1017,18 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) { Group </div> <div class="col"> - <a target="_blank" href="/groups#group-`) -//line app/vmalert/web.qtpl:330 + <a target="_blank" href="`) +//line app/vmalert/web.qtpl:332 + qw422016.E().S(prefix) +//line app/vmalert/web.qtpl:332 + qw422016.N().S(`groups#group-`) +//line app/vmalert/web.qtpl:332 qw422016.E().S(alert.GroupID) -//line app/vmalert/web.qtpl:330 +//line app/vmalert/web.qtpl:332 qw422016.N().S(`">`) -//line app/vmalert/web.qtpl:330 +//line app/vmalert/web.qtpl:332 qw422016.E().S(alert.GroupID) -//line app/vmalert/web.qtpl:330 +//line app/vmalert/web.qtpl:332 qw422016.N().S(`</a> </div> </div> @@ -1024,132 +1040,132 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) { </div> <div class="col"> <a target="_blank" href="`) -//line app/vmalert/web.qtpl:340 +//line app/vmalert/web.qtpl:342 qw422016.E().S(alert.SourceLink) -//line app/vmalert/web.qtpl:340 +//line app/vmalert/web.qtpl:342 qw422016.N().S(`">Link</a> </div> </div> </div> `) -//line app/vmalert/web.qtpl:344 +//line app/vmalert/web.qtpl:346 tpl.StreamFooter(qw422016, r) -//line app/vmalert/web.qtpl:344 +//line app/vmalert/web.qtpl:346 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 } -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 func WriteAlert(qq422016 qtio422016.Writer, r *http.Request, alert *APIAlert) { -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 StreamAlert(qw422016, r, alert) -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 qt422016.ReleaseWriter(qw422016) -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 } -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 func Alert(r *http.Request, alert *APIAlert) string { -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 WriteAlert(qb422016, r, alert) -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 qs422016 := string(qb422016.B) -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 return qs422016 -//line app/vmalert/web.qtpl:346 +//line app/vmalert/web.qtpl:348 } -//line app/vmalert/web.qtpl:348 +//line app/vmalert/web.qtpl:350 func streambadgeState(qw422016 *qt422016.Writer, state string) { -//line app/vmalert/web.qtpl:348 +//line app/vmalert/web.qtpl:350 qw422016.N().S(` `) -//line app/vmalert/web.qtpl:350 +//line app/vmalert/web.qtpl:352 badgeClass := "bg-warning text-dark" if state == "firing" { badgeClass = "bg-danger" } -//line app/vmalert/web.qtpl:354 +//line app/vmalert/web.qtpl:356 qw422016.N().S(` <span class="badge `) -//line app/vmalert/web.qtpl:355 +//line app/vmalert/web.qtpl:357 qw422016.E().S(badgeClass) -//line app/vmalert/web.qtpl:355 +//line app/vmalert/web.qtpl:357 qw422016.N().S(`">`) -//line app/vmalert/web.qtpl:355 +//line app/vmalert/web.qtpl:357 qw422016.E().S(state) -//line app/vmalert/web.qtpl:355 +//line app/vmalert/web.qtpl:357 qw422016.N().S(`</span> `) -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 } -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 func writebadgeState(qq422016 qtio422016.Writer, state string) { -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 streambadgeState(qw422016, state) -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 qt422016.ReleaseWriter(qw422016) -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 } -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 func badgeState(state string) string { -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 writebadgeState(qb422016, state) -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 qs422016 := string(qb422016.B) -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 return qs422016 -//line app/vmalert/web.qtpl:356 +//line app/vmalert/web.qtpl:358 } -//line app/vmalert/web.qtpl:358 +//line app/vmalert/web.qtpl:360 func streambadgeRestored(qw422016 *qt422016.Writer) { -//line app/vmalert/web.qtpl:358 +//line app/vmalert/web.qtpl:360 qw422016.N().S(` <span class="badge bg-warning text-dark" title="Alert state was restored after the service restart from remote storage">restored</span> `) -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 } -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 func writebadgeRestored(qq422016 qtio422016.Writer) { -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 qw422016 := qt422016.AcquireWriter(qq422016) -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 streambadgeRestored(qw422016) -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 qt422016.ReleaseWriter(qw422016) -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 } -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 func badgeRestored() string { -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 qb422016 := qt422016.AcquireByteBuffer() -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 writebadgeRestored(qb422016) -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 qs422016 := string(qb422016.B) -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 qt422016.ReleaseByteBuffer(qb422016) -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 return qs422016 -//line app/vmalert/web.qtpl:360 +//line app/vmalert/web.qtpl:362 } diff --git a/app/vmalert/web_test.go b/app/vmalert/web_test.go index ef639b11bc..af77967ec4 100644 --- a/app/vmalert/web_test.go +++ b/app/vmalert/web_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "reflect" @@ -29,7 +30,7 @@ func TestHandler(t *testing.T) { t.Helper() resp, err := http.Get(url) if err != nil { - t.Errorf("unexpected err %s", err) + t.Fatalf("unexpected err %s", err) } if code != resp.StatusCode { t.Errorf("unexpected status code %d want %d", resp.StatusCode, code) @@ -47,20 +48,72 @@ func TestHandler(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })) defer ts.Close() + + t.Run("/", func(t *testing.T) { + getResp(ts.URL, nil, 200) + getResp(ts.URL+"/vmalert", nil, 200) + getResp(ts.URL+"/vmalert/home", nil, 200) + }) + t.Run("/api/v1/alerts", func(t *testing.T) { lr := listAlertsResponse{} getResp(ts.URL+"/api/v1/alerts", &lr, 200) if length := len(lr.Data.Alerts); length != 1 { t.Errorf("expected 1 alert got %d", length) } + + lr = listAlertsResponse{} + getResp(ts.URL+"/vmalert/api/v1/alerts", &lr, 200) + if length := len(lr.Data.Alerts); length != 1 { + t.Errorf("expected 1 alert got %d", length) + } }) + t.Run("/api/v1/alert?alertID&groupID", func(t *testing.T) { + expAlert := ar.newAlertAPI(*ar.alerts[0]) + alert := &APIAlert{} + getResp(ts.URL+"/"+expAlert.APILink(), alert, 200) + if !reflect.DeepEqual(alert, expAlert) { + t.Errorf("expected %v is equal to %v", alert, expAlert) + } + + alert = &APIAlert{} + getResp(ts.URL+"/vmalert/"+expAlert.APILink(), alert, 200) + if !reflect.DeepEqual(alert, expAlert) { + t.Errorf("expected %v is equal to %v", alert, expAlert) + } + }) + + t.Run("/api/v1/alert?badParams", func(t *testing.T) { + params := fmt.Sprintf("?%s=0&%s=1", paramGroupID, paramAlertID) + getResp(ts.URL+"/api/v1/alert"+params, nil, 404) + getResp(ts.URL+"/vmalert/api/v1/alert"+params, nil, 404) + + params = fmt.Sprintf("?%s=1&%s=0", paramGroupID, paramAlertID) + getResp(ts.URL+"/api/v1/alert"+params, nil, 404) + getResp(ts.URL+"/vmalert/api/v1/alert"+params, nil, 404) + + // bad request, alertID is missing + params = fmt.Sprintf("?%s=1", paramGroupID) + getResp(ts.URL+"/api/v1/alert"+params, nil, 400) + getResp(ts.URL+"/vmalert/api/v1/alert"+params, nil, 400) + }) + t.Run("/api/v1/rules", func(t *testing.T) { lr := listGroupsResponse{} getResp(ts.URL+"/api/v1/rules", &lr, 200) if length := len(lr.Data.Groups); length != 1 { t.Errorf("expected 1 group got %d", length) } + + lr = listGroupsResponse{} + getResp(ts.URL+"/vmalert/api/v1/rules", &lr, 200) + if length := len(lr.Data.Groups); length != 1 { + t.Errorf("expected 1 group got %d", length) + } }) + + // check deprecated links support + // TODO: remove as soon as deprecated links removed t.Run("/api/v1/0/0/status", func(t *testing.T) { alert := &APIAlert{} getResp(ts.URL+"/api/v1/0/0/status", alert, 200) @@ -75,7 +128,5 @@ func TestHandler(t *testing.T) { t.Run("/api/v1/1/0/status", func(t *testing.T) { getResp(ts.URL+"/api/v1/1/0/status", nil, 404) }) - t.Run("/", func(t *testing.T) { - getResp(ts.URL, nil, 200) - }) + } diff --git a/app/vmalert/web_types.go b/app/vmalert/web_types.go index fad7be4710..b2e150807e 100644 --- a/app/vmalert/web_types.go +++ b/app/vmalert/web_types.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "time" ) @@ -33,6 +34,18 @@ type APIAlert struct { Restored bool `json:"restored"` } +// WebLink returns a link to the alert which can be used in UI. +func (aa *APIAlert) WebLink() string { + return fmt.Sprintf("alert?%s=%s&%s=%s", + paramGroupID, aa.GroupID, paramAlertID, aa.ID) +} + +// APILink returns a link to the alert's JSON representation. +func (aa *APIAlert) APILink() string { + return fmt.Sprintf("api/v1/alert?%s=%s&%s=%s", + paramGroupID, aa.GroupID, paramAlertID, aa.ID) +} + // APIGroup represents Group for WEB view // https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules type APIGroup struct { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 095cc9f7d2..98b18d3ef3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,6 +18,12 @@ The following tip changes can be tested by building VictoriaMetrics components f **Update notes:** this release introduces backwards-incompatible changes to `vm_partial_results_total` metric by changing its labels to be consistent with `vm_requests_total` metric. If you use alerting rules or Grafana dashboards, which rely on this metric, then they must be updated. The official dashboards for VictoriaMetrics don't use this metric. +[vmalert](https://docs.victoriametrics.com/vmalert.html) routing was updated according to [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2825). +The change may affect users with `-http.pathPrefix` flag configured for vmalert. With the update, configuring this flag is no longer needed. Here's [why](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2799#issuecomment-1171392005). + +* CHANGE: [vmalert](https://docs.victoriametrics.com/vmalert.html): deprecate alert's status link `/api/v1/<groupID>/<alertID>/status` in favour of `api/v1/alert?group_id=<group_id>&alert_id=<alert_id>"`. +The old alert's status link will be still supported for a few versions but will be removed in the future. + * FEATURE: [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): add support for querying lower-level `vmselect` nodes from upper-level `vmselect` nodes. This makes possible to build multi-level cluster setups for global querying view and HA purposes without the need to use [Promxy](https://github.com/jacksontj/promxy). See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multi-level-cluster-setup) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2778). * FEATURE: add `-search.setLookbackToStep` command-line flag, which enables InfluxDB-like gap filling during querying. See [these docs](https://docs.victoriametrics.com/guides/migrate-from-influx.html) for details. * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add an UI for [query tracing](https://docs.victoriametrics.com/#query-tracing). It can be enabled by clicking `trace query` checkbox and re-running the query. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2703). diff --git a/docs/vmalert.md b/docs/vmalert.md index e11cb61b76..0da33a649c 100644 --- a/docs/vmalert.md +++ b/docs/vmalert.md @@ -485,7 +485,7 @@ or time series modification via [relabeling](https://docs.victoriametrics.com/vm * `http://<vmalert-addr>` - UI; * `http://<vmalert-addr>/api/v1/rules` - list of all loaded groups and rules; * `http://<vmalert-addr>/api/v1/alerts` - list of all active alerts; -* `http://<vmalert-addr>/api/v1/<groupID>/<alertID>/status"` - get alert status by ID. +* `http://<vmalert-addr>/vmalert/api/v1/alert?group_id=<group_id>&alert_id=<alert_id>"` - get alert status by ID. Used as alert source in AlertManager. * `http://<vmalert-addr>/metrics` - application metrics. * `http://<vmalert-addr>/-/reload` - hot configuration reload. @@ -685,7 +685,7 @@ The shortlist of configuration flags is the following: How often to evaluate the rules (default 1m0s) -external.alert.source string External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. - eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used + eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/vmalert/api/v1/alert?group_id=&alert_id=' is used -external.label array Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets. Supports an array of values separated by comma or specified via multiple flags.