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

@ -57,6 +57,8 @@ If functions are used in a pipeline, the pipeline value is passed as the last ar
| humanizePercentage | number or string | 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 or string | 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. |
| toTime | number or string | *time.Time | Converts a Unix timestamp in seconds to a time.Time. | | 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 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

@ -263,6 +263,17 @@ func NewTemplateExpander(
return floatToTime(v) 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 { "pathPrefix": func() string {
return externalURL.Path return externalURL.Path
}, },
@ -270,7 +281,7 @@ func NewTemplateExpander(
return externalURL.String() return externalURL.String()
}, },
"parseDuration": func(d string) (float64, error) { "parseDuration": func(d string) (float64, error) {
v, err := model.ParseDuration(d) v, err := model.ParseDurationAllowNegative(d)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -21,6 +21,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/histogram"
@ -467,6 +468,31 @@ func TestTemplateExpansion(t *testing.T) {
text: `{{ ("1435065584.128" | toTime).Format "2006" }}`, text: `{{ ("1435065584.128" | toTime).Format "2006" }}`,
output: "2015", 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. // Title.
text: "{{ \"aa bb CC\" | title }}", text: "{{ \"aa bb CC\" | title }}",
@ -514,10 +540,15 @@ func TestTemplateExpansion(t *testing.T) {
output: "http://testhost:9090/path/prefix", 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\") }}", text: "{{ printf \"%0.2f\" (parseDuration \"1h2m10ms\") }}",
output: "3720.01", 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. // Simple hostname.
text: "{{ \"foo.example.com\" | stripDomain }}", text: "{{ \"foo.example.com\" | stripDomain }}",
@ -579,7 +610,7 @@ func testTemplateExpansion(t *testing.T, scenarios []scenario) {
} }
var result string var result string
var err error 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 { if s.html {
result, err = expander.ExpandHTML(nil) result, err = expander.ExpandHTML(nil)
} else { } else {