diff --git a/rules/manager/manager.go b/rules/manager/manager.go index a980402e2..adda1c703 100644 --- a/rules/manager/manager.go +++ b/rules/manager/manager.go @@ -137,7 +137,8 @@ func (m *ruleManager) queueAlertNotifications(rule *rules.AlertingRule, timestam defs := "{{$labels := .Labels}}{{$value := .Value}}" expand := func(text string) string { - result, err := templates.Expand(defs+text, "__alert_"+rule.Name(), tmplData, timestamp, m.storage) + template := templates.NewTemplateExpander(defs+text, "__alert_"+rule.Name(), tmplData, timestamp, m.storage) + result, err := template.Expand() if err != nil { result = err.Error() glog.Warningf("Error expanding alert template %v with data '%v': %v", rule.Name(), tmplData, err) diff --git a/templates/templates.go b/templates/templates.go index 479427466..5a0183358 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -21,7 +21,9 @@ import ( "regexp" "sort" "strings" - "text/template" + + html_template "html/template" + text_template "text/template" clientmodel "github.com/prometheus/client_golang/model" @@ -55,105 +57,6 @@ func (q queryResultByLabelSorter) Swap(i, j int) { q.results[i], q.results[j] = q.results[j], q.results[i] } -// Expand a template, using the given data, time and storage. -func Expand(text string, name string, data interface{}, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (result string, resultErr error) { - - // It'd better to have no alert description than to kill the whole process - // if there's a bug in the template. Similarly with console templates. - defer func() { - if r := recover(); r != nil { - var ok bool - resultErr, ok = r.(error) - if !ok { - resultErr = fmt.Errorf("Panic expanding template: %v", r) - } - } - }() - - funcMap := template.FuncMap{ - "query": func(q string) (queryResult, error) { - return query(q, timestamp, storage) - }, - "first": func(v queryResult) (*sample, error) { - if len(v) > 0 { - return v[0], nil - } - return nil, errors.New("first() called on vector with no elements") - }, - "label": func(label string, s *sample) string { - return s.Labels[label] - }, - "value": func(s *sample) float64 { - return s.Value - }, - "strvalue": func(s *sample) string { - return s.Labels["__value__"] - }, - "reReplaceAll": func(pattern, repl, text string) string { - re := regexp.MustCompile(pattern) - return re.ReplaceAllString(text, repl) - }, - "match": regexp.MatchString, - "title": strings.Title, - "sortByLabel": func(label string, v queryResult) queryResult { - sorter := queryResultByLabelSorter{v[:], label} - sort.Stable(sorter) - return v - }, - "humanize": func(v float64) string { - if v == 0 { - return fmt.Sprintf("%.4g ", v) - } - if math.Abs(v) >= 1 { - prefix := "" - for _, p := range []string{"k", "M", "G", "T", "P", "E", "Z", "Y"} { - if math.Abs(v) < 1000 { - break - } - prefix = p - v /= 1000 - } - return fmt.Sprintf("%.4g %s", v, prefix) - } else { - prefix := "" - for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { - if math.Abs(v) >= 1 { - break - } - prefix = p - v *= 1000 - } - return fmt.Sprintf("%.4g %s", v, prefix) - } - }, - "humanize1024": func(v float64) string { - if math.Abs(v) <= 1 { - return fmt.Sprintf("%.4g ", v) - } - prefix := "" - for _, p := range []string{"ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"} { - if math.Abs(v) < 1024 { - break - } - prefix = p - v /= 1024 - } - return fmt.Sprintf("%.4g %s", v, prefix) - }, - } - - var buffer bytes.Buffer - tmpl, err := template.New(name).Funcs(funcMap).Parse(text) - if err != nil { - return "", fmt.Errorf("Error parsing template %v: %v", name, err) - } - err = tmpl.Execute(&buffer, data) - if err != nil { - return "", fmt.Errorf("Error executing template %v: %v", name, err) - } - return buffer.String(), nil -} - func query(q string, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (queryResult, error) { exprNode, err := rules.LoadExprFromString(q) if err != nil { @@ -180,3 +83,155 @@ func query(q string, timestamp clientmodel.Timestamp, storage metric.PreloadingP } return result, nil } + +type templateExpander struct { + text string + name string + data interface{} + funcMap text_template.FuncMap +} + +func NewTemplateExpander(text string, name string, data interface{}, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) *templateExpander { + return &templateExpander{ + text: text, + name: name, + data: data, + funcMap: text_template.FuncMap{ + "query": func(q string) (queryResult, error) { + return query(q, timestamp, storage) + }, + "first": func(v queryResult) (*sample, error) { + if len(v) > 0 { + return v[0], nil + } + return nil, errors.New("first() called on vector with no elements") + }, + "label": func(label string, s *sample) string { + return s.Labels[label] + }, + "value": func(s *sample) float64 { + return s.Value + }, + "strvalue": func(s *sample) string { + return s.Labels["__value__"] + }, + "args": func(args ...interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for i, a := range args { + result[fmt.Sprintf("arg%d", i)] = a + } + return result + }, + "reReplaceAll": func(pattern, repl, text string) string { + re := regexp.MustCompile(pattern) + return re.ReplaceAllString(text, repl) + }, + "safeHtml": func(text string) html_template.HTML { + return html_template.HTML(text) + }, + "match": regexp.MatchString, + "title": strings.Title, + "sortByLabel": func(label string, v queryResult) queryResult { + sorter := queryResultByLabelSorter{v[:], label} + sort.Stable(sorter) + return v + }, + "humanize": func(v float64) string { + if v == 0 { + return fmt.Sprintf("%.4g ", v) + } + if math.Abs(v) >= 1 { + prefix := "" + for _, p := range []string{"k", "M", "G", "T", "P", "E", "Z", "Y"} { + if math.Abs(v) < 1000 { + break + } + prefix = p + v /= 1000 + } + return fmt.Sprintf("%.4g %s", v, prefix) + } else { + prefix := "" + for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { + if math.Abs(v) >= 1 { + break + } + prefix = p + v *= 1000 + } + return fmt.Sprintf("%.4g %s", v, prefix) + } + }, + "humanize1024": func(v float64) string { + if math.Abs(v) <= 1 { + return fmt.Sprintf("%.4g ", v) + } + prefix := "" + for _, p := range []string{"ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"} { + if math.Abs(v) < 1024 { + break + } + prefix = p + v /= 1024 + } + return fmt.Sprintf("%.4g %s", v, prefix) + }, + }, + } +} + +// Expand a template. +func (te templateExpander) Expand() (result string, resultErr error) { + // It'd better to have no alert description than to kill the whole process + // if there's a bug in the template. + defer func() { + if r := recover(); r != nil { + var ok bool + resultErr, ok = r.(error) + if !ok { + resultErr = fmt.Errorf("Panic expanding template %v: %v", te.name, r) + } + } + }() + + var buffer bytes.Buffer + tmpl, err := text_template.New(te.name).Funcs(te.funcMap).Parse(te.text) + if err != nil { + return "", fmt.Errorf("Error parsing template %v: %v", te.name, err) + } + err = tmpl.Execute(&buffer, te.data) + if err != nil { + return "", fmt.Errorf("Error executing template %v: %v", te.name, err) + } + return buffer.String(), nil +} + +// Expand a template with HTML escaping, with templates read from the given files. +func (te templateExpander) ExpandHTML(templateFiles []string) (result string, resultErr error) { + defer func() { + if r := recover(); r != nil { + var ok bool + resultErr, ok = r.(error) + if !ok { + resultErr = fmt.Errorf("Panic expanding template %v: %v", te.name, r) + } + } + }() + + var buffer bytes.Buffer + tmpl, err := html_template.New(te.name).Funcs(html_template.FuncMap(te.funcMap)).Parse(te.text) + if err != nil { + return "", fmt.Errorf("Error parsing template %v: %v", te.name, err) + } + if len(templateFiles) > 0 { + _, err = tmpl.ParseFiles(templateFiles...) + if err != nil { + return "", fmt.Errorf("Error parsing template files for %v: %v", te.name, err) + } + } + err = tmpl.Execute(&buffer, te.data) + if err != nil { + return "", fmt.Errorf("Error executing template %v: %v", te.name, err) + } + return buffer.String(), nil +} diff --git a/templates/templates_test.go b/templates/templates_test.go index 1de164589..cef49d601 100644 --- a/templates/templates_test.go +++ b/templates/templates_test.go @@ -25,6 +25,7 @@ type testTemplatesScenario struct { text string output string shouldFail bool + html bool } func TestTemplateExpansion(t *testing.T) { @@ -39,6 +40,28 @@ func TestTemplateExpansion(t *testing.T) { text: "{{ 1 }}", output: "1", }, + { + // HTML escaping. + text: "{{ \"\" }}", + output: "<b>", + html: true, + }, + { + // Disabling HTML escaping. + text: "{{ \"\" | safeHtml }}", + output: "", + html: true, + }, + { + // HTML escaping doesn't apply to non-html. + text: "{{ \"\" }}", + output: "", + }, + { + // Pass multiple arguments to templates. + text: "{{define \"x\"}}{{.arg0}} {{.arg1}}{{end}}{{template \"x\" (args 1 \"2\")}}", + output: "1 2", + }, { // Get value from query. text: "{{ query \"metric{instance='a'}\" | first | value }}", @@ -120,7 +143,14 @@ func TestTemplateExpansion(t *testing.T) { }) for _, s := range scenarios { - result, err := Expand(s.text, "test", nil, time, ts) + var result string + var err error + expander := NewTemplateExpander(s.text, "test", nil, time, ts) + if s.html { + result, err = expander.ExpandHTML(nil) + } else { + result, err = expander.Expand() + } if s.shouldFail { if err == nil { t.Fatalf("Error not returned from %v", s.text) diff --git a/web/consoles.go b/web/consoles.go index f352da6f8..4e9c71a4b 100644 --- a/web/consoles.go +++ b/web/consoles.go @@ -19,6 +19,7 @@ import ( "io/ioutil" "net/http" "net/url" + "path/filepath" clientmodel "github.com/prometheus/client_golang/model" "github.com/prometheus/prometheus/storage/metric" @@ -27,6 +28,7 @@ import ( var ( consoleTemplatesPath = flag.String("consoleTemplates", "consoles", "Path to console template directory, available at /console") + consoleLibrariesPath = flag.String("consoleLibraries", "console_libraries", "Path to console library directory") ) type ConsolesHandler struct { @@ -64,8 +66,13 @@ func (h *ConsolesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { Params: params, } - now := clientmodel.Now() - result, err := templates.Expand(string(text), "__console_"+r.URL.Path, data, now, h.Storage) + template := templates.NewTemplateExpander(string(text), "__console_"+r.URL.Path, data, clientmodel.Now(), h.Storage) + filenames, err := filepath.Glob(*consoleLibrariesPath + "/*.lib") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + result, err := template.ExpandHTML(filenames) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return