From eabf3b9f731e03912428cc9f1a5b2d5290a1f63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 12 Sep 2024 15:33:00 +0200 Subject: [PATCH] feat(alerting): add support for query service instant vectors (#92091) --- pkg/services/ngalert/eval/eval.go | 62 ++++++++++++++++++-------- pkg/services/ngalert/eval/eval_test.go | 46 +++++++++++++++++++ 2 files changed, 90 insertions(+), 18 deletions(-) diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 548724ab0c8..0f0bf3074c1 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/expr/classic" @@ -648,6 +647,9 @@ func datasourceUIDsToRefIDs(refIDsToDatasourceUIDs map[string]string) map[string // Each non-empty Frame must be a single Field of type []*float64 and of length 1. // Also, each Frame must be uniquely identified by its Field.Labels or a single Error result will be returned. // +// An exception to this is data that is returned by the query service, which might have a timestamp and single value. +// Those are handled with the appropriated logic. +// // Per Frame, data becomes a State based on the following rules: // // If no value is set: @@ -702,6 +704,13 @@ func evaluateExecutionResult(execResults ExecutionResults, ts time.Time) Results continue } + // The query service returns instant vectors from prometheus as scalars. We need to handle them accordingly. + if val, ok := scalarInstantVector(f); ok { + r := buildResult(f, val, ts) + evalResults = append(evalResults, r) + continue + } + if len(f.TypeIndices(data.FieldTypeTime, data.FieldTypeNullableTime)) > 0 { appendErrRes(&invalidEvalResultFormatError{refID: f.RefID, reason: "looks like time series data, only reduced data can be alerted on."}) continue @@ -734,23 +743,7 @@ func evaluateExecutionResult(execResults ExecutionResults, ts time.Time) Results } val := f.Fields[0].At(0).(*float64) // type checked by data.FieldTypeNullableFloat64 above - - r := Result{ - Instance: f.Fields[0].Labels, - EvaluatedAt: ts, - EvaluationDuration: time.Since(ts), - EvaluationString: extractEvalString(f), - Values: extractValues(f), - } - - switch { - case val == nil: - r.State = NoData - case *val == 0: - r.State = Normal - default: - r.State = Alerting - } + r := buildResult(f, val, ts) evalResults = append(evalResults, r) } @@ -776,6 +769,39 @@ func evaluateExecutionResult(execResults ExecutionResults, ts time.Time) Results return evalResults } +func buildResult(f *data.Frame, val *float64, ts time.Time) Result { + r := Result{ + Instance: f.Fields[0].Labels, + EvaluatedAt: ts, + EvaluationDuration: time.Since(ts), + EvaluationString: extractEvalString(f), + Values: extractValues(f), + } + switch { + case val == nil: + r.State = NoData + case *val == 0: + r.State = Normal + default: + r.State = Alerting + } + return r +} + +func scalarInstantVector(f *data.Frame) (*float64, bool) { + defaultReturnValue := 0.0 + if len(f.Fields) != 2 { + return &defaultReturnValue, false + } + if f.Fields[0].Len() > 1 || (f.Fields[0].Type() != data.FieldTypeNullableTime && f.Fields[0].Type() != data.FieldTypeTime) { + return &defaultReturnValue, false + } + if f.Fields[1].Len() > 1 || f.Fields[1].Type() != data.FieldTypeNullableFloat64 { + return &defaultReturnValue, false + } + return f.Fields[1].At(0).(*float64), true +} + // AsDataFrame forms the EvalResults in Frame suitable for displaying in the table panel of the front end. // It displays one row per alert instance, with a column for each label and one for the alerting state. func (evalResults Results) AsDataFrame() data.Frame { diff --git a/pkg/services/ngalert/eval/eval_test.go b/pkg/services/ngalert/eval/eval_test.go index 6710737891b..f2cc7273ab9 100644 --- a/pkg/services/ngalert/eval/eval_test.go +++ b/pkg/services/ngalert/eval/eval_test.go @@ -62,6 +62,52 @@ func TestEvaluateExecutionResult(t *testing.T) { }, }, }, + { + desc: "query service dataframe works as instant vector", + execResults: ExecutionResults{ + Condition: func() []*data.Frame { + f := data.NewFrame("", + data.NewField("", nil, []*time.Time{util.Pointer(time.Now())}), + data.NewField("", nil, []*float64{util.Pointer(0.0)}), + ) + f.Meta = &data.FrameMeta{ + Custom: map[string]any{ + "resultType": "scalar", + }, + } + return []*data.Frame{f} + }(), + }, + expectResultLength: 1, + expectResults: Results{ + { + State: Normal, + }, + }, + }, + { + desc: "query service dataframe works as instant vector when alerting", + execResults: ExecutionResults{ + Condition: func() []*data.Frame { + f := data.NewFrame("", + data.NewField("", nil, []*time.Time{util.Pointer(time.Now())}), + data.NewField("", nil, []*float64{util.Pointer(1.0)}), + ) + f.Meta = &data.FrameMeta{ + Custom: map[string]any{ + "resultType": "scalar", + }, + } + return []*data.Frame{f} + }(), + }, + expectResultLength: 1, + expectResults: Results{ + { + State: Alerting, + }, + }, + }, { desc: "nil value single instance is single a NoData state result", execResults: ExecutionResults{