Skip to content

Commit

Permalink
feat(alerting): add support for query service instant vectors (grafan…
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnnyQQQQ authored Sep 12, 2024
1 parent 04d9fa0 commit eabf3b9
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 18 deletions.
62 changes: 44 additions & 18 deletions pkg/services/ngalert/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down
46 changes: 46 additions & 0 deletions pkg/services/ngalert/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit eabf3b9

Please sign in to comment.