Merge pull request #826 from prometheus/alerting-ui-improvements

Use TemplateExpander for main pages, improve alerting UI
pull/831/head
Julius Volz 2015-06-23 19:03:14 +02:00
commit cc18191b5e
9 changed files with 100 additions and 75 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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&hellip;</a> <a href="#" class="silence_children_link">Silence all instances of this alert&hellip;</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&hellip;</button><td> <td><a href="#" class="silence_alert_link">Silence&hellip;</a></td>
</tr> </tr>
{{end}} {{end}}
</table> </table>

View File

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

View File

@ -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) {