From 4ed8de62ac34fa01b34257a483159d69194c3e72 Mon Sep 17 00:00:00 2001
From: Roman Khavronenko <hagen1778@gmail.com>
Date: Thu, 8 Apr 2021 16:19:08 +0100
Subject: [PATCH] vmalert: document template functions and mention them in
 README (#1197)

---
 app/vmalert/README.md                 |   5 +
 app/vmalert/notifier/template_func.go | 133 ++++++++++++++++++++------
 2 files changed, 109 insertions(+), 29 deletions(-)

diff --git a/app/vmalert/README.md b/app/vmalert/README.md
index 2f3daa0468..fc2366caa8 100644
--- a/app/vmalert/README.md
+++ b/app/vmalert/README.md
@@ -131,6 +131,11 @@ annotations:
   [ <labelname>: <tmpl_string> ]
 ```
 
+It is allowed to use [Go templating](https://golang.org/pkg/text/template/) in annotations
+to format data, iterate over it or execute expressions.
+Additionally, `vmalert` provides some extra templating functions
+listed [here](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/notifier/template_func.go).
+
 #### Recording rules
 
 The syntax for recording rules is following:
diff --git a/app/vmalert/notifier/template_func.go b/app/vmalert/notifier/template_func.go
index 8751abcc72..f21c41a5c7 100644
--- a/app/vmalert/notifier/template_func.go
+++ b/app/vmalert/notifier/template_func.go
@@ -32,40 +32,44 @@ import (
 // for templating functions.
 type QueryFn func(query string) ([]datasource.Metric, error)
 
-func funcsWithQuery(query QueryFn) textTpl.FuncMap {
-	fm := make(textTpl.FuncMap)
-	for k, fn := range tmplFunc {
-		fm[k] = fn
-	}
-	fm["query"] = func(q string) ([]datasource.Metric, error) {
-		return query(q)
-	}
-	return fm
-}
-
 var tmplFunc textTpl.FuncMap
 
 // InitTemplateFunc initiates template helper functions
 func InitTemplateFunc(externalURL *url.URL) {
 	tmplFunc = textTpl.FuncMap{
-		"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
-		},
+		/* Strings */
+
+		// reReplaceAll ReplaceAllString returns a copy of src, replacing matches of the Regexp with
+		// the replacement string repl. Inside repl, $ signs are interpreted as in Expand,
+		// so for instance $1 represents the text of the first submatch.
+		// alias for https://golang.org/pkg/regexp/#Regexp.ReplaceAllString
 		"reReplaceAll": func(pattern, repl, text string) string {
 			re := regexp.MustCompile(pattern)
 			return re.ReplaceAllString(text, repl)
 		},
-		"safeHtml": func(text string) htmlTpl.HTML {
-			return htmlTpl.HTML(text)
-		},
-		"match":   regexp.MatchString,
-		"title":   strings.Title,
+
+		// match reports whether the string s
+		// contains any match of the regular expression pattern.
+		// alias for https://golang.org/pkg/regexp/#MatchString
+		"match": regexp.MatchString,
+
+		// title returns a copy of the string s with all Unicode letters
+		// that begin words mapped to their Unicode title case.
+		// alias for https://golang.org/pkg/strings/#Title
+		"title": strings.Title,
+
+		// toUpper returns s with all Unicode letters mapped to their upper case.
+		// alias for https://golang.org/pkg/strings/#ToUpper
 		"toUpper": strings.ToUpper,
+
+		// toLower returns s with all Unicode letters mapped to their lower case.
+		// alias for https://golang.org/pkg/strings/#ToLower
 		"toLower": strings.ToLower,
+
+		/* Numbers */
+
+		// humanize converts given number to a human readable format
+		// by adding metric prefixes https://en.wikipedia.org/wiki/Metric_prefix
 		"humanize": func(v float64) string {
 			if v == 0 || math.IsNaN(v) || math.IsInf(v, 0) {
 				return fmt.Sprintf("%.4g", v)
@@ -91,6 +95,8 @@ func InitTemplateFunc(externalURL *url.URL) {
 			}
 			return fmt.Sprintf("%.4g%s", v, prefix)
 		},
+
+		// humanize1024 converts given number to a human readable format with 1024 as base
 		"humanize1024": func(v float64) string {
 			if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) {
 				return fmt.Sprintf("%.4g", v)
@@ -105,6 +111,8 @@ func InitTemplateFunc(externalURL *url.URL) {
 			}
 			return fmt.Sprintf("%.4g%s", v, prefix)
 		},
+
+		// humanizeDuration converts given seconds to a human readable duration
 		"humanizeDuration": func(v float64) string {
 			if math.IsNaN(v) || math.IsInf(v, 0) {
 				return fmt.Sprintf("%.4g", v)
@@ -145,9 +153,13 @@ func InitTemplateFunc(externalURL *url.URL) {
 			}
 			return fmt.Sprintf("%.4g%ss", v, prefix)
 		},
+
+		// humanizePercentage converts given ratio value to a fraction of 100
 		"humanizePercentage": func(v float64) string {
 			return fmt.Sprintf("%.4g%%", v*100)
 		},
+
+		// humanizeTimestamp converts given timestamp to a human readable time equivalent
 		"humanizeTimestamp": func(v float64) string {
 			if math.IsNaN(v) || math.IsInf(v, 0) {
 				return fmt.Sprintf("%.4g", v)
@@ -155,48 +167,111 @@ func InitTemplateFunc(externalURL *url.URL) {
 			t := TimeFromUnixNano(int64(v * 1e9)).Time().UTC()
 			return fmt.Sprint(t)
 		},
-		"pathPrefix": func() string {
-			return externalURL.Path
-		},
+
+		/* URLs */
+
+		// externalURL returns value of `external.url` flag
 		"externalURL": func() string {
 			return externalURL.String()
 		},
+
+		// pathPrefix returns a Path segment from the URL value in `external.url` flag
+		"pathPrefix": func() string {
+			return externalURL.Path
+		},
+
+		// pathEscape escapes the string so it can be safely placed inside a URL path segment,
+		// replacing special characters (including /) with %XX sequences as needed.
+		// alias for https://golang.org/pkg/net/url/#PathEscape
 		"pathEscape": func(u string) string {
 			return url.PathEscape(u)
 		},
+
+		// queryEscape escapes the string so it can be safely placed
+		// inside a URL query.
+		// alias for https://golang.org/pkg/net/url/#QueryEscape
 		"queryEscape": func(q string) string {
 			return url.QueryEscape(q)
 		},
+
+		// crlfEscape replaces new line chars to skip URL encoding.
+		// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/890
 		"crlfEscape": func(q string) string {
 			q = strings.Replace(q, "\n", `\n`, -1)
 			return strings.Replace(q, "\r", `\r`, -1)
 		},
+
+		// quotesEscape escapes quote char
 		"quotesEscape": func(q string) string {
 			return strings.Replace(q, `"`, `\"`, -1)
 		},
-		// query function supposed to be substituted at funcsWithQuery().
-		// it is present here only for validation purposes, when there is no
-		// provided datasource.
+
+		// query executes the MetricsQL/PromQL query against
+		// configured `datasource.url` address.
+		// For example, {{ query "foo" | first | value }} will
+		// execute "/api/v1/query?query=foo" request and will return
+		// the first value in response.
 		"query": func(q string) ([]datasource.Metric, error) {
+			// query function supposed to be substituted at funcsWithQuery().
+			// it is present here only for validation purposes, when there is no
+			// provided datasource.
+			//
 			// return non-empty slice to pass validation with chained functions in template
 			// see issue #989 for details
 			return []datasource.Metric{{}}, nil
 		},
+
+		// first returns the first by order element from the given metrics list.
+		// usually used alongside with `query` template function.
 		"first": func(metrics []datasource.Metric) (datasource.Metric, error) {
 			if len(metrics) > 0 {
 				return metrics[0], nil
 			}
 			return datasource.Metric{}, errors.New("first() called on vector with no elements")
 		},
+
+		// label returns the value of the given label name for the given metric.
+		// usually used alongside with `query` template function.
 		"label": func(label string, m datasource.Metric) string {
 			return m.Label(label)
 		},
+
+		// value returns the value of the given metric.
+		// usually used alongside with `query` template function.
 		"value": func(m datasource.Metric) float64 {
 			return m.Value
 		},
+
+		/* Helpers */
+
+		// Converts a list of objects to a map with keys arg0, arg1 etc.
+		// This is intended to allow multiple arguments to be passed to templates.
+		"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
+		},
+
+		// safeHtml marks string as HTML not requiring auto-escaping.
+		"safeHtml": func(text string) htmlTpl.HTML {
+			return htmlTpl.HTML(text)
+		},
 	}
 }
 
+func funcsWithQuery(query QueryFn) textTpl.FuncMap {
+	fm := make(textTpl.FuncMap)
+	for k, fn := range tmplFunc {
+		fm[k] = fn
+	}
+	fm["query"] = func(q string) ([]datasource.Metric, error) {
+		return query(q)
+	}
+	return fm
+}
+
 // Time is the number of milliseconds since the epoch
 // (1970-01-01 00:00 UTC) excluding leap seconds.
 type Time int64