From ea6f6bba74dec06dd7a439304d6187f3b193e112 Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Wed, 14 Apr 2021 00:30:15 +0200 Subject: [PATCH] Enable parsing strings in humanize functions (#8682) * Enable parsing strings in humanize functions This is useful to humanize count_values or buckets labels. Signed-off-by: Julien Pivotto --- docs/configuration/template_reference.md | 14 ++--- template/template.go | 72 ++++++++++++++++------ template/template_test.go | 78 +++++++++++++++++++++--- 3 files changed, 130 insertions(+), 34 deletions(-) diff --git a/docs/configuration/template_reference.md b/docs/configuration/template_reference.md index 49a1e412f..92161f4c6 100644 --- a/docs/configuration/template_reference.md +++ b/docs/configuration/template_reference.md @@ -51,13 +51,13 @@ If functions are used in a pipeline, the pipeline value is passed as the last ar ### Numbers -| Name | Arguments | Returns | Notes | -| ------------- | --------------| --------| --------- | -| humanize | number | string | Converts a number to a more readable format, using [metric prefixes](https://en.wikipedia.org/wiki/Metric_prefix). -| humanize1024 | number | string | Like `humanize`, but uses 1024 as the base rather than 1000. | -| humanizeDuration | number | string | Converts a duration in seconds to a more readable format. | -| humanizePercentage | number | string | Converts a ratio value to a fraction of 100. | -| humanizeTimestamp | number | string | Converts a Unix timestamp in seconds to a more readable format. | +| Name | Arguments | Returns | Notes | +| ------------------ | -----------------| --------| --------- | +| humanize | number or string | string | Converts a number to a more readable format, using [metric prefixes](https://en.wikipedia.org/wiki/Metric_prefix). +| humanize1024 | number or string | string | Like `humanize`, but uses 1024 as the base rather than 1000. | +| humanizeDuration | number or string | string | Converts a duration in seconds to a more readable format. | +| humanizePercentage | number or string | string | Converts a ratio value to a fraction of 100. | +| humanizeTimestamp | number or string | string | Converts a Unix timestamp in seconds to a more readable format. | Humanizing functions are intended to produce reasonable output for consumption by humans, and are not guaranteed to return the same results between Prometheus diff --git a/template/template.go b/template/template.go index 10a9241c7..ae427738e 100644 --- a/template/template.go +++ b/template/template.go @@ -22,6 +22,7 @@ import ( "net/url" "regexp" "sort" + "strconv" "strings" text_template "text/template" "time" @@ -97,6 +98,17 @@ func query(ctx context.Context, q string, ts time.Time, queryFn QueryFunc) (quer return result, nil } +func convertToFloat(i interface{}) (float64, error) { + switch v := i.(type) { + case float64: + return v, nil + case string: + return strconv.ParseFloat(v, 64) + default: + return 0, fmt.Errorf("can't convert %T to float", v) + } +} + // Expander executes templates in text or HTML mode with a common set of Prometheus template functions. type Expander struct { text string @@ -163,9 +175,13 @@ func NewTemplateExpander( sort.Stable(sorter) return v }, - "humanize": func(v float64) string { + "humanize": func(i interface{}) (string, error) { + v, err := convertToFloat(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 := "" @@ -176,7 +192,7 @@ func NewTemplateExpander( 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"} { @@ -186,11 +202,15 @@ func NewTemplateExpander( prefix = p v *= 1000 } - return fmt.Sprintf("%.4g%s", v, prefix) + return fmt.Sprintf("%.4g%s", v, prefix), nil }, - "humanize1024": func(v float64) string { + "humanize1024": func(i interface{}) (string, error) { + v, err := convertToFloat(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"} { @@ -200,14 +220,18 @@ func NewTemplateExpander( prefix = p v /= 1024 } - return fmt.Sprintf("%.4g%s", v, prefix) + return fmt.Sprintf("%.4g%s", v, prefix), nil }, - "humanizeDuration": func(v float64) string { + "humanizeDuration": func(i interface{}) (string, error) { + v, err := convertToFloat(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 := "" @@ -221,16 +245,16 @@ func NewTemplateExpander( 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"} { @@ -240,17 +264,25 @@ func NewTemplateExpander( prefix = p v *= 1000 } - return fmt.Sprintf("%.4g%ss", v, prefix) + return fmt.Sprintf("%.4g%ss", v, prefix), nil }, - "humanizePercentage": func(v float64) string { - return fmt.Sprintf("%.4g%%", v*100) + "humanizePercentage": func(i interface{}) (string, error) { + v, err := convertToFloat(i) + if err != nil { + return "", err + } + return fmt.Sprintf("%.4g%%", v*100), nil }, - "humanizeTimestamp": func(v float64) string { + "humanizeTimestamp": func(i interface{}) (string, error) { + v, err := convertToFloat(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 := model.TimeFromUnixNano(int64(v * 1e9)).Time().UTC() - return fmt.Sprint(t) + return fmt.Sprint(t), nil }, "pathPrefix": func() string { return externalURL.Path diff --git a/template/template_test.go b/template/template_test.go index a47af9d6b..21583d911 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -179,45 +179,109 @@ func TestTemplateExpansion(t *testing.T) { output: "xa", }, { - // Humanize. + // Humanize - float64. text: "{{ range . }}{{ humanize . }}:{{ end }}", input: []float64{0.0, 1.0, 1234567.0, .12}, output: "0:1:1.235M:120m:", }, { - // Humanize1024. + // Humanize - string. + text: "{{ range . }}{{ humanize . }}:{{ end }}", + input: []string{"0.0", "1.0", "1234567.0", ".12"}, + output: "0:1:1.235M:120m:", + }, + { + // Humanize - string with error. + text: `{{ humanize "one" }}`, + shouldFail: true, + errorMsg: `strconv.ParseFloat: parsing "one": invalid syntax`, + }, + { + // Humanize1024 - float64. text: "{{ range . }}{{ humanize1024 . }}:{{ end }}", input: []float64{0.0, 1.0, 1048576.0, .12}, output: "0:1:1Mi:0.12:", }, { - // HumanizeDuration - seconds. + // Humanize1024 - string. + text: "{{ range . }}{{ humanize1024 . }}:{{ end }}", + input: []string{"0.0", "1.0", "1048576.0", ".12"}, + output: "0:1:1Mi:0.12:", + }, + { + // Humanize1024 - string with error. + text: `{{ humanize1024 "one" }}`, + shouldFail: true, + errorMsg: `strconv.ParseFloat: parsing "one": invalid syntax`, + }, + { + // HumanizeDuration - seconds - float64. text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}", input: []float64{0, 1, 60, 3600, 86400, 86400 + 3600, -(86400*2 + 3600*3 + 60*4 + 5), 899.99}, output: "0s:1s:1m 0s:1h 0m 0s:1d 0h 0m 0s:1d 1h 0m 0s:-2d 3h 4m 5s:14m 59s:", }, { - // HumanizeDuration - subsecond and fractional seconds. + // HumanizeDuration - seconds - string. + text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}", + input: []string{"0", "1", "60", "3600", "86400"}, + output: "0s:1s:1m 0s:1h 0m 0s:1d 0h 0m 0s:", + }, + { + // HumanizeDuration - subsecond and fractional seconds - float64. text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}", input: []float64{.1, .0001, .12345, 60.1, 60.5, 1.2345, 12.345}, output: "100ms:100us:123.5ms:1m 0s:1m 0s:1.234s:12.35s:", }, { - // Humanize* Inf and NaN. + // HumanizeDuration - subsecond and fractional seconds - string. + text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}", + input: []string{".1", ".0001", ".12345", "60.1", "60.5", "1.2345", "12.345"}, + output: "100ms:100us:123.5ms:1m 0s:1m 0s:1.234s:12.35s:", + }, + { + // HumanizeDuration - string with error. + text: `{{ humanizeDuration "one" }}`, + shouldFail: true, + errorMsg: `strconv.ParseFloat: parsing "one": invalid syntax`, + }, + { + // Humanize* Inf and NaN - float64. text: "{{ range . }}{{ humanize . }}:{{ humanize1024 . }}:{{ humanizeDuration . }}:{{humanizeTimestamp .}}:{{ end }}", input: []float64{math.Inf(1), math.Inf(-1), math.NaN()}, output: "+Inf:+Inf:+Inf:+Inf:-Inf:-Inf:-Inf:-Inf:NaN:NaN:NaN:NaN:", }, { - // HumanizePercentage - model.SampleValue input. + // Humanize* Inf and NaN - string. + text: "{{ range . }}{{ humanize . }}:{{ humanize1024 . }}:{{ humanizeDuration . }}:{{humanizeTimestamp .}}:{{ end }}", + input: []string{"+Inf", "-Inf", "NaN"}, + output: "+Inf:+Inf:+Inf:+Inf:-Inf:-Inf:-Inf:-Inf:NaN:NaN:NaN:NaN:", + }, + { + // HumanizePercentage - model.SampleValue input - float64. text: "{{ -0.22222 | humanizePercentage }}:{{ 0.0 | humanizePercentage }}:{{ 0.1234567 | humanizePercentage }}:{{ 1.23456 | humanizePercentage }}", output: "-22.22%:0%:12.35%:123.5%", }, { - // HumanizeTimestamp - model.SampleValue input. + // HumanizePercentage - model.SampleValue input - string. + text: `{{ "-0.22222" | humanizePercentage }}:{{ "0.0" | humanizePercentage }}:{{ "0.1234567" | humanizePercentage }}:{{ "1.23456" | humanizePercentage }}`, + output: "-22.22%:0%:12.35%:123.5%", + }, + { + // HumanizePercentage - model.SampleValue input - string with error. + text: `{{ "one" | humanizePercentage }}`, + shouldFail: true, + errorMsg: `strconv.ParseFloat: parsing "one": invalid syntax`, + }, + { + // HumanizeTimestamp - model.SampleValue input - float64. text: "{{ 1435065584.128 | humanizeTimestamp }}", output: "2015-06-23 13:19:44.128 +0000 UTC", }, + { + // HumanizeTimestamp - model.SampleValue input - string. + text: `{{ "1435065584.128" | humanizeTimestamp }}`, + output: "2015-06-23 13:19:44.128 +0000 UTC", + }, { // Title. text: "{{ \"aa bb CC\" | title }}",