From 44f385fd519197f8b93075fc5ec795818f5fdb74 Mon Sep 17 00:00:00 2001 From: suntala Date: Tue, 12 Mar 2024 20:14:31 +0100 Subject: [PATCH 1/2] Support expansion of native histogram values in alert templates Co-authored-by: Aleks Fazlieva Signed-off-by: suntala --- model/rulefmt/rulefmt.go | 3 +- rules/alerting.go | 2 +- rules/alerting_test.go | 62 ++++++++++++++++++++++++++++++++++++++++ template/template.go | 14 ++++++--- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/model/rulefmt/rulefmt.go b/model/rulefmt/rulefmt.go index 03cbd8849..4ed1619d6 100644 --- a/model/rulefmt/rulefmt.go +++ b/model/rulefmt/rulefmt.go @@ -27,6 +27,7 @@ import ( "gopkg.in/yaml.v3" "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/template" ) @@ -256,7 +257,7 @@ func testTemplateParsing(rl *RuleNode) (errs []error) { } // Trying to parse templates. - tmplData := template.AlertTemplateData(map[string]string{}, map[string]string{}, "", 0) + tmplData := template.AlertTemplateData(map[string]string{}, map[string]string{}, "", promql.Sample{}) defs := []string{ "{{$labels := .Labels}}", "{{$externalLabels := .ExternalLabels}}", diff --git a/rules/alerting.go b/rules/alerting.go index 7b0921a72..50c67fa2d 100644 --- a/rules/alerting.go +++ b/rules/alerting.go @@ -364,7 +364,7 @@ func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc, // Provide the alert information to the template. l := smpl.Metric.Map() - tmplData := template.AlertTemplateData(l, r.externalLabels, r.externalURL, smpl.F) + tmplData := template.AlertTemplateData(l, r.externalLabels, r.externalURL, smpl) // Inject some convenience variables that are easier to remember for users // who are not used to Go's templating system. defs := []string{ diff --git a/rules/alerting_test.go b/rules/alerting_test.go index a270731d9..ddfe345ef 100644 --- a/rules/alerting_test.go +++ b/rules/alerting_test.go @@ -23,6 +23,7 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/relabel" "github.com/prometheus/prometheus/model/timestamp" @@ -85,6 +86,67 @@ func TestAlertingRuleState(t *testing.T) { } } +func TestAlertingRuleTemplateWithHistogram(t *testing.T) { + h := histogram.FloatHistogram{ + Schema: 0, + Count: 30, + Sum: 1111.1, + ZeroThreshold: 0.001, + ZeroCount: 2, + PositiveSpans: []histogram.Span{ + {Offset: 0, Length: 1}, + {Offset: 1, Length: 5}, + }, + PositiveBuckets: []float64{1, 1, 2, 1, 1, 1}, + NegativeSpans: []histogram.Span{ + {Offset: 1, Length: 4}, + {Offset: 4, Length: 3}, + }, + NegativeBuckets: []float64{-2, 2, 2, 7, 5, 5, 2}, + } + + q := func(ctx context.Context, qs string, t time.Time) (promql.Vector, error) { + return []promql.Sample{{H: &h}}, nil + } + + expr, err := parser.ParseExpr("foo") + require.NoError(t, err) + + rule := NewAlertingRule( + "HistogramAsValue", + expr, + time.Minute, + 0, + labels.FromStrings("histogram", "{{ $value }}"), + labels.EmptyLabels(), labels.EmptyLabels(), "", true, nil, + ) + + evalTime := time.Now() + res, err := rule.Eval(context.TODO(), evalTime, q, nil, 0) + require.NoError(t, err) + + require.Len(t, res, 2) + for _, smpl := range res { + smplName := smpl.Metric.Get("__name__") + if smplName == "ALERTS" { + result := promql.Sample{ + Metric: labels.FromStrings( + "__name__", "ALERTS", + "alertname", "HistogramAsValue", + "alertstate", "pending", + "histogram", h.String(), + ), + T: timestamp.FromTime(evalTime), + F: 1, + } + testutil.RequireEqual(t, result, smpl) + } else { + // If not 'ALERTS', it has to be 'ALERTS_FOR_STATE'. + require.Equal(t, "ALERTS_FOR_STATE", smplName) + } + } +} + func TestAlertingRuleLabelsUpdate(t *testing.T) { storage := promql.LoadedStorage(t, ` load 1m diff --git a/template/template.go b/template/template.go index 5d72a7e83..f04f5ea53 100644 --- a/template/template.go +++ b/template/template.go @@ -355,18 +355,24 @@ func NewTemplateExpander( } // AlertTemplateData returns the interface to be used in expanding the template. -func AlertTemplateData(labels, externalLabels map[string]string, externalURL string, value float64) interface{} { - return struct { +func AlertTemplateData(labels, externalLabels map[string]string, externalURL string, smpl promql.Sample) interface{} { + res := struct { Labels map[string]string ExternalLabels map[string]string ExternalURL string - Value float64 + Value interface{} }{ Labels: labels, ExternalLabels: externalLabels, ExternalURL: externalURL, - Value: value, + Value: smpl.F, } + + if smpl.H != nil { + res.Value = smpl.H + } + + return res } // Funcs adds the functions in fm to the Expander's function map. From 9a7c6a5cc44541dbdd71e985f182be7079aa4ff6 Mon Sep 17 00:00:00 2001 From: suntala Date: Tue, 26 Mar 2024 22:17:19 +0100 Subject: [PATCH 2/2] Support native histogram values in template functions Co-authored-by: Aleks Fazlieva Signed-off-by: suntala --- docs/configuration/template_reference.md | 4 ++-- template/template.go | 7 +++++-- template/template_test.go | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/configuration/template_reference.md b/docs/configuration/template_reference.md index 06942891b..47df9d1e0 100644 --- a/docs/configuration/template_reference.md +++ b/docs/configuration/template_reference.md @@ -18,7 +18,7 @@ The primary data structure for dealing with time series data is the sample, defi ```go type sample struct { Labels map[string]string - Value float64 + Value interface{} } ``` @@ -44,7 +44,7 @@ If functions are used in a pipeline, the pipeline value is passed as the last ar | query | query string | []sample | Queries the database, does not support returning range vectors. | | first | []sample | sample | Equivalent to `index a 0` | | label | label, sample | string | Equivalent to `index sample.Labels label` | -| value | sample | float64 | Equivalent to `sample.Value` | +| value | sample | interface{} | Equivalent to `sample.Value` | | sortByLabel | label, []samples | []sample | Sorts the samples by the given label. Is stable. | `first`, `label` and `value` are intended to make query results easily usable in pipelines. diff --git a/template/template.go b/template/template.go index f04f5ea53..43772805c 100644 --- a/template/template.go +++ b/template/template.go @@ -57,7 +57,7 @@ func init() { // A version of vector that's easier to use from templates. type sample struct { Labels map[string]string - Value float64 + Value interface{} } type queryResult []*sample @@ -96,6 +96,9 @@ func query(ctx context.Context, q string, ts time.Time, queryFn QueryFunc) (quer Value: v.F, Labels: v.Metric.Map(), } + if v.H != nil { + s.Value = v.H + } result[n] = &s } return result, nil @@ -160,7 +163,7 @@ func NewTemplateExpander( "label": func(label string, s *sample) string { return s.Labels[label] }, - "value": func(s *sample) float64 { + "value": func(s *sample) interface{} { return s.Value }, "strvalue": func(s *sample) string { diff --git a/template/template_test.go b/template/template_test.go index e7bdcc3b8..57de1d0f5 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql" ) @@ -39,6 +40,12 @@ func TestTemplateExpansion(t *testing.T) { text: "{{ 1 }}", output: "1", }, + { + // Native histogram value. + text: "{{ . | value }}", + input: &sample{Value: &histogram.FloatHistogram{Count: 3, Sum: 10}}, + output: (&histogram.FloatHistogram{Count: 3, Sum: 10}).String(), + }, { // Non-ASCII space (not allowed in text/template, see https://github.com/golang/go/blob/master/src/text/template/parse/lex.go#L98) text: "{{ }}", @@ -84,6 +91,18 @@ func TestTemplateExpansion(t *testing.T) { }, output: "11", }, + { + // Get value of a native histogram from query. + text: "{{ query \"metric{instance='a'}\" | first | value }}", + queryResult: promql.Vector{ + { + Metric: labels.FromStrings(labels.MetricName, "metric", "instance", "a"), + T: 0, + H: &histogram.FloatHistogram{Count: 3, Sum: 10}, + }, + }, + output: (&histogram.FloatHistogram{Count: 3, Sum: 10}).String(), + }, { // Get label from query. text: "{{ query \"metric{instance='a'}\" | first | label \"instance\" }}",