From d363e195171936e7f41b4f8446d5b65590562fb8 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 16 Nov 2021 10:03:18 +0000 Subject: [PATCH] Alerting: Add datasource_uid label to DatasourceNoData alerts (#41621) --- pkg/services/ngalert/eval/eval.go | 44 ++++++++++++++++++++++---- pkg/services/ngalert/eval/eval_test.go | 34 ++++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index c1f56eec961..9a38d8e8466 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -7,6 +7,7 @@ import ( "fmt" "runtime/debug" "sort" + "strconv" "time" "github.com/grafana/grafana/pkg/expr/classic" @@ -49,6 +50,9 @@ func (e *invalidEvalResultFormatError) Unwrap() error { type ExecutionResults struct { Error error + // NoData contains the DatasourceUID for RefIDs that returned no data. + NoData map[string]string + Results data.Frames } @@ -165,17 +169,13 @@ type NumberValueCapture struct { } func executeCondition(ctx AlertExecCtx, c *models.Condition, now time.Time, exprService *expr.Service) ExecutionResults { - result := ExecutionResults{} - execResp, err := executeQueriesAndExpressions(ctx, c.Data, now, exprService) - if err != nil { return ExecutionResults{Error: err} } // eval captures for the '__value_string__' annotation and the Value property of the API response. captures := make([]NumberValueCapture, 0, len(execResp.Responses)) - captureVal := func(refID string, labels data.Labels, value *float64) { captures = append(captures, NumberValueCapture{ Var: refID, @@ -184,7 +184,28 @@ func executeCondition(ctx AlertExecCtx, c *models.Condition, now time.Time, expr }) } + // datasourceUIDsForRefIDs is a short-lived lookup table of RefID to DatasourceUID + // for efficient lookups of the DatasourceUID when a RefID returns no data + datasourceUIDsForRefIDs := make(map[string]string) + for _, next := range c.Data { + datasourceUIDsForRefIDs[next.RefID] = next.DatasourceUID + } + // datasourceExprUID is a special DatasourceUID for expressions + datasourceExprUID := strconv.FormatInt(expr.DatasourceID, 10) + + var result ExecutionResults for refID, res := range execResp.Responses { + if len(res.Frames) == 0 { + // to ensure that NoData is consistent with Results we do not initialize NoData + // unless there is at least one RefID that returned no data + if result.NoData == nil { + result.NoData = make(map[string]string) + } + if s, ok := datasourceUIDsForRefIDs[refID]; ok && s != datasourceExprUID { + result.NoData[refID] = s + } + } + // for each frame within each response, the response can contain several data types including time-series data. // For now, we favour simplicity and only care about single scalar values. for _, frame := range res.Frames { @@ -281,10 +302,10 @@ func evaluateExecutionResult(execResults ExecutionResults, ts time.Time) Results }) } - appendNoData := func(l data.Labels) { + appendNoData := func(labels data.Labels) { evalResults = append(evalResults, Result{ State: NoData, - Instance: l, + Instance: labels, EvaluatedAt: ts, EvaluationDuration: time.Since(ts), }) @@ -295,6 +316,17 @@ func evaluateExecutionResult(execResults ExecutionResults, ts time.Time) Results return evalResults } + if len(execResults.NoData) > 0 { + m := make(map[string]struct{}) + for _, datasourceUID := range execResults.NoData { + if _, ok := m[datasourceUID]; !ok { + appendNoData(data.Labels{"datasource_uid": datasourceUID}) + m[datasourceUID] = struct{}{} + } + } + return evalResults + } + if len(execResults.Results) == 0 { appendNoData(nil) return evalResults diff --git a/pkg/services/ngalert/eval/eval_test.go b/pkg/services/ngalert/eval/eval_test.go index 46b8de08853..da9132919cd 100644 --- a/pkg/services/ngalert/eval/eval_test.go +++ b/pkg/services/ngalert/eval/eval_test.go @@ -276,6 +276,40 @@ func TestEvaluateExecutionResult(t *testing.T) { }, }, }, + { + desc: "no data for Ref ID will produce NoData result", + execResults: ExecutionResults{ + NoData: map[string]string{"A": "1"}, + }, + expectResultLength: 1, + expectResults: Results{ + { + Instance: data.Labels{"datasource_uid": "1"}, + State: NoData, + }, + }, + }, + { + desc: "no data for Ref IDs will produce NoData result for each data source", + execResults: ExecutionResults{ + NoData: map[string]string{ + "A": "1", + "B": "1", + "C": "2", + }, + }, + expectResultLength: 2, + expectResults: Results{ + { + Instance: data.Labels{"datasource_uid": "1"}, + State: NoData, + }, + { + Instance: data.Labels{"datasource_uid": "2"}, + State: NoData, + }, + }, + }, } for _, tc := range cases {