Repository: VictoriaMetrics/VictoriaMetrics
Pull request #396 (2020-03-31 21:27:26)

[vmalert] add prometheus template function

codecov[bot]: # [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396?src=pr&el=h1) Report
> Merging [#396](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396?src=pr&el=desc) into [master](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/b699c46046fc8b894cc774e71728b1639e5e2002&el=desc) will **decrease** coverage by `0.17%`.
> The diff coverage is `1.12%`.

[![Impacted file tree graph](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396/graphs/tree.svg?width=650&height=150&src=pr&token=YND8dYVcXn)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #396      +/-   ##
==========================================
- Coverage   71.63%   71.46%   -0.18%     
==========================================
  Files         128      129       +1     
  Lines       20143    20231      +88     
==========================================
+ Hits        14430    14458      +28     
- Misses       4603     4667      +64     
+ Partials     1110     1106       -4     
```


| [Impacted Files](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [app/vmalert/common/template\_func.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396/diff?src=pr&el=tree#diff-YXBwL3ZtYWxlcnQvY29tbW9uL3RlbXBsYXRlX2Z1bmMuZ28=) | `0.00% <0.00%> (ø)` | |
| [app/vmalert/common/alert.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396/diff?src=pr&el=tree#diff-YXBwL3ZtYWxlcnQvY29tbW9uL2FsZXJ0Lmdv) | `69.11% <100.00%> (ø)` | |
| [app/vmselect/promql/aggr\_incremental.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396/diff?src=pr&el=tree#diff-YXBwL3Ztc2VsZWN0L3Byb21xbC9hZ2dyX2luY3JlbWVudGFsLmdv) | `98.52% <0.00%> (-1.48%)` | :arrow_down: |
| [lib/storage/partition.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvcGFydGl0aW9uLmdv) | `71.59% <0.00%> (-0.24%)` | :arrow_down: |
| [lib/storage/index\_db.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvaW5kZXhfZGIuZ28=) | `76.57% <0.00%> (+0.77%)` | :arrow_up: |
| [lib/mergeset/table.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396/diff?src=pr&el=tree#diff-bGliL21lcmdlc2V0L3RhYmxlLmdv) | `69.71% <0.00%> (+1.00%)` | :arrow_up: |
| [lib/fs/dir\_remover.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396/diff?src=pr&el=tree#diff-bGliL2ZzL2Rpcl9yZW1vdmVyLmdv) | `26.66% <0.00%> (+23.33%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396?src=pr&el=footer). Last update [8661dc4...785a932](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/396?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/app/vmalert/common/alert.go b/app/vmalert/common/alert.go
index 02e79734f..6707a2396 100644
--- a/app/vmalert/common/alert.go
+++ b/app/vmalert/common/alert.go
@@ -106,8 +106,7 @@ func ValidateAnnotations(annotations map[string]string) error {
 }
 
 func templateAnnotation(dst io.Writer, text string, data alertTplData) error {
-	// todo add template helper func from Prometheus
-	tpl, err := template.New("").Option("missingkey=zero").Parse(text)
+	tpl, err := template.New("").Funcs(tmplFunc).Option("missingkey=zero").Parse(text)
 	if err != nil {
 		return fmt.Errorf("error parsing annotation:%w", err)
 	}
diff --git a/app/vmalert/common/template_func.go b/app/vmalert/common/template_func.go
new file mode 100644
index 000000000..86f1e3798
--- /dev/null
+++ b/app/vmalert/common/template_func.go
@@ -0,0 +1,171 @@
+// Copyright 2013 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package common
+
+import (
+	"fmt"
+	html_template "html/template"
+	"math"
+	"net/url"
+	"regexp"
+	"strings"
+	text_template "text/template"
+	"time"
+)
+
+var tmplFunc text_template.FuncMap
+
+// InitTemplateFunc returns template helper functions
+func InitTemplateFunc(externalURL *url.URL) {
+	tmplFunc = text_template.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
+		},
+		"reReplaceAll": func(pattern, repl, text string) string {
+			re := regexp.MustCompile(pattern)
+			return re.ReplaceAllString(text, repl)
+		},
+		"safeHtml": func(text string) html_template.HTML {
+			return html_template.HTML(text)
+		},
+		"match":   regexp.MatchString,
+		"title":   strings.Title,
+		"toUpper": strings.ToUpper,
+		"toLower": strings.ToLower,
+		"humanize": func(v float64) string {
+			if v == 0 || math.IsNaN(v) || math.IsInf(v, 0) {
+				return fmt.Sprintf("%.4g", v)
+			}
+			if math.Abs(v) >= 1 {
+				prefix := ""
+				for _, p := range []string{"k", "M", "G", "T", "P", "E", "Z", "Y"} {
+					if math.Abs(v) < 1000 {
+						break
+					}
+					prefix = p
+					v /= 1000
+				}
+				return fmt.Sprintf("%.4g%s", v, prefix)
+			}
+			prefix := ""
+			for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
+				if math.Abs(v) >= 1 {
+					break
+				}
+				prefix = p
+				v *= 1000
+			}
+			return fmt.Sprintf("%.4g%s", v, prefix)
+		},
+		"humanize1024": func(v float64) string {
+			if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) {
+				return fmt.Sprintf("%.4g", v)
+			}
+			prefix := ""
+			for _, p := range []string{"ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"} {
+				if math.Abs(v) < 1024 {
+					break
+				}
+				prefix = p
+				v /= 1024
+			}
+			return fmt.Sprintf("%.4g%s", v, prefix)
+		},
+		"humanizeDuration": func(v float64) string {
+			if math.IsNaN(v) || math.IsInf(v, 0) {
+				return fmt.Sprintf("%.4g", v)
+			}
+			if v == 0 {
+				return fmt.Sprintf("%.4gs", v)
+			}
+			if math.Abs(v) >= 1 {
+				sign := ""
+				if v < 0 {
+					sign = "-"
+					v = -v
+				}
+				seconds := int64(v) % 60
+				minutes := (int64(v) / 60) % 60
+				hours := (int64(v) / 60 / 60) % 24
+				days := int64(v) / 60 / 60 / 24
+				// For days to minutes, we display seconds as an integer.
+				if days != 0 {
+					return fmt.Sprintf("%s%dd %dh %dm %ds", sign, days, hours, minutes, seconds)
+				}
+				if hours != 0 {
+					return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds)
+				}
+				if minutes != 0 {
+					return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds)
+				}
+				// For seconds, we display 4 significant digits.
+				return fmt.Sprintf("%s%.4gs", sign, v)
+			}
+			prefix := ""
+			for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
+				if math.Abs(v) >= 1 {
+					break
+				}
+				prefix = p
+				v *= 1000
+			}
+			return fmt.Sprintf("%.4g%ss", v, prefix)
+		},
+		"humanizePercentage": func(v float64) string {
+			return fmt.Sprintf("%.4g%%", v*100)
+		},
+		"humanizeTimestamp": func(v float64) string {
+			if math.IsNaN(v) || math.IsInf(v, 0) {
+				return fmt.Sprintf("%.4g", v)
+			}
+			t := TimeFromUnixNano(int64(v * 1e9)).Time().UTC()
+			return fmt.Sprint(t)
+		},
+		"pathPrefix": func() string {
+			return externalURL.Path
+		},
+		"externalURL": func() string {
+			return externalURL.String()
+		},
+	}
+}
+
+// Time is the number of milliseconds since the epoch
+// (1970-01-01 00:00 UTC) excluding leap seconds.
+type Time int64
+
+// TimeFromUnixNano returns the Time equivalent to the Unix Time
+// t provided in nanoseconds.
+func TimeFromUnixNano(t int64) Time {
+	return Time(t / nanosPerTick)
+}
+
+// The number of nanoseconds per minimum tick.
+const nanosPerTick = int64(minimumTick / time.Nanosecond)
+
+// MinimumTick is the minimum supported time resolution. This has to be
+// at least time.Second in order for the code below to work.
+const minimumTick = time.Millisecond
+
+// second is the Time duration equivalent to one second.
+const second = int64(time.Second / minimumTick)
+
+// Time returns the time.Time representation of t.
+func (t Time) Time() time.Time {
+	return time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick)
+}
diff --git a/app/vmalert/main.go b/app/vmalert/main.go
index 39c77c649..c51ef5394 100644
--- a/app/vmalert/main.go
+++ b/app/vmalert/main.go
@@ -4,8 +4,9 @@ import (
 	"context"
 	"flag"
 	"fmt"
-	"net"
 	"net/http"
+	"net/url"
+	"os"
 	"strings"
 	"time"
 
@@ -34,6 +35,7 @@ Examples:
 	basicAuthPassword        = flag.String("datasource.basicAuth.password", "", "Optional basic auth password to use for -datasource.url")
 	evaluationInterval       = flag.Duration("evaluationInterval", 1*time.Minute, "How often to evaluate the rules. Default 1m")
 	providerURL              = flag.String("provider.url", "", "Prometheus alertmanager url. Required parameter. e.g. http://127.0.0.1:9093")
+	externalURL              = flag.String("external.url", "", "Reachable external url. URL is used to generate sharable alert url and in annotation templates")
 )
 
 func main() {
@@ -42,6 +44,12 @@ func main() {
 	logger.Init()
 	checkFlags()
 	ctx, cancel := context.WithCancel(context.Background())
+	// todo handle secure connection
+	eu, err := getExternalURL(*externalURL, *httpListenAddr, false)
+	if err != nil {
+		logger.Fatalf("can not get external url:%s ", err)
+	}
+	common.InitTemplateFunc(eu)
 
 	logger.Infof("reading alert rules configuration file from %s", strings.Join(*rulePath, ";"))
 	alertGroups, err := config.Parse(*rulePath, *validateAlertAnnotations)
@@ -49,11 +57,10 @@ func main() {
 		logger.Fatalf("Cannot parse configuration file: %s", err)
 	}
 
-	addr := getWebServerAddr(*httpListenAddr, false)
 	w := &watchdog{
 		storage: datasource.NewVMStorage(*datasourceURL, *basicAuthUsername, *basicAuthPassword, &http.Client{}),
 		alertProvider: provider.NewAlertManager(*providerURL, func(group, name string) string {
-			return addr + fmt.Sprintf("/%s/%s/status", group, name)
+			return fmt.Sprintf("%s://%s/%s/%s/status", eu.Scheme, eu.Host, group, name)
 		}, &http.Client{}),
 	}
 	for id := range alertGroups {
@@ -117,27 +124,23 @@ func (w *watchdog) run(ctx context.Context, a common.Group, evaluationInterval t
 	}
 }
 
-func getWebServerAddr(httpListenAddr string, isSecure bool) string {
-	if strings.Index(httpListenAddr, ":") != 0 {
-		if isSecure {
-			return "https://" + httpListenAddr
-		}
-		return "http://" + httpListenAddr
+func getExternalURL(externalURL, httpListenAddr string, isSecure bool) (*url.URL, error) {
+	if externalURL != "" {
+		return url.Parse(externalURL)
 	}
-
-	addrs, err := net.InterfaceAddrs()
+	hname, err := os.Hostname()
 	if err != nil {
-		panic("error getting the interface addresses ")
+		return nil, err
 	}
-	for _, a := range addrs {
-		if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
-			if ipnet.IP.To4() != nil {
-				return "http://" + ipnet.IP.String() + httpListenAddr
-			}
-		}
+	port := ""
+	if ipport := strings.Split(httpListenAddr, ":"); len(ipport) > 1 {
+		port = ":" + ipport[1]
+	}
+	schema := "http://"
+	if isSecure {
+		schema = "https://"
 	}
-	// no loopback ip return internal address
-	return "http://127.0.0.1" + httpListenAddr
+	return url.Parse(fmt.Sprintf("%s%s%s", schema, hname, port))
 }
 
 func (w *watchdog) stop() {


Test patch:
diff --git a/app/vmalert/config/parser_test.go b/app/vmalert/config/parser_test.go
index 55a7214c6..3fff26ee8 100644
--- a/app/vmalert/config/parser_test.go
+++ b/app/vmalert/config/parser_test.go
@@ -1,9 +1,19 @@
 package config
 
 import (
+	"net/url"
+	"os"
 	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/common"
 )
 
+func TestMain(m *testing.M) {
+	u, _ := url.Parse("https://victoriametrics.com/path")
+	common.InitTemplateFunc(u)
+	os.Exit(m.Run())
+}
+
 func TestParseGood(t *testing.T) {
 	if _, err := Parse([]string{"testdata/*good.rules", "testdata/dir/*good.*"}, true); err != nil {
 		t.Errorf("error parsing files %s", err)
diff --git a/app/vmalert/config/testdata/dir/rules0-bad.rules b/app/vmalert/config/testdata/dir/rules0-bad.rules
index a499fea21..c4a971a9b 100644
--- a/app/vmalert/config/testdata/dir/rules0-bad.rules
+++ b/app/vmalert/config/testdata/dir/rules0-bad.rules
@@ -15,5 +15,5 @@ groups:
         labels:
           label: bar
         annotations:
-          summary: "{{ value|humanize }}"
+          summary: "{{ value|query }}"
           description: "{{$labels}}"
diff --git a/app/vmalert/config/testdata/dir/rules0-good.rules b/app/vmalert/config/testdata/dir/rules0-good.rules
index 1e602e031..ec5b0bc9d 100644
--- a/app/vmalert/config/testdata/dir/rules0-good.rules
+++ b/app/vmalert/config/testdata/dir/rules0-good.rules
@@ -7,7 +7,7 @@ groups:
         labels:
           label: bar
         annotations:
-          summary: "{{ $value }}"
+          summary: "{{ $value|humanize }}"
           description: "{{$labels}}"
 
 
diff --git a/app/vmalert/config/testdata/rules0-good.rules b/app/vmalert/config/testdata/rules0-good.rules
index eb3cf8e1d..d8ae2f8f5 100644
--- a/app/vmalert/config/testdata/rules0-good.rules
+++ b/app/vmalert/config/testdata/rules0-good.rules
@@ -7,6 +7,6 @@ groups:
         labels:
           label: bar
         annotations:
-          summary: "{{ $value }}"
+          summary: "{{ $value|humanize }}"
           description: "{{$labels}}"