From 12fe915b483f805ef87bdcc73c4df7923d53bb95 Mon Sep 17 00:00:00 2001 From: kreedom <60944649+kreedom@users.noreply.github.com> Date: Wed, 1 Apr 2020 18:17:53 +0300 Subject: [PATCH] [vmalert] add prometheus template function (#396) * [vmalert] add prometheus template function * make linter be happy Co-authored-by: Aliaksandr Valialkin --- app/vmalert/common/alert.go | 3 +- app/vmalert/common/template_func.go | 171 ++++++++++++++++++ app/vmalert/config/parser_test.go | 10 + .../config/testdata/dir/rules0-bad.rules | 2 +- .../config/testdata/dir/rules0-good.rules | 2 +- app/vmalert/config/testdata/rules0-good.rules | 2 +- app/vmalert/main.go | 43 +++-- 7 files changed, 208 insertions(+), 25 deletions(-) create mode 100644 app/vmalert/common/template_func.go 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/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}}" 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] } - // no loopback ip return internal address - return "http://127.0.0.1" + httpListenAddr + schema := "http://" + if isSecure { + schema = "https://" + } + return url.Parse(fmt.Sprintf("%s%s%s", schema, hname, port)) } func (w *watchdog) stop() {