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
| 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. |
| ------------------ | -----------------| --------| --------- |
| 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

View file

@ -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

View file

@ -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 }}",