mirror of https://github.com/prometheus/prometheus
Merge pull request #826 from prometheus/alerting-ui-improvements
Use TemplateExpander for main pages, improve alerting UIpull/831/head
commit
cc18191b5e
|
@ -73,11 +73,11 @@ type TargetHealth int
|
||||||
func (t TargetHealth) String() string {
|
func (t TargetHealth) String() string {
|
||||||
switch t {
|
switch t {
|
||||||
case HealthUnknown:
|
case HealthUnknown:
|
||||||
return "UNKNOWN"
|
return "unknown"
|
||||||
case HealthGood:
|
case HealthGood:
|
||||||
return "HEALTHY"
|
return "healthy"
|
||||||
case HealthBad:
|
case HealthBad:
|
||||||
return "UNHEALTHY"
|
return "unhealthy"
|
||||||
}
|
}
|
||||||
panic("unknown state")
|
panic("unknown state")
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,23 +209,38 @@ func (rule *AlertingRule) eval(timestamp clientmodel.Timestamp, engine *promql.E
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *AlertingRule) String() string {
|
func (rule *AlertingRule) String() string {
|
||||||
return fmt.Sprintf("ALERT %s IF %s FOR %s WITH %s", rule.name, rule.vector, strutil.DurationToString(rule.holdDuration), rule.labels)
|
s := fmt.Sprintf("ALERT %s", rule.name)
|
||||||
|
s += fmt.Sprintf("\n\tIF %s", rule.vector)
|
||||||
|
if rule.holdDuration > 0 {
|
||||||
|
s += fmt.Sprintf("\n\tFOR %s", strutil.DurationToString(rule.holdDuration))
|
||||||
|
}
|
||||||
|
if len(rule.labels) > 0 {
|
||||||
|
s += fmt.Sprintf("\n\tWITH %s", rule.labels)
|
||||||
|
}
|
||||||
|
s += fmt.Sprintf("\n\tSUMMARY %q", rule.summary)
|
||||||
|
s += fmt.Sprintf("\n\tDESCRIPTION %q", rule.description)
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTMLSnippet returns an HTML snippet representing this alerting rule.
|
// HTMLSnippet returns an HTML snippet representing this alerting rule. The
|
||||||
|
// resulting snippet is expected to be presented in a <pre> element, so that
|
||||||
|
// line breaks and other returned whitespace is respected.
|
||||||
func (rule *AlertingRule) HTMLSnippet(pathPrefix string) template.HTML {
|
func (rule *AlertingRule) HTMLSnippet(pathPrefix string) template.HTML {
|
||||||
alertMetric := clientmodel.Metric{
|
alertMetric := clientmodel.Metric{
|
||||||
clientmodel.MetricNameLabel: alertMetricName,
|
clientmodel.MetricNameLabel: alertMetricName,
|
||||||
alertNameLabel: clientmodel.LabelValue(rule.name),
|
alertNameLabel: clientmodel.LabelValue(rule.name),
|
||||||
}
|
}
|
||||||
return template.HTML(fmt.Sprintf(
|
s := fmt.Sprintf("ALERT <a href=%q>%s</a>", pathPrefix+strutil.GraphLinkForExpression(alertMetric.String()), rule.name)
|
||||||
`ALERT <a href="%s">%s</a> IF <a href="%s">%s</a> FOR %s WITH %s`,
|
s += fmt.Sprintf("\n IF <a href=%q>%s</a>", pathPrefix+strutil.GraphLinkForExpression(rule.vector.String()), rule.vector)
|
||||||
pathPrefix+strutil.GraphLinkForExpression(alertMetric.String()),
|
if rule.holdDuration > 0 {
|
||||||
rule.name,
|
s += fmt.Sprintf("\n FOR %s", strutil.DurationToString(rule.holdDuration))
|
||||||
pathPrefix+strutil.GraphLinkForExpression(rule.vector.String()),
|
}
|
||||||
rule.vector,
|
if len(rule.labels) > 0 {
|
||||||
strutil.DurationToString(rule.holdDuration),
|
s += fmt.Sprintf("\n WITH %s", rule.labels)
|
||||||
rule.labels))
|
}
|
||||||
|
s += fmt.Sprintf("\n SUMMARY %q", rule.summary)
|
||||||
|
s += fmt.Sprintf("\n DESCRIPTION %q", rule.description)
|
||||||
|
return template.HTML(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// State returns the "maximum" state: firing > pending > inactive.
|
// State returns the "maximum" state: firing > pending > inactive.
|
||||||
|
|
|
@ -238,6 +238,13 @@ func NewTemplateExpander(text string, name string, data interface{}, timestamp c
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%.4g%ss", v, prefix)
|
return fmt.Sprintf("%.4g%ss", v, prefix)
|
||||||
},
|
},
|
||||||
|
"humanizeTimestamp": func(v float64) string {
|
||||||
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||||
|
return fmt.Sprintf("%.4g", v)
|
||||||
|
}
|
||||||
|
t := clientmodel.TimestampFromUnixNano(int64(v * 1000000000)).Time()
|
||||||
|
return fmt.Sprint(t)
|
||||||
|
},
|
||||||
"pathPrefix": func() string {
|
"pathPrefix": func() string {
|
||||||
return pathPrefix
|
return pathPrefix
|
||||||
},
|
},
|
||||||
|
@ -245,6 +252,14 @@ func NewTemplateExpander(text string, name string, data interface{}, timestamp c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Funcs adds the functions in fm to the templateExpander's function map.
|
||||||
|
// Existing functions will be overwritten in case of conflict.
|
||||||
|
func (te templateExpander) Funcs(fm text_template.FuncMap) {
|
||||||
|
for k, v := range fm {
|
||||||
|
te.funcMap[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Expand a template.
|
// Expand a template.
|
||||||
func (te templateExpander) Expand() (result string, resultErr error) {
|
func (te templateExpander) Expand() (result string, resultErr error) {
|
||||||
// It'd better to have no alert description than to kill the whole process
|
// It'd better to have no alert description than to kill the whole process
|
||||||
|
|
|
@ -135,9 +135,14 @@ func TestTemplateExpansion(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Humanize* Inf and NaN.
|
// Humanize* Inf and NaN.
|
||||||
text: "{{ range . }}{{ humanize . }}:{{ humanize1024 . }}:{{ humanizeDuration . }}:{{ end }}",
|
text: "{{ range . }}{{ humanize . }}:{{ humanize1024 . }}:{{ humanizeDuration . }}:{{humanizeTimestamp .}}:{{ end }}",
|
||||||
input: []float64{math.Inf(1), math.Inf(-1), math.NaN()},
|
input: []float64{math.Inf(1), math.Inf(-1), math.NaN()},
|
||||||
output: "+Inf:+Inf:+Inf:-Inf:-Inf:-Inf:NaN:NaN:NaN:",
|
output: "+Inf:+Inf:+Inf:+Inf:-Inf:-Inf:-Inf:-Inf:NaN:NaN:NaN:NaN:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// HumanizeTimestamp - clientmodel.SampleValue input.
|
||||||
|
text: "{{ 1435065584.128 | humanizeTimestamp }}",
|
||||||
|
output: "2015-06-23 15:19:44.128 +0200 CEST",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Title.
|
// Title.
|
||||||
|
|
|
@ -5,19 +5,3 @@
|
||||||
.alert_details {
|
.alert_details {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.silence_children_link {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert_rule {
|
|
||||||
padding: 5px;
|
|
||||||
color: #333;
|
|
||||||
background-color: #ddd;
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert_description {
|
|
||||||
padding: 8px 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,8 +10,9 @@ th.job_header {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.target_status_alert {
|
.state_indicator {
|
||||||
padding: 0 4px 0 4px;
|
padding: 0 4px 0 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.literal_output td {
|
.literal_output td {
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="alert_details">
|
<tr class="alert_details">
|
||||||
<td>
|
<td>
|
||||||
<div class="alert_description">
|
<div>
|
||||||
<span class="label alert_rule">{{.HTMLSnippet pathPrefix}}</span>
|
<pre><code>{{.HTMLSnippet pathPrefix}}</code></pre>
|
||||||
<a href="#" class="silence_children_link">Silence All Children…</a>
|
<a href="#" class="silence_children_link">Silence all instances of this alert…</a>
|
||||||
</div>
|
</div>
|
||||||
{{if $activeAlerts}}
|
{{if $activeAlerts}}
|
||||||
<table class="table table-bordered table-hover table-condensed alert_elements_table">
|
<table class="table table-bordered table-hover table-condensed alert_elements_table">
|
||||||
|
@ -30,12 +30,16 @@
|
||||||
<th>Silence</th>
|
<th>Silence</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{range $activeAlerts}}
|
{{range $activeAlerts}}
|
||||||
<tr class="{{index $alertStateToRowClass .State}}">
|
<tr>
|
||||||
<td>{{.Labels}}</td>
|
<td>
|
||||||
<td>{{.State}}</td>
|
{{range $label, $value := .Labels}}
|
||||||
<td>{{.ActiveSince}}</td>
|
<span class="label label-primary">{{$label}}="{{$value}}"</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td><span class="alert alert-{{ .State | alertStateToClass }} state_indicator">{{.State}}</span></td>
|
||||||
|
<td>{{.ActiveSince.Time}}</td>
|
||||||
<td>{{.Value}}</td>
|
<td>{{.Value}}</td>
|
||||||
<td><a href="#" class="silence_alert_link">Silence…</button><td>
|
<td><a href="#" class="silence_alert_link">Silence…</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="alert alert-{{ .Status.Health | healthToClass }} target_status_alert">
|
<span class="alert alert-{{ .Status.Health | healthToClass }} state_indicator">
|
||||||
{{.Status.Health}}
|
{{.Status.Health}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
69
web/web.go
69
web/web.go
|
@ -27,8 +27,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
template_std "html/template"
|
|
||||||
pprof_runtime "runtime/pprof"
|
pprof_runtime "runtime/pprof"
|
||||||
|
template_text "text/template"
|
||||||
|
|
||||||
clientmodel "github.com/prometheus/client_golang/model"
|
clientmodel "github.com/prometheus/client_golang/model"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
@ -320,11 +320,26 @@ func (h *Handler) getConsoles() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getTemplate(name string) (*template_std.Template, error) {
|
func (h *Handler) getTemplate(name string) (string, error) {
|
||||||
t := template_std.New("_base")
|
baseTmpl, err := h.getTemplateFile("_base")
|
||||||
var err error
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error reading base template: %s", err)
|
||||||
|
}
|
||||||
|
pageTmpl, err := h.getTemplateFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error reading page template %s: %s", name, err)
|
||||||
|
}
|
||||||
|
return baseTmpl + pageTmpl, nil
|
||||||
|
}
|
||||||
|
|
||||||
t.Funcs(template_std.FuncMap{
|
func (h *Handler) executeTemplate(w http.ResponseWriter, name string, data interface{}) {
|
||||||
|
text, err := h.getTemplate(name)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := template.NewTemplateExpander(text, name, data, clientmodel.Now(), h.queryEngine, h.options.PathPrefix)
|
||||||
|
tmpl.Funcs(template_text.FuncMap{
|
||||||
"since": time.Since,
|
"since": time.Since,
|
||||||
"getConsoles": h.getConsoles,
|
"getConsoles": h.getConsoles,
|
||||||
"pathPrefix": func() string { return h.options.PathPrefix },
|
"pathPrefix": func() string { return h.options.PathPrefix },
|
||||||
|
@ -352,40 +367,26 @@ func (h *Handler) getTemplate(name string) (*template_std.Template, error) {
|
||||||
return "danger"
|
return "danger"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"alertStateToClass": func(as rules.AlertState) string {
|
||||||
|
switch as {
|
||||||
|
case rules.StateInactive:
|
||||||
|
return "success"
|
||||||
|
case rules.StatePending:
|
||||||
|
return "warning"
|
||||||
|
case rules.StateFiring:
|
||||||
|
return "danger"
|
||||||
|
default:
|
||||||
|
panic("unknown alert state")
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
file, err := h.getTemplateFile("_base")
|
result, err := tmpl.ExpandHTML(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("Could not read base template:", err)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t, err = t.Parse(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("Could not parse base template:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err = h.getTemplateFile(name)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Could not read template %s: %s", name, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t, err = t.Parse(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Could not parse template %s: %s", name, err)
|
|
||||||
}
|
|
||||||
return t, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) executeTemplate(w http.ResponseWriter, name string, data interface{}) {
|
|
||||||
tpl, err := h.getTemplate(name)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error preparing layout template: ", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = tpl.Execute(w, data)
|
io.WriteString(w, result)
|
||||||
if err != nil {
|
|
||||||
log.Error("Error executing template: ", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func dumpHeap(w http.ResponseWriter, r *http.Request) {
|
func dumpHeap(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
Loading…
Reference in New Issue