mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-10 23:54:05 -08:00
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:
parent
6003cdbae0
commit
29cf4d6ad9
|
@ -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)
|
||||||
|
|
|
@ -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,22 +57,46 @@ 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 query(q string, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (queryResult, error) {
|
||||||
func Expand(text string, name string, data interface{}, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (result string, resultErr error) {
|
exprNode, err := rules.LoadExprFromString(q)
|
||||||
|
if err != nil {
|
||||||
// It'd better to have no alert description than to kill the whole process
|
return nil, err
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
queryStats := stats.NewTimerGroup()
|
||||||
|
vector, err := ast.EvalToVector(exprNode, timestamp, storage, queryStats)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
funcMap := template.FuncMap{
|
// ast.Vector is hard to work with in templates, so convert to
|
||||||
|
// base data types.
|
||||||
|
var result = make(queryResult, len(vector))
|
||||||
|
for n, v := range vector {
|
||||||
|
s := sample{
|
||||||
|
Value: float64(v.Value),
|
||||||
|
Labels: make(map[string]string),
|
||||||
|
}
|
||||||
|
for label, value := range v.Metric {
|
||||||
|
s.Labels[string(label)] = string(value)
|
||||||
|
}
|
||||||
|
result[n] = &s
|
||||||
|
}
|
||||||
|
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) {
|
"query": func(q string) (queryResult, error) {
|
||||||
return query(q, timestamp, storage)
|
return query(q, timestamp, storage)
|
||||||
},
|
},
|
||||||
|
@ -89,10 +115,20 @@ func Expand(text string, name string, data interface{}, timestamp clientmodel.Ti
|
||||||
"strvalue": func(s *sample) string {
|
"strvalue": func(s *sample) string {
|
||||||
return s.Labels["__value__"]
|
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 {
|
"reReplaceAll": func(pattern, repl, text string) string {
|
||||||
re := regexp.MustCompile(pattern)
|
re := regexp.MustCompile(pattern)
|
||||||
return re.ReplaceAllString(text, repl)
|
return re.ReplaceAllString(text, repl)
|
||||||
},
|
},
|
||||||
|
"safeHtml": func(text string) html_template.HTML {
|
||||||
|
return html_template.HTML(text)
|
||||||
|
},
|
||||||
"match": regexp.MatchString,
|
"match": regexp.MatchString,
|
||||||
"title": strings.Title,
|
"title": strings.Title,
|
||||||
"sortByLabel": func(label string, v queryResult) queryResult {
|
"sortByLabel": func(label string, v queryResult) queryResult {
|
||||||
|
@ -140,43 +176,62 @@ func Expand(text string, name string, data interface{}, timestamp clientmodel.Ti
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%.4g %s", v, prefix)
|
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
|
var buffer bytes.Buffer
|
||||||
tmpl, err := template.New(name).Funcs(funcMap).Parse(text)
|
tmpl, err := text_template.New(te.name).Funcs(te.funcMap).Parse(te.text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Error parsing template %v: %v", name, err)
|
return "", fmt.Errorf("Error parsing template %v: %v", te.name, err)
|
||||||
}
|
}
|
||||||
err = tmpl.Execute(&buffer, data)
|
err = tmpl.Execute(&buffer, te.data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Error executing template %v: %v", name, err)
|
return "", fmt.Errorf("Error executing template %v: %v", te.name, err)
|
||||||
}
|
}
|
||||||
return buffer.String(), nil
|
return buffer.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func query(q string, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (queryResult, error) {
|
// Expand a template with HTML escaping, with templates read from the given files.
|
||||||
exprNode, err := rules.LoadExprFromString(q)
|
func (te templateExpander) ExpandHTML(templateFiles []string) (result string, resultErr error) {
|
||||||
if err != nil {
|
defer func() {
|
||||||
return nil, err
|
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)
|
||||||
}
|
}
|
||||||
queryStats := stats.NewTimerGroup()
|
|
||||||
vector, err := ast.EvalToVector(exprNode, timestamp, storage, queryStats)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// ast.Vector is hard to work with in templates, so convert to
|
var buffer bytes.Buffer
|
||||||
// base data types.
|
tmpl, err := html_template.New(te.name).Funcs(html_template.FuncMap(te.funcMap)).Parse(te.text)
|
||||||
var result = make(queryResult, len(vector))
|
if err != nil {
|
||||||
for n, v := range vector {
|
return "", fmt.Errorf("Error parsing template %v: %v", te.name, err)
|
||||||
s := sample{
|
|
||||||
Value: float64(v.Value),
|
|
||||||
Labels: make(map[string]string),
|
|
||||||
}
|
}
|
||||||
for label, value := range v.Metric {
|
if len(templateFiles) > 0 {
|
||||||
s.Labels[string(label)] = string(value)
|
_, err = tmpl.ParseFiles(templateFiles...)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error parsing template files for %v: %v", te.name, err)
|
||||||
}
|
}
|
||||||
result[n] = &s
|
|
||||||
}
|
}
|
||||||
return result, nil
|
err = tmpl.Execute(&buffer, te.data)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error executing template %v: %v", te.name, err)
|
||||||
|
}
|
||||||
|
return buffer.String(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: "<b>",
|
||||||
|
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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue