mirror of https://github.com/grafana/grafana
parent
0961de396e
commit
641e53753e
@ -0,0 +1,132 @@ |
|||||||
|
package prom |
||||||
|
|
||||||
|
import ( |
||||||
|
"reflect" |
||||||
|
"strings" |
||||||
|
"text/template/parse" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
PrometheusModeTemplateCall = "{{- _prometheusMode -}}" |
||||||
|
) |
||||||
|
|
||||||
|
func convertTemplates(m map[string]string) error { |
||||||
|
for k, v := range m { |
||||||
|
nv, err := convertTemplate(v) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
m[k] = nv |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// convertTemplate analyzes a template string to determine if it uses Prometheus-style
|
||||||
|
// value references ($value or .Value). If it does, it adds a _prometheusMode function call
|
||||||
|
// to the beginning of the template to switch the template rendering to Prometheus compatibility mode.
|
||||||
|
// This allows templates converted from Prometheus alerting rules to continue working without modification.
|
||||||
|
func convertTemplate(tmpl string) (string, error) { |
||||||
|
searchFor := map[string]struct{}{ |
||||||
|
"$value": {}, |
||||||
|
".Value": {}, |
||||||
|
"$.Value": {}, |
||||||
|
} |
||||||
|
|
||||||
|
// Create a parser with SkipFuncCheck to avoid errors with unknown functions
|
||||||
|
p := parse.New("tmpl") |
||||||
|
p.Mode = parse.ParseComments | parse.SkipFuncCheck |
||||||
|
|
||||||
|
// Add temporary prefix with the variables that are expected by the template,
|
||||||
|
// otherwise the parsing fails.
|
||||||
|
tmpPrefix := `{{$value := ""}}{{$labels := ""}}` |
||||||
|
|
||||||
|
tree, err := p.Parse(tmpPrefix+tmpl, "{{", "}}", map[string]*parse.Tree{}) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
// Search for the variables in the template
|
||||||
|
found := walkNodes(tree.Root, searchFor) |
||||||
|
if found { |
||||||
|
return PrometheusModeTemplateCall + tmpl, nil |
||||||
|
} |
||||||
|
|
||||||
|
return tmpl, nil |
||||||
|
} |
||||||
|
|
||||||
|
// walkNodes recursively traverses the AST to find all variable nodes with certain names
|
||||||
|
// and returns the boolean indicating whether they are present in the template.
|
||||||
|
func walkNodes(node parse.Node, searchFor map[string]struct{}) bool { |
||||||
|
var found bool |
||||||
|
|
||||||
|
var walk func(node parse.Node) |
||||||
|
walk = func(node parse.Node) { |
||||||
|
if node == nil || reflect.ValueOf(node).IsNil() { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
switch n := node.(type) { |
||||||
|
case *parse.VariableNode: |
||||||
|
if _, ok := searchFor[strings.Join(n.Ident, ".")]; ok { |
||||||
|
found = true |
||||||
|
} |
||||||
|
|
||||||
|
case *parse.FieldNode: |
||||||
|
if _, ok := searchFor["."+strings.Join(n.Ident, ".")]; ok { |
||||||
|
found = true |
||||||
|
} |
||||||
|
|
||||||
|
case *parse.ActionNode: |
||||||
|
walk(n.Pipe) |
||||||
|
|
||||||
|
case *parse.ChainNode: |
||||||
|
walk(n.Node) |
||||||
|
|
||||||
|
case *parse.CommandNode: |
||||||
|
for _, arg := range n.Args { |
||||||
|
walk(arg) |
||||||
|
} |
||||||
|
|
||||||
|
case *parse.ListNode: |
||||||
|
for _, child := range n.Nodes { |
||||||
|
walk(child) |
||||||
|
} |
||||||
|
|
||||||
|
case *parse.PipeNode: |
||||||
|
for _, cmd := range n.Cmds { |
||||||
|
walk(cmd) |
||||||
|
} |
||||||
|
|
||||||
|
case *parse.IfNode: |
||||||
|
walk(n.Pipe) |
||||||
|
walk(n.List) |
||||||
|
if n.ElseList != nil { |
||||||
|
walk(n.ElseList) |
||||||
|
} |
||||||
|
|
||||||
|
case *parse.RangeNode: |
||||||
|
walk(n.Pipe) |
||||||
|
walk(n.List) |
||||||
|
if n.ElseList != nil { |
||||||
|
walk(n.ElseList) |
||||||
|
} |
||||||
|
|
||||||
|
case *parse.WithNode: |
||||||
|
walk(n.Pipe) |
||||||
|
walk(n.List) |
||||||
|
if n.ElseList != nil { |
||||||
|
walk(n.ElseList) |
||||||
|
} |
||||||
|
|
||||||
|
case *parse.TemplateNode: |
||||||
|
if n.Pipe != nil { |
||||||
|
walk(n.Pipe) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
walk(node) |
||||||
|
|
||||||
|
return found |
||||||
|
} |
@ -0,0 +1,180 @@ |
|||||||
|
package prom |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
func TestConvertTemplates(t *testing.T) { |
||||||
|
testCases := []struct { |
||||||
|
name string |
||||||
|
templates map[string]string |
||||||
|
expected map[string]string |
||||||
|
shouldFail bool |
||||||
|
}{ |
||||||
|
{ |
||||||
|
name: "simple template with $value", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "Value is {{$value}}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "{{- _prometheusMode -}}Value is {{$value}}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "simple template with .Value", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "Value is {{.Value}}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "{{- _prometheusMode -}}Value is {{.Value}}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "template with $.Value", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": ` |
||||||
|
{{- range .Items}} |
||||||
|
Value: {{.Value}} |
||||||
|
Global Value: {{$.Value}} |
||||||
|
{{- end}} |
||||||
|
`, |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": `{{- _prometheusMode -}} |
||||||
|
{{- range .Items}} |
||||||
|
Value: {{.Value}} |
||||||
|
Global Value: {{$.Value}} |
||||||
|
{{- end}} |
||||||
|
`, |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "complex template with $value in if statement", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "{{if gt $value 100.0}}Too high{{else}}Good{{end}}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "{{- _prometheusMode -}}{{if gt $value 100.0}}Too high{{else}}Good{{end}}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "complex template with .Value in if statement", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "{{if gt .Value 100.0}}Too high{{else}}Good{{end}}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "{{- _prometheusMode -}}{{if gt .Value 100.0}}Too high{{else}}Good{{end}}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "template with $value in nested structures", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "{{range .Values}}{{if eq . $value}}Match{{end}}{{end}}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "{{- _prometheusMode -}}{{range .Values}}{{if eq . $value}}Match{{end}}{{end}}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "template without $value or .Value", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "This template does not use the values", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "This template does not use the values", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "template with $labels", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "Instance: {{ $labels.instance }}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "Instance: {{ $labels.instance }}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "template with both $value and $labels", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "{{ $labels.instance }} has value {{ $value }}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "{{- _prometheusMode -}}{{ $labels.instance }} has value {{ $value }}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "template with multiple $value occurrences", |
||||||
|
templates: map[string]string{ |
||||||
|
"template": "First: {{ $value }}, Second: {{ $value }}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"template": "{{- _prometheusMode -}}First: {{ $value }}, Second: {{ $value }}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "multiple templates with mixed patterns", |
||||||
|
templates: map[string]string{ |
||||||
|
"summary": "Instance {{ $labels.instance }} has high CPU usage", |
||||||
|
"description": "{{ $labels.instance }} has value {{ $value }}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"summary": "Instance {{ $labels.instance }} has high CPU usage", |
||||||
|
"description": "{{- _prometheusMode -}}{{ $labels.instance }} has value {{ $value }}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "all templates with values", |
||||||
|
templates: map[string]string{ |
||||||
|
"summary": "Value is {{ $value }}", |
||||||
|
"description": "Detailed value is {{ .Value }}", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"summary": "{{- _prometheusMode -}}Value is {{ $value }}", |
||||||
|
"description": "{{- _prometheusMode -}}Detailed value is {{ .Value }}", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "no templates with values", |
||||||
|
templates: map[string]string{ |
||||||
|
"summary": "Instance {{ $labels.instance }} is down", |
||||||
|
"description": "Instance {{ $labels.instance }} has been down for more than 5 minutes", |
||||||
|
}, |
||||||
|
expected: map[string]string{ |
||||||
|
"summary": "Instance {{ $labels.instance }} is down", |
||||||
|
"description": "Instance {{ $labels.instance }} has been down for more than 5 minutes", |
||||||
|
}, |
||||||
|
shouldFail: false, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, tc := range testCases { |
||||||
|
t.Run(tc.name, func(t *testing.T) { |
||||||
|
input := make(map[string]string) |
||||||
|
for k, v := range tc.templates { |
||||||
|
input[k] = v |
||||||
|
} |
||||||
|
|
||||||
|
err := convertTemplates(input) |
||||||
|
if tc.shouldFail { |
||||||
|
require.Error(t, err) |
||||||
|
} else { |
||||||
|
require.NoError(t, err) |
||||||
|
require.Equal(t, tc.expected, input) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue