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