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;
This commit is contained in:
Roman Khavronenko 2021-09-07 22:39:22 +03:00 committed by Aliaksandr Valialkin
parent 83356af4c7
commit bb26aa9b1f
13 changed files with 1638 additions and 18 deletions

View file

@ -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:

View file

@ -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://<vmalert-addr>` - UI;
* `http://<vmalert-addr>/api/v1/groups` - 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.

View file

@ -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),
}
}

View file

@ -0,0 +1,36 @@
{% func Footer() %}
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
function expandAll() {
$('.collapse').addClass('show');
}
function collapseAll() {
$('.collapse').removeClass('show');
}
$(document).ready(function() {
// prevent collapse logic on link click
$(".group-heading a").click(function(e) {
e.stopPropagation();
});
$(".group-heading").click(function(e) {
let target = $(this).attr('data-bs-target');
let el = $('#'+target);
new bootstrap.Collapse(el, {
toggle: true
});
});
var hash = window.location.hash.substr(1);
let group = $('#'+hash);
if (group.length > 0) {
group.click();
}
});
</script>
</body>
</html>
{% endfunc %}

View file

@ -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(`
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
function expandAll() {
$('.collapse').addClass('show');
}
function collapseAll() {
$('.collapse').removeClass('show');
}
$(document).ready(function() {
// prevent collapse logic on link click
$(".group-heading a").click(function(e) {
e.stopPropagation();
});
$(".group-heading").click(function(e) {
let target = $(this).attr('data-bs-target');
let el = $('#'+target);
new bootstrap.Collapse(el, {
toggle: true
});
});
var hash = window.location.hash.substr(1);
let group = $('#'+hash);
if (group.length > 0) {
group.click();
}
});
</script>
</body>
</html>
`)
//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
}

View file

@ -0,0 +1,43 @@
{% func Header(title string, pages []NavItem) %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>vmalert{% if title != "" %} - {%s title %}{% endif %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<style>
body{
min-height: 75rem;
padding-top: 4.5rem;
}
pre {
overflow: scroll;
max-width: 600px;
min-height: 30px;
}
.group-heading {
cursor: pointer;
padding: 5px;
margin-top: 5px;
position: relative;
}
.group-heading .anchor {
position:absolute;
top:-60px;
}
.group-heading span {
float: right;
margin-left: 5px;
margin-right: 5px;
}
.group-heading:hover {
background-color: #f8f9fa!important;
}
.table .error-cell{
word-break: break-word;
}
</style>
</head>
<body>
{%= PrintNavItems(title, pages) %}
<main class="px-2">
{% endfunc %}

View file

@ -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(`
<!DOCTYPE html>
<html lang="en">
<head>
<title>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(`</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<style>
body{
min-height: 75rem;
padding-top: 4.5rem;
}
pre {
overflow: scroll;
max-width: 600px;
min-height: 30px;
}
.group-heading {
cursor: pointer;
padding: 5px;
margin-top: 5px;
position: relative;
}
.group-heading .anchor {
position:absolute;
top:-60px;
}
.group-heading span {
float: right;
margin-left: 5px;
margin-right: 5px;
}
.group-heading:hover {
background-color: #f8f9fa!important;
}
.table .error-cell{
word-break: break-word;
}
</style>
</head>
<body>
`)
//line app/vmalert/tpl/header.qtpl:41
StreamPrintNavItems(qw422016, title, pages)
//line app/vmalert/tpl/header.qtpl:41
qw422016.N().S(`
<main class="px-2">
`)
//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
}

25
app/vmalert/tpl/nav.qtpl Normal file
View file

@ -0,0 +1,25 @@
{% code
type NavItem struct {
Name string
Url string
}
%}
{% func PrintNavItems(current string, items []NavItem) %}
<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">
{% for _, item := range items %}
<li class="nav-item">
<a class="nav-link{% if current == item.Name %} active{% endif %}" href="{%s item.Url %}">
{%s item.Name %}
</a>
</li>
{% endfor %}
</ul>
</div>
</nav>
{% endfunc %}

View file

@ -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(`
<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/nav.qtpl:13
for _, item := range items {
//line app/vmalert/tpl/nav.qtpl:13
qw422016.N().S(`
<li class="nav-item">
<a class="nav-link`)
//line app/vmalert/tpl/nav.qtpl:15
if current == item.Name {
//line app/vmalert/tpl/nav.qtpl:15
qw422016.N().S(` active`)
//line app/vmalert/tpl/nav.qtpl:15
}
//line app/vmalert/tpl/nav.qtpl:15
qw422016.N().S(`" href="`)
//line app/vmalert/tpl/nav.qtpl:15
qw422016.E().S(item.Url)
//line app/vmalert/tpl/nav.qtpl:15
qw422016.N().S(`">
`)
//line app/vmalert/tpl/nav.qtpl:16
qw422016.E().S(item.Name)
//line app/vmalert/tpl/nav.qtpl:16
qw422016.N().S(`
</a>
</li>
`)
//line app/vmalert/tpl/nav.qtpl:19
}
//line app/vmalert/tpl/nav.qtpl:19
qw422016.N().S(`
</ul>
</div>
</nav>
`)
//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
}

View file

@ -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/<groupName>/<alertID>/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/<groupID>/<alertID>/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
}
// <groupID>/<alertID>/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) {

278
app/vmalert/web.qtpl Normal file
View file

@ -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) %}
<p>
API:<br>
{% for _, p := range pathList %}
{%code
p, doc := p[0], p[1]
%}
<a href="{%s p %}">{%s p %}</a> - {%s doc %}<br/>
{% endfor %}
</p>
{%= 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]++
}
}
}
%}
<a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
<a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a>
{% for _, g := range groups %}
<div class="group-heading{% if rNotOk[g.Name] > 0 %} alert-danger{% endif %}" data-bs-target="rules-{%s g.ID %}">
<span class="anchor" id="group-{%s g.ID %}"></span>
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%s g.Interval %})</a>
{% if rNotOk[g.Name] > 0 %}<span class="badge bg-danger" title="Number of rules withs status Error">{%d rNotOk[g.Name] %}</span> {% endif %}
<span class="badge bg-success" title="Number of rules withs status Ok">{%d rOk[g.Name] %}</span>
<p class="fs-6 fw-lighter">{%s g.File %}</p>
</div>
<div class="collapse" id="rules-{%s g.ID %}">
<table class="table table-striped table-hover table-sm">
<thead>
<tr>
<th scope="col">Rule</th>
<th scope="col" title="Shows if rule's execution ended with error">Error</th>
<th scope="col" title="How many samples were produced by the rule">Samples</th>
<th scope="col" title="How many seconds ago rule was executed">Updated</th>
</tr>
</thead>
<tbody>
{% for _, ar := range g.AlertingRules %}
<tr{% if ar.LastError != "" %} class="alert-danger"{% endif %}>
<td>
<b>alert:</b> {%s ar.Name %} (for: {%v ar.For %})<br>
<code><pre>{%s ar.Expression %}</pre></code><br>
{% if len(ar.Labels) > 0 %} <b>Labels:</b>{% endif %}
{% for k, v := range ar.Labels %}
<span class="ms-1 badge bg-primary">{%s k %}={%s v %}</span>
{% endfor %}
</td>
<td><div class="error-cell">{%s ar.LastError %}</div></td>
<td>{%d ar.LastSamples %}</td>
<td>{%f.3 time.Since(ar.LastExec).Seconds() %}s ago</td>
</tr>
{% endfor %}
{% for _, rr := range g.RecordingRules %}
<tr>
<td>
<b>record:</b> {%s rr.Name %}<br>
<code><pre>{%s rr.Expression %}</pre></code>
{% if len(rr.Labels) > 0 %} <b>Labels:</b>{% endif %}
{% for k, v := range rr.Labels %}
<span class="ms-1 badge bg-primary">{%s k %}={%s v %}</span>
{% endfor %}
</td>
<td><div class="error-cell">{%s rr.LastError %}</div></td>
<td>{%d rr.LastSamples %}</td>
<td>{%f.3 time.Since(rr.LastExec).Seconds() %}s ago</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
{% else %}
<div>
<p>No items...</p>
</div>
{% endif %}
{%= tpl.Footer() %}
{% endfunc %}
{% func ListAlerts(groupAlerts []GroupAlerts) %}
{%= tpl.Header("Alerts", navItems) %}
{% if len(groupAlerts) > 0 %}
<a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
<a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a>
{% for _, ga := range groupAlerts %}
{%code g := ga.Group %}
<div class="group-heading alert-danger" data-bs-target="rules-{%s g.ID %}">
<span class="anchor" id="group-{%s g.ID %}"></span>
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %}</a>
<span class="badge bg-danger" title="Number of active alerts">{%d len(ga.Alerts) %}</span>
<br>
<p class="fs-6 fw-lighter">{%s g.File %}</p>
</div>
{%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)
%}
<div class="collapse" id="rules-{%s g.ID %}">
{% for _, ruleID := range keys %}
{%code
defaultAR := alertsByRule[ruleID][0]
var labelKeys []string
for k := range defaultAR.Labels {
labelKeys = append(labelKeys, k)
}
sort.Strings(labelKeys)
%}
<br>
<b>alert:</b> {%s defaultAR.Name %} ({%d len(alertsByRule[ruleID]) %})<br>
<b>expr:</b><code><pre>{%s defaultAR.Expression %}</pre></code>
<table class="table table-striped table-hover table-sm">
<thead>
<tr>
<th scope="col">Labels</th>
<th scope="col">State</th>
<th scope="col">Active at</th>
<th scope="col">Value</th>
<th scope="col">Link</th>
</tr>
</thead>
<tbody>
{% for _, ar := range alertsByRule[ruleID] %}
<tr>
<td>
{% for _, k := range labelKeys %}
<span class="ms-1 badge bg-primary">{%s k %}={%s ar.Labels[k] %}</span>
{% endfor %}
</td>
<td><span class="badge {% if ar.State=="firing" %}bg-danger{% else %} bg-warning text-dark{% endif %}">{%s ar.State %}</span></td>
<td>{%s ar.ActiveAt.Format("2006-01-02T15:04:05Z07:00") %}</td>
<td>{%s ar.Value %}</td>
<td>
<a href="/{%s g.ID %}/{%s ar.ID %}/status">Details</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
</div>
<br>
{% endfor %}
{% else %}
<div>
<p>No items...</p>
</div>
{% 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)
%}
<div class="display-6 pb-3 mb-3">{%s alert.Name %}<span class="ms-2 badge {% if alert.State=="firing" %}bg-danger{% else %} bg-warning text-dark{% endif %}">{%s alert.State %}</span></div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Active at
</div>
<div class="col">
{%s alert.ActiveAt.Format("2006-01-02T15:04:05Z07:00") %}
</div>
</div>
</div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Expr
</div>
<div class="col">
<code><pre>{%s alert.Expression %}</pre></code>
</div>
</div>
</div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Labels
</div>
<div class="col">
{% for _, k := range labelKeys %}
<span class="m-1 badge bg-primary">{%s k %}={%s alert.Labels[k] %}</span>
{% endfor %}
</div>
</div>
</div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Annotations
</div>
<div class="col">
{% for _, k := range annotationKeys %}
<b>{%s k %}:</b><br>
<p>{%s alert.Annotations[k] %}</p>
{% endfor %}
</div>
</div>
</div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Group
</div>
<div class="col">
<a target="_blank" href="/groups#group-{%s alert.GroupID %}">{%s alert.GroupID %}</a>
</div>
</div>
</div>
{%= tpl.Footer() %}
{% endfunc %}

889
app/vmalert/web.qtpl.go Normal file
View file

@ -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(`
<p>
API:<br>
`)
//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(`
<a href="`)
//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(p)
//line app/vmalert/web.qtpl:28
qw422016.N().S(`</a> - `)
//line app/vmalert/web.qtpl:28
qw422016.E().S(doc)
//line app/vmalert/web.qtpl:28
qw422016.N().S(`<br/>
`)
//line app/vmalert/web.qtpl:29
}
//line app/vmalert/web.qtpl:29
qw422016.N().S(`
</p>
`)
//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(`
<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:59
for _, g := range groups {
//line app/vmalert/web.qtpl:59
qw422016.N().S(`
<div class="group-heading`)
//line app/vmalert/web.qtpl:60
if rNotOk[g.Name] > 0 {
//line app/vmalert/web.qtpl:60
qw422016.N().S(` alert-danger`)
//line app/vmalert/web.qtpl:60
}
//line app/vmalert/web.qtpl:60
qw422016.N().S(`" data-bs-target="rules-`)
//line app/vmalert/web.qtpl:60
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:60
qw422016.N().S(`">
<span class="anchor" id="group-`)
//line app/vmalert/web.qtpl:61
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:61
qw422016.N().S(`"></span>
<a href="#group-`)
//line app/vmalert/web.qtpl:62
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:62
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(`)</a>
`)
//line app/vmalert/web.qtpl:63
if rNotOk[g.Name] > 0 {
//line app/vmalert/web.qtpl:63
qw422016.N().S(`<span class="badge bg-danger" title="Number of rules withs status Error">`)
//line app/vmalert/web.qtpl:63
qw422016.N().D(rNotOk[g.Name])
//line app/vmalert/web.qtpl:63
qw422016.N().S(`</span> `)
//line app/vmalert/web.qtpl:63
}
//line app/vmalert/web.qtpl:63
qw422016.N().S(`
<span class="badge bg-success" title="Number of rules withs status Ok">`)
//line app/vmalert/web.qtpl:64
qw422016.N().D(rOk[g.Name])
//line app/vmalert/web.qtpl:64
qw422016.N().S(`</span>
<p class="fs-6 fw-lighter">`)
//line app/vmalert/web.qtpl:65
qw422016.E().S(g.File)
//line app/vmalert/web.qtpl:65
qw422016.N().S(`</p>
</div>
<div class="collapse" id="rules-`)
//line app/vmalert/web.qtpl:67
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:67
qw422016.N().S(`">
<table class="table table-striped table-hover table-sm">
<thead>
<tr>
<th scope="col">Rule</th>
<th scope="col" title="Shows if rule's execution ended with error">Error</th>
<th scope="col" title="How many samples were produced by the rule">Samples</th>
<th scope="col" title="How many seconds ago rule was executed">Updated</th>
</tr>
</thead>
<tbody>
`)
//line app/vmalert/web.qtpl:78
for _, ar := range g.AlertingRules {
//line app/vmalert/web.qtpl:78
qw422016.N().S(`
<tr`)
//line app/vmalert/web.qtpl:79
if ar.LastError != "" {
//line app/vmalert/web.qtpl:79
qw422016.N().S(` class="alert-danger"`)
//line app/vmalert/web.qtpl:79
}
//line app/vmalert/web.qtpl:79
qw422016.N().S(`>
<td>
<b>alert:</b> `)
//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(`)<br>
<code><pre>`)
//line app/vmalert/web.qtpl:82
qw422016.E().S(ar.Expression)
//line app/vmalert/web.qtpl:82
qw422016.N().S(`</pre></code><br>
`)
//line app/vmalert/web.qtpl:83
if len(ar.Labels) > 0 {
//line app/vmalert/web.qtpl:83
qw422016.N().S(` <b>Labels:</b>`)
//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(`
<span class="ms-1 badge bg-primary">`)
//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(`</span>
`)
//line app/vmalert/web.qtpl:86
}
//line app/vmalert/web.qtpl:86
qw422016.N().S(`
</td>
<td><div class="error-cell">`)
//line app/vmalert/web.qtpl:88
qw422016.E().S(ar.LastError)
//line app/vmalert/web.qtpl:88
qw422016.N().S(`</div></td>
<td>`)
//line app/vmalert/web.qtpl:89
qw422016.N().D(ar.LastSamples)
//line app/vmalert/web.qtpl:89
qw422016.N().S(`</td>
<td>`)
//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</td>
</tr>
`)
//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(`
<tr>
<td>
<b>record:</b> `)
//line app/vmalert/web.qtpl:96
qw422016.E().S(rr.Name)
//line app/vmalert/web.qtpl:96
qw422016.N().S(`<br>
<code><pre>`)
//line app/vmalert/web.qtpl:97
qw422016.E().S(rr.Expression)
//line app/vmalert/web.qtpl:97
qw422016.N().S(`</pre></code>
`)
//line app/vmalert/web.qtpl:98
if len(rr.Labels) > 0 {
//line app/vmalert/web.qtpl:98
qw422016.N().S(` <b>Labels:</b>`)
//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(`
<span class="ms-1 badge bg-primary">`)
//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(`</span>
`)
//line app/vmalert/web.qtpl:101
}
//line app/vmalert/web.qtpl:101
qw422016.N().S(`
</td>
<td><div class="error-cell">`)
//line app/vmalert/web.qtpl:103
qw422016.E().S(rr.LastError)
//line app/vmalert/web.qtpl:103
qw422016.N().S(`</div></td>
<td>`)
//line app/vmalert/web.qtpl:104
qw422016.N().D(rr.LastSamples)
//line app/vmalert/web.qtpl:104
qw422016.N().S(`</td>
<td>`)
//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</td>
</tr>
`)
//line app/vmalert/web.qtpl:107
}
//line app/vmalert/web.qtpl:107
qw422016.N().S(`
</tbody>
</table>
</div>
`)
//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(`
<div>
<p>No items...</p>
</div>
`)
//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(`
<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: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(`
<div class="group-heading alert-danger" data-bs-target="rules-`)
//line app/vmalert/web.qtpl:131
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:131
qw422016.N().S(`">
<span class="anchor" id="group-`)
//line app/vmalert/web.qtpl:132
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:132
qw422016.N().S(`"></span>
<a href="#group-`)
//line app/vmalert/web.qtpl:133
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:133
qw422016.N().S(`">`)
//line app/vmalert/web.qtpl:133
qw422016.E().S(g.Name)
//line app/vmalert/web.qtpl:133
if g.Type != "prometheus" {
//line app/vmalert/web.qtpl:133
qw422016.N().S(` (`)
//line app/vmalert/web.qtpl:133
qw422016.E().S(g.Type)
//line app/vmalert/web.qtpl:133
qw422016.N().S(`)`)
//line app/vmalert/web.qtpl:133
}
//line app/vmalert/web.qtpl:133
qw422016.N().S(`</a>
<span class="badge bg-danger" title="Number of active alerts">`)
//line app/vmalert/web.qtpl:134
qw422016.N().D(len(ga.Alerts))
//line app/vmalert/web.qtpl:134
qw422016.N().S(`</span>
<br>
<p class="fs-6 fw-lighter">`)
//line app/vmalert/web.qtpl:136
qw422016.E().S(g.File)
//line app/vmalert/web.qtpl:136
qw422016.N().S(`</p>
</div>
`)
//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(`
<div class="collapse" id="rules-`)
//line app/vmalert/web.qtpl:149
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:149
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(`
<br>
<b>alert:</b> `)
//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(`)<br>
<b>expr:</b><code><pre>`)
//line app/vmalert/web.qtpl:161
qw422016.E().S(defaultAR.Expression)
//line app/vmalert/web.qtpl:161
qw422016.N().S(`</pre></code>
<table class="table table-striped table-hover table-sm">
<thead>
<tr>
<th scope="col">Labels</th>
<th scope="col">State</th>
<th scope="col">Active at</th>
<th scope="col">Value</th>
<th scope="col">Link</th>
</tr>
</thead>
<tbody>
`)
//line app/vmalert/web.qtpl:173
for _, ar := range alertsByRule[ruleID] {
//line app/vmalert/web.qtpl:173
qw422016.N().S(`
<tr>
<td>
`)
//line app/vmalert/web.qtpl:176
for _, k := range labelKeys {
//line app/vmalert/web.qtpl:176
qw422016.N().S(`
<span class="ms-1 badge bg-primary">`)
//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(`</span>
`)
//line app/vmalert/web.qtpl:178
}
//line app/vmalert/web.qtpl:178
qw422016.N().S(`
</td>
<td><span class="badge `)
//line app/vmalert/web.qtpl:180
if ar.State == "firing" {
//line app/vmalert/web.qtpl:180
qw422016.N().S(`bg-danger`)
//line app/vmalert/web.qtpl:180
} else {
//line app/vmalert/web.qtpl:180
qw422016.N().S(` bg-warning text-dark`)
//line app/vmalert/web.qtpl:180
}
//line app/vmalert/web.qtpl:180
qw422016.N().S(`">`)
//line app/vmalert/web.qtpl:180
qw422016.E().S(ar.State)
//line app/vmalert/web.qtpl:180
qw422016.N().S(`</span></td>
<td>`)
//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(`</td>
<td>`)
//line app/vmalert/web.qtpl:182
qw422016.E().S(ar.Value)
//line app/vmalert/web.qtpl:182
qw422016.N().S(`</td>
<td>
<a href="/`)
//line app/vmalert/web.qtpl:184
qw422016.E().S(g.ID)
//line app/vmalert/web.qtpl:184
qw422016.N().S(`/`)
//line app/vmalert/web.qtpl:184
qw422016.E().S(ar.ID)
//line app/vmalert/web.qtpl:184
qw422016.N().S(`/status">Details</a>
</td>
</tr>
`)
//line app/vmalert/web.qtpl:187
}
//line app/vmalert/web.qtpl:187
qw422016.N().S(`
</tbody>
</table>
`)
//line app/vmalert/web.qtpl:190
}
//line app/vmalert/web.qtpl:190
qw422016.N().S(`
</div>
<br>
`)
//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(`
<div>
<p>No items...</p>
</div>
`)
//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(`
<div class="display-6 pb-3 mb-3">`)
//line app/vmalert/web.qtpl:220
qw422016.E().S(alert.Name)
//line app/vmalert/web.qtpl:220
qw422016.N().S(`<span class="ms-2 badge `)
//line app/vmalert/web.qtpl:220
if alert.State == "firing" {
//line app/vmalert/web.qtpl:220
qw422016.N().S(`bg-danger`)
//line app/vmalert/web.qtpl:220
} else {
//line app/vmalert/web.qtpl:220
qw422016.N().S(` bg-warning text-dark`)
//line app/vmalert/web.qtpl:220
}
//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(`</span></div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Active at
</div>
<div class="col">
`)
//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(`
</div>
</div>
</div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Expr
</div>
<div class="col">
<code><pre>`)
//line app/vmalert/web.qtpl:237
qw422016.E().S(alert.Expression)
//line app/vmalert/web.qtpl:237
qw422016.N().S(`</pre></code>
</div>
</div>
</div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Labels
</div>
<div class="col">
`)
//line app/vmalert/web.qtpl:247
for _, k := range labelKeys {
//line app/vmalert/web.qtpl:247
qw422016.N().S(`
<span class="m-1 badge bg-primary">`)
//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(`</span>
`)
//line app/vmalert/web.qtpl:249
}
//line app/vmalert/web.qtpl:249
qw422016.N().S(`
</div>
</div>
</div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Annotations
</div>
<div class="col">
`)
//line app/vmalert/web.qtpl:259
for _, k := range annotationKeys {
//line app/vmalert/web.qtpl:259
qw422016.N().S(`
<b>`)
//line app/vmalert/web.qtpl:260
qw422016.E().S(k)
//line app/vmalert/web.qtpl:260
qw422016.N().S(`:</b><br>
<p>`)
//line app/vmalert/web.qtpl:261
qw422016.E().S(alert.Annotations[k])
//line app/vmalert/web.qtpl:261
qw422016.N().S(`</p>
`)
//line app/vmalert/web.qtpl:262
}
//line app/vmalert/web.qtpl:262
qw422016.N().S(`
</div>
</div>
</div>
<div class="container border-bottom p-2">
<div class="row">
<div class="col-2">
Group
</div>
<div class="col">
<a target="_blank" href="/groups#group-`)
//line app/vmalert/web.qtpl:272
qw422016.E().S(alert.GroupID)
//line app/vmalert/web.qtpl:272
qw422016.N().S(`">`)
//line app/vmalert/web.qtpl:272
qw422016.E().S(alert.GroupID)
//line app/vmalert/web.qtpl:272
qw422016.N().S(`</a>
</div>
</div>
</div>
`)
//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
}

View file

@ -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
}