mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat(alerting): add support for query service instant vectors (#92091)
This commit is contained in:
parent
04d9fa04a7
commit
eabf3b9f73
@ -15,7 +15,6 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||||
"github.com/grafana/grafana/pkg/expr"
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
"github.com/grafana/grafana/pkg/expr/classic"
|
"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.
|
// 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.
|
// 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:
|
// Per Frame, data becomes a State based on the following rules:
|
||||||
//
|
//
|
||||||
// If no value is set:
|
// If no value is set:
|
||||||
@ -702,6 +704,13 @@ func evaluateExecutionResult(execResults ExecutionResults, ts time.Time) Results
|
|||||||
continue
|
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 {
|
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."})
|
appendErrRes(&invalidEvalResultFormatError{refID: f.RefID, reason: "looks like time series data, only reduced data can be alerted on."})
|
||||||
continue
|
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
|
val := f.Fields[0].At(0).(*float64) // type checked by data.FieldTypeNullableFloat64 above
|
||||||
|
r := buildResult(f, val, ts)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
evalResults = append(evalResults, r)
|
evalResults = append(evalResults, r)
|
||||||
}
|
}
|
||||||
@ -776,6 +769,39 @@ func evaluateExecutionResult(execResults ExecutionResults, ts time.Time) Results
|
|||||||
return evalResults
|
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.
|
// 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.
|
// 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 {
|
func (evalResults Results) AsDataFrame() data.Frame {
|
||||||
|
@ -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",
|
desc: "nil value single instance is single a NoData state result",
|
||||||
execResults: ExecutionResults{
|
execResults: ExecutionResults{
|
||||||
|
Loading…
Reference in New Issue
Block a user