From b18272a5727be12a21784f9221070965e8d1c71a Mon Sep 17 00:00:00 2001 From: Dmitry Ponomaryov Date: Thu, 10 Jul 2025 03:33:20 +0500 Subject: [PATCH] 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 Signed-off-by: Dmitry Ponomaryov --- docs/configuration/template_reference.md | 6 ++-- template/template.go | 13 ++++++++- template/template_test.go | 35 ++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/docs/configuration/template_reference.md b/docs/configuration/template_reference.md index 57f2606b13..300b8666a4 100644 --- a/docs/configuration/template_reference.md +++ b/docs/configuration/template_reference.md @@ -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 diff --git a/template/template.go b/template/template.go index 75a9f33bd2..87ca32b346 100644 --- a/template/template.go +++ b/template/template.go @@ -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 } diff --git a/template/template_test.go b/template/template_test.go index 57de1d0f55..4e108da571 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -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 {