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 <roidelapluie@inuits.eu>
This commit is contained in:
Julien Pivotto 2021-04-14 00:30:15 +02:00 committed by GitHub
parent 62afcabd01
commit ea6f6bba74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 130 additions and 34 deletions

View file

@ -52,12 +52,12 @@ If functions are used in a pipeline, the pipeline value is passed as the last ar
### Numbers ### Numbers
| Name | Arguments | Returns | Notes | | 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). | 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 | string | Like `humanize`, but uses 1024 as the base rather than 1000. | | humanize1024 | number or string | 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. | | humanizeDuration | number or string | string | Converts a duration in seconds to a more readable format. |
| humanizePercentage | number | string | Converts a ratio value to a fraction of 100. | | humanizePercentage | number or string | string | Converts a ratio value to a fraction of 100. |
| humanizeTimestamp | number | string | Converts a Unix timestamp in seconds to a more readable format. | | 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 Humanizing functions are intended to produce reasonable output for consumption
by humans, and are not guaranteed to return the same results between Prometheus by humans, and are not guaranteed to return the same results between Prometheus

View file

@ -22,6 +22,7 @@ import (
"net/url" "net/url"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
text_template "text/template" text_template "text/template"
"time" "time"
@ -97,6 +98,17 @@ func query(ctx context.Context, q string, ts time.Time, queryFn QueryFunc) (quer
return result, nil 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. // Expander executes templates in text or HTML mode with a common set of Prometheus template functions.
type Expander struct { type Expander struct {
text string text string
@ -163,9 +175,13 @@ func NewTemplateExpander(
sort.Stable(sorter) sort.Stable(sorter)
return v 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) { 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 { if math.Abs(v) >= 1 {
prefix := "" prefix := ""
@ -176,7 +192,7 @@ func NewTemplateExpander(
prefix = p prefix = p
v /= 1000 v /= 1000
} }
return fmt.Sprintf("%.4g%s", v, prefix) return fmt.Sprintf("%.4g%s", v, prefix), nil
} }
prefix := "" prefix := ""
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
@ -186,11 +202,15 @@ func NewTemplateExpander(
prefix = p prefix = p
v *= 1000 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) { if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v) return fmt.Sprintf("%.4g", v), nil
} }
prefix := "" prefix := ""
for _, p := range []string{"ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"} { for _, p := range []string{"ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"} {
@ -200,14 +220,18 @@ func NewTemplateExpander(
prefix = p prefix = p
v /= 1024 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) { if math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v) return fmt.Sprintf("%.4g", v), nil
} }
if v == 0 { if v == 0 {
return fmt.Sprintf("%.4gs", v) return fmt.Sprintf("%.4gs", v), nil
} }
if math.Abs(v) >= 1 { if math.Abs(v) >= 1 {
sign := "" sign := ""
@ -221,16 +245,16 @@ func NewTemplateExpander(
days := int64(v) / 60 / 60 / 24 days := int64(v) / 60 / 60 / 24
// For days to minutes, we display seconds as an integer. // For days to minutes, we display seconds as an integer.
if days != 0 { 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 { 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 { 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. // For seconds, we display 4 significant digits.
return fmt.Sprintf("%s%.4gs", sign, v) return fmt.Sprintf("%s%.4gs", sign, v), nil
} }
prefix := "" prefix := ""
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
@ -240,17 +264,25 @@ func NewTemplateExpander(
prefix = p prefix = p
v *= 1000 v *= 1000
} }
return fmt.Sprintf("%.4g%ss", v, prefix) return fmt.Sprintf("%.4g%ss", v, prefix), nil
}, },
"humanizePercentage": func(v float64) string { "humanizePercentage": func(i interface{}) (string, error) {
return fmt.Sprintf("%.4g%%", v*100) 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) { 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() t := model.TimeFromUnixNano(int64(v * 1e9)).Time().UTC()
return fmt.Sprint(t) return fmt.Sprint(t), nil
}, },
"pathPrefix": func() string { "pathPrefix": func() string {
return externalURL.Path return externalURL.Path

View file

@ -179,45 +179,109 @@ func TestTemplateExpansion(t *testing.T) {
output: "xa", output: "xa",
}, },
{ {
// Humanize. // Humanize - float64.
text: "{{ range . }}{{ humanize . }}:{{ end }}", text: "{{ range . }}{{ humanize . }}:{{ end }}",
input: []float64{0.0, 1.0, 1234567.0, .12}, input: []float64{0.0, 1.0, 1234567.0, .12},
output: "0:1:1.235M:120m:", 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 }}", text: "{{ range . }}{{ humanize1024 . }}:{{ end }}",
input: []float64{0.0, 1.0, 1048576.0, .12}, input: []float64{0.0, 1.0, 1048576.0, .12},
output: "0:1:1Mi: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 }}", text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}",
input: []float64{0, 1, 60, 3600, 86400, 86400 + 3600, -(86400*2 + 3600*3 + 60*4 + 5), 899.99}, 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:", 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 }}", text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}",
input: []float64{.1, .0001, .12345, 60.1, 60.5, 1.2345, 12.345}, 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:", 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 }}", text: "{{ range . }}{{ humanize . }}:{{ humanize1024 . }}:{{ humanizeDuration . }}:{{humanizeTimestamp .}}:{{ end }}",
input: []float64{math.Inf(1), math.Inf(-1), math.NaN()}, input: []float64{math.Inf(1), math.Inf(-1), math.NaN()},
output: "+Inf:+Inf:+Inf:+Inf:-Inf:-Inf:-Inf:-Inf:NaN:NaN:NaN: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 }}", text: "{{ -0.22222 | humanizePercentage }}:{{ 0.0 | humanizePercentage }}:{{ 0.1234567 | humanizePercentage }}:{{ 1.23456 | humanizePercentage }}",
output: "-22.22%:0%:12.35%:123.5%", 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 }}", text: "{{ 1435065584.128 | humanizeTimestamp }}",
output: "2015-06-23 13:19:44.128 +0000 UTC", 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. // Title.
text: "{{ \"aa bb CC\" | title }}", text: "{{ \"aa bb CC\" | title }}",