Add template functions to support various use cases. (#16619)

Presumably, this will help with Loki alerts, but the added functionality is also generally useful.

For one, this enables `parseDuration` to also accept negative duration (as that's something that is also used in PromQL by now).

This also adds a function `now` to return the evaluation time of the template (as seconds since epoch AKA Unix time) and a function `toDuration` (akin to `toTime`), which creates a Go `time.Duration` from a duration in seconds.

---------

Signed-off-by: Dmitry Ponomaryov <me@halje.ru>
Signed-off-by: Dmitry Ponomaryov <iamhalje@gmail.com>
This commit is contained in:
Dmitry Ponomaryov 2025-07-10 03:33:20 +05:00 committed by GitHub
parent 846acc10bb
commit b18272a572
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 49 additions and 5 deletions

View File

@ -55,8 +55,10 @@ If functions are used in a pipeline, the pipeline value is passed as the last ar
| 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. |
| toTime | number or string | *time.Time | Converts a Unix timestamp in seconds to a time.Time. |
| humanizeTimestamp | number or string | string | Converts a Unix timestamp in seconds to a more readable format. |
| toTime | number or string | *time.Time | Converts a Unix timestamp in seconds to a time.Time. |
| toDuration | number or string | *time.Duration | Converts a duration in seconds to a time.Duration. |
| now | none | float64 | Returns the Unix timestamp in seconds at the time of the template evaluation. |
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

@ -263,6 +263,17 @@ func NewTemplateExpander(
return floatToTime(v)
},
"toDuration": func(i interface{}) (*time.Duration, error) {
v, err := common_templates.ConvertToFloat(i)
if err != nil {
return nil, err
}
d := time.Duration(v * float64(time.Second))
return &d, nil
},
"now": func() float64 {
return float64(timestamp) / 1000.0
},
"pathPrefix": func() string {
return externalURL.Path
},
@ -270,7 +281,7 @@ func NewTemplateExpander(
return externalURL.String()
},
"parseDuration": func(d string) (float64, error) {
v, err := model.ParseDuration(d)
v, err := model.ParseDurationAllowNegative(d)
if err != nil {
return 0, err
}

View File

@ -21,6 +21,7 @@ import (
"testing"
"time"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/histogram"
@ -467,6 +468,31 @@ func TestTemplateExpansion(t *testing.T) {
text: `{{ ("1435065584.128" | toTime).Format "2006" }}`,
output: "2015",
},
{
// toDuration - input as float64 seconds, returns *time.Duration.
text: `{{ (1800 | toDuration).String }}`,
output: "30m0s",
},
{
// toDuration - input as string seconds, returns *time.Duration.
text: `{{ ("1800" | toDuration).String }}`,
output: "30m0s",
},
{
// now - returns fixed timestamp as float64 seconds.
text: `{{ now }}`,
output: "1.353755652e+09",
},
{
// now - returns fixed timestamp converted to formatted time string.
text: `{{ (now | toTime).Format "Mon Jan 2 15:04:05 2006" }}`,
output: "Sat Nov 24 11:14:12 2012",
},
{
// returns Unix milliseconds timestamp for 30 minutes ago.
text: `{{ ("-30m" | parseDuration | toDuration | (now | toTime).Add).UnixMilli }}`,
output: "1353753852000",
},
{
// Title.
text: "{{ \"aa bb CC\" | title }}",
@ -514,10 +540,15 @@ func TestTemplateExpansion(t *testing.T) {
output: "http://testhost:9090/path/prefix",
},
{
// parseDuration (using printf to ensure the return is a string).
// parseDuration with positive duration (using printf to ensure the return is a string).
text: "{{ printf \"%0.2f\" (parseDuration \"1h2m10ms\") }}",
output: "3720.01",
},
{
// parseDuration with negative duration (using printf to ensure the return is a string).
text: "{{ printf \"%0.2f\" (parseDuration \"-1h2m10ms\") }}",
output: "-3720.01",
},
{
// Simple hostname.
text: "{{ \"foo.example.com\" | stripDomain }}",
@ -579,7 +610,7 @@ func testTemplateExpansion(t *testing.T, scenarios []scenario) {
}
var result string
var err error
expander := NewTemplateExpander(context.Background(), s.text, "test", s.input, 0, queryFunc, extURL, s.options)
expander := NewTemplateExpander(context.Background(), s.text, "test", s.input, model.Time(1353755652000), queryFunc, extURL, s.options)
if s.html {
result, err = expander.ExpandHTML(nil)
} else {