Use html/template for console templates and add template libary support.

Add a function to bypass the new auto-escaping.
Add a function to workaround go's templates only allowing passing in one argument.

Change-Id: Id7aa3f95e7c227692dc22108388b1d9b1e2eec99
This commit is contained in:
Brian Brazil 2014-06-10 15:30:06 +01:00
parent 6003cdbae0
commit 29cf4d6ad9
4 changed files with 197 additions and 104 deletions

View file

@ -137,7 +137,8 @@ func (m *ruleManager) queueAlertNotifications(rule *rules.AlertingRule, timestam
defs := "{{$labels := .Labels}}{{$value := .Value}}" defs := "{{$labels := .Labels}}{{$value := .Value}}"
expand := func(text string) string { 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 { if err != nil {
result = err.Error() result = err.Error()
glog.Warningf("Error expanding alert template %v with data '%v': %v", rule.Name(), tmplData, err) glog.Warningf("Error expanding alert template %v with data '%v': %v", rule.Name(), tmplData, err)

View file

@ -21,7 +21,9 @@ import (
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"text/template"
html_template "html/template"
text_template "text/template"
clientmodel "github.com/prometheus/client_golang/model" 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] 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) { func query(q string, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (queryResult, error) {
exprNode, err := rules.LoadExprFromString(q) exprNode, err := rules.LoadExprFromString(q)
if err != nil { if err != nil {
@ -180,3 +83,155 @@ func query(q string, timestamp clientmodel.Timestamp, storage metric.PreloadingP
} }
return result, nil 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
}

View file

@ -25,6 +25,7 @@ type testTemplatesScenario struct {
text string text string
output string output string
shouldFail bool shouldFail bool
html bool
} }
func TestTemplateExpansion(t *testing.T) { func TestTemplateExpansion(t *testing.T) {
@ -39,6 +40,28 @@ func TestTemplateExpansion(t *testing.T) {
text: "{{ 1 }}", text: "{{ 1 }}",
output: "1", output: "1",
}, },
{
// HTML escaping.
text: "{{ \"<b>\" }}",
output: "&lt;b&gt;",
html: true,
},
{
// Disabling HTML escaping.
text: "{{ \"<b>\" | safeHtml }}",
output: "<b>",
html: true,
},
{
// HTML escaping doesn't apply to non-html.
text: "{{ \"<b>\" }}",
output: "<b>",
},
{
// Pass multiple arguments to templates.
text: "{{define \"x\"}}{{.arg0}} {{.arg1}}{{end}}{{template \"x\" (args 1 \"2\")}}",
output: "1 2",
},
{ {
// Get value from query. // Get value from query.
text: "{{ query \"metric{instance='a'}\" | first | value }}", text: "{{ query \"metric{instance='a'}\" | first | value }}",
@ -120,7 +143,14 @@ func TestTemplateExpansion(t *testing.T) {
}) })
for _, s := range scenarios { 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 s.shouldFail {
if err == nil { if err == nil {
t.Fatalf("Error not returned from %v", s.text) t.Fatalf("Error not returned from %v", s.text)

View file

@ -19,6 +19,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath"
clientmodel "github.com/prometheus/client_golang/model" clientmodel "github.com/prometheus/client_golang/model"
"github.com/prometheus/prometheus/storage/metric" "github.com/prometheus/prometheus/storage/metric"
@ -27,6 +28,7 @@ import (
var ( var (
consoleTemplatesPath = flag.String("consoleTemplates", "consoles", "Path to console template directory, available at /console") 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 { type ConsolesHandler struct {
@ -64,8 +66,13 @@ func (h *ConsolesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Params: params, Params: params,
} }
now := clientmodel.Now() template := templates.NewTemplateExpander(string(text), "__console_"+r.URL.Path, data, clientmodel.Now(), h.Storage)
result, err := templates.Expand(string(text), "__console_"+r.URL.Path, data, 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 { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return