The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/migration/template.go

213 lines
4.7 KiB

// This file contains code that parses templates from old alerting into a sequence
// of tokens. Each token can be either a string literal or a variable.
package migration
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"unicode"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/state/template"
)
// Token contains either a string literal or a variable.
type Token struct {
Literal string
Variable string
}
func (t Token) IsLiteral() bool {
return t.Literal != ""
}
func (t Token) IsVariable() bool {
return t.Variable != ""
}
func (t Token) String() string {
if t.IsLiteral() {
return t.Literal
} else if t.IsVariable() {
return t.Variable
} else {
return ""
}
}
func MigrateTmpl(l log.Logger, oldTmpl string) string {
var newTmpl string
tokens := tokenizeTmpl(l, oldTmpl)
tokens = escapeLiterals(tokens)
if anyVariableToken(tokens) {
tokens = variablesToMapLookups(tokens, "mergedLabels")
newTmpl += fmt.Sprintf("{{- $mergedLabels := %s $values -}}\n", template.MergeLabelValuesFuncName)
}
newTmpl += tokensToTmpl(tokens)
return newTmpl
}
func tokenizeTmpl(logger log.Logger, tmpl string) []Token {
var (
tokens []Token
l int
r int
err error
)
in := []rune(tmpl)
for r < len(in) {
if !startVariable(in[r:]) {
r++
continue
}
token, offset, tokenErr := tokenizeVariable(in[r:])
if tokenErr != nil {
err = errors.Join(err, tokenErr)
r += offset
continue
}
// we've found a variable, so everything from l -> r is the literal before the variable
// ex: "foo ${bar}" -> Literal: "foo ", Variable: "bar"
if r > l {
tokens = append(tokens, Token{Literal: string(in[l:r])})
}
tokens = append(tokens, token)
// seek l and r past the variable
r += offset
l = r
}
// any remaining runes will be a final literal
if r > l {
tokens = append(tokens, Token{Literal: string(in[l:r])})
}
if err != nil {
logger.Warn("Encountered malformed template", "template", tmpl, "err", err)
}
return tokens
}
func tokenizeVariable(in []rune) (Token, int, error) {
var (
pos int
r rune
runes []rune
)
if !startVariable(in) {
return Token{}, pos, fmt.Errorf("expected '${', got '%s'", string(in[:2]))
}
pos += 2 // seek past opening delimiter
// consume valid runes until we hit a closing brace
// non-space whitespace and the opening delimiter are invalid
for pos < len(in) {
r = in[pos]
if unicode.IsSpace(r) && r != ' ' {
return Token{}, pos, errors.New("unexpected whitespace")
}
if startVariable(in[pos:]) {
return Token{}, pos, errors.New("ambiguous delimiter")
}
if r == '}' {
pos++
break
}
runes = append(runes, r)
pos++
}
// variable must end with '}' delimiter
if r != '}' {
return Token{}, pos, fmt.Errorf("expected '}', got '%c'", r)
}
token := Token{Variable: string(runes)}
if !token.IsVariable() {
return Token{}, pos, errors.New("empty variable")
}
return token, pos, nil
}
func startVariable(in []rune) bool {
return len(in) >= 2 && in[0] == '$' && in[1] == '{'
}
func anyVariableToken(tokens []Token) bool {
for _, token := range tokens {
if token.IsVariable() {
return true
}
}
return false
}
// tokensToTmpl returns the tokens as a Go template
func tokensToTmpl(tokens []Token) string {
buf := bytes.Buffer{}
for _, token := range tokens {
if token.IsVariable() {
buf.WriteString("{{")
buf.WriteString(token.String())
buf.WriteString("}}")
} else {
buf.WriteString(token.String())
}
}
return buf.String()
}
// escapeLiterals escapes any token literals with substrings that would be interpreted as Go template syntax
func escapeLiterals(tokens []Token) []Token {
result := make([]Token, 0, len(tokens))
for _, token := range tokens {
if token.IsLiteral() && shouldEscape(token.Literal) {
token.Literal = fmt.Sprintf("{{`%s`}}", token.Literal)
}
result = append(result, token)
}
return result
}
func shouldEscape(literal string) bool {
return strings.Contains(literal, "{{") || literal[len(literal)-1] == '{'
}
// variablesToMapLookups converts any variables in a slice of tokens to Go template map lookups
func variablesToMapLookups(tokens []Token, mapName string) []Token {
result := make([]Token, 0, len(tokens))
for _, token := range tokens {
if token.IsVariable() {
token.Variable = mapLookupString(token.Variable, mapName)
}
result = append(result, token)
}
return result
}
func mapLookupString(v string, mapName string) string {
for _, r := range v {
if !(unicode.IsDigit(r) || unicode.IsLetter(r) || r == '_') {
return fmt.Sprintf(`index $%s %s`, mapName, strconv.Quote(v)) // quote v to escape any special characters
}
}
return fmt.Sprintf(`$%s.%s`, mapName, v)
}