diff --git a/app/vmalert/templates/template.go b/app/vmalert/templates/template.go index 12de9b3c17..3907ba97e2 100644 --- a/app/vmalert/templates/template.go +++ b/app/vmalert/templates/template.go @@ -24,6 +24,7 @@ import ( "path/filepath" "regexp" "sort" + "strconv" "strings" "sync" "time" @@ -258,9 +259,13 @@ func templateFuncs() textTpl.FuncMap { // 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 { + "humanize": func(i interface{}) (string, error) { + v, err := toFloat64(i) + if err != nil { + return "", err + } if v == 0 || math.IsNaN(v) || math.IsInf(v, 0) { - return fmt.Sprintf("%.4g", v) + return fmt.Sprintf("%.4g", v), nil } if math.Abs(v) >= 1 { prefix := "" @@ -271,7 +276,7 @@ func templateFuncs() textTpl.FuncMap { prefix = p v /= 1000 } - return fmt.Sprintf("%.4g%s", v, prefix) + return fmt.Sprintf("%.4g%s", v, prefix), nil } prefix := "" for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { @@ -281,13 +286,17 @@ func templateFuncs() textTpl.FuncMap { prefix = p v *= 1000 } - return fmt.Sprintf("%.4g%s", v, prefix) + return fmt.Sprintf("%.4g%s", v, prefix), nil }, // humanize1024 converts given number to a human readable format with 1024 as base - "humanize1024": func(v float64) string { + "humanize1024": func(i interface{}) (string, error) { + v, err := toFloat64(i) + if err != nil { + return "", err + } if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) { - return fmt.Sprintf("%.4g", v) + return fmt.Sprintf("%.4g", v), nil } prefix := "" for _, p := range []string{"ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"} { @@ -297,16 +306,20 @@ func templateFuncs() textTpl.FuncMap { prefix = p v /= 1024 } - return fmt.Sprintf("%.4g%s", v, prefix) + return fmt.Sprintf("%.4g%s", v, prefix), nil }, // humanizeDuration converts given seconds to a human readable duration - "humanizeDuration": func(v float64) string { + "humanizeDuration": func(i interface{}) (string, error) { + v, err := toFloat64(i) + if err != nil { + return "", err + } if math.IsNaN(v) || math.IsInf(v, 0) { - return fmt.Sprintf("%.4g", v) + return fmt.Sprintf("%.4g", v), nil } if v == 0 { - return fmt.Sprintf("%.4gs", v) + return fmt.Sprintf("%.4gs", v), nil } if math.Abs(v) >= 1 { sign := "" @@ -320,16 +333,16 @@ func templateFuncs() textTpl.FuncMap { 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) + return fmt.Sprintf("%s%dd %dh %dm %ds", sign, days, hours, minutes, seconds), nil } if hours != 0 { - return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds) + return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds), nil } if minutes != 0 { - return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds) + return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds), nil } // For seconds, we display 4 significant digits. - return fmt.Sprintf("%s%.4gs", sign, v) + return fmt.Sprintf("%s%.4gs", sign, v), nil } prefix := "" for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { @@ -339,21 +352,29 @@ func templateFuncs() textTpl.FuncMap { prefix = p v *= 1000 } - return fmt.Sprintf("%.4g%ss", v, prefix) + return fmt.Sprintf("%.4g%ss", v, prefix), nil }, // humanizePercentage converts given ratio value to a fraction of 100 - "humanizePercentage": func(v float64) string { - return fmt.Sprintf("%.4g%%", v*100) + "humanizePercentage": func(i interface{}) (string, error) { + v, err := toFloat64(i) + if err != nil { + return "", err + } + return fmt.Sprintf("%.4g%%", v*100), nil }, // humanizeTimestamp converts given timestamp to a human readable time equivalent - "humanizeTimestamp": func(v float64) string { + "humanizeTimestamp": func(i interface{}) (string, error) { + v, err := toFloat64(i) + if err != nil { + return "", err + } if math.IsNaN(v) || math.IsInf(v, 0) { - return fmt.Sprintf("%.4g", v) + return fmt.Sprintf("%.4g", v), nil } t := TimeFromUnixNano(int64(v * 1e9)).Time().UTC() - return fmt.Sprint(t) + return fmt.Sprint(t), nil }, /* URLs */ @@ -491,3 +512,28 @@ const second = int64(time.Second / minimumTick) func (t Time) Time() time.Time { return time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick) } + +func toFloat64(v interface{}) (float64, error) { + switch i := v.(type) { + case float64: + return i, nil + case float32: + return float64(i), nil + case int64: + return float64(i), nil + case int32: + return float64(i), nil + case int: + return float64(i), nil + case uint64: + return float64(i), nil + case uint32: + return float64(i), nil + case uint: + return float64(i), nil + case string: + return strconv.ParseFloat(i, 64) + default: + return 0, fmt.Errorf("unexpected value type %v", i) + } +} diff --git a/app/vmalert/templates/template_test.go b/app/vmalert/templates/template_test.go index dc2cb09ce3..16e4b5c252 100644 --- a/app/vmalert/templates/template_test.go +++ b/app/vmalert/templates/template_test.go @@ -23,7 +23,7 @@ func mkTemplate(current, replacement interface{}) textTemplate { return tmpl } -func equalTemplates(t *testing.T, tmpls ...*textTpl.Template) bool { +func equalTemplates(tmpls ...*textTpl.Template) bool { var cmp *textTpl.Template for i, tmpl := range tmpls { if i == 0 { @@ -191,10 +191,10 @@ func TestTemplates_Load(t *testing.T) { t.Error("%+w", err) t.Error("expected string doesn't exist in error message") } - if !equalTemplates(t, masterTmpl.replacement, tc.expectedTemplate.replacement) { + if !equalTemplates(masterTmpl.replacement, tc.expectedTemplate.replacement) { t.Fatalf("replacement template is not as expected") } - if !equalTemplates(t, masterTmpl.current, tc.expectedTemplate.current) { + if !equalTemplates(masterTmpl.current, tc.expectedTemplate.current) { t.Fatalf("current template is not as expected") } }) @@ -264,10 +264,10 @@ func TestTemplates_Reload(t *testing.T) { t.Run(tc.name, func(t *testing.T) { masterTmpl = tc.initialTemplate Reload() - if !equalTemplates(t, masterTmpl.replacement, tc.expectedTemplate.replacement) { + if !equalTemplates(masterTmpl.replacement, tc.expectedTemplate.replacement) { t.Fatalf("replacement template is not as expected") } - if !equalTemplates(t, masterTmpl.current, tc.expectedTemplate.current) { + if !equalTemplates(masterTmpl.current, tc.expectedTemplate.current) { t.Fatalf("current template is not as expected") } }) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 341370a65f..7040871123 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,6 +18,7 @@ The following tip changes can be tested by building VictoriaMetrics components f * FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): support [reusable templates](https://prometheus.io/docs/prometheus/latest/configuration/template_examples/#defining-reusable-templates) for rules annotations. The path to the template files can be specified via `-rule.templates` flag. See more about this feature [here](https://docs.victoriametrics.com/vmalert.html#reusable-templates). Thanks to @AndrewChubatiuk for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2532). * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add `influx-prometheus-mode` command-line flag, which allows to restore the original time series written from Prometheus into InfluxDB during data migration from InfluxDB to VictoriaMetrics. See [this feature request](https://github.com/VictoriaMetrics/vmctl/issues/8). Thanks to @mback2k for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2545). +* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): support strings in `humanize.*` template function. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2569. * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): proxy `/rules` requests to vmalert from Grafana's alerting UI. This removes errors in Grafana's UI for Grafana versions older than 8.5.*. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2583. * BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): do not return values from [label_value()](https://docs.victoriametrics.com/MetricsQL.html#label_value) function if the original time series has no values at the selected timestamps. * BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): limit the number of concurrently established connections from vmselect to vmstorage. This should prevent from potentially high spikes in the number of established connections after temporary slowdown in connection handshake procedure between vmselect and vmstorage because of spikes in workload. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2552).