package expr import ( "fmt" "strings" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/expr/classic" "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/services/featuremgmt" ) // Once we are comfortable with the parsing logic, this struct will // be merged/replace the existing Query struct in grafana/pkg/expr/transform.go type ExpressionQuery struct { GraphID int64 `json:"id,omitempty"` RefID string `json:"refId"` QueryType QueryType `json:"type"` // The typed query parameters Properties any `json:"properties"` // Hidden in debug JSON Command Command `json:"-"` } // ID is used to identify nodes in the directed graph func (q ExpressionQuery) ID() int64 { return q.GraphID } type ExpressionQueryReader struct { features featuremgmt.FeatureToggles } func NewExpressionQueryReader(features featuremgmt.FeatureToggles) *ExpressionQueryReader { return &ExpressionQueryReader{ features: features, } } // nolint:gocyclo func (h *ExpressionQueryReader) ReadQuery( // Properties that have been parsed off the same node common data.DataQuery, // An iterator with context for the full node (include common values) iter *jsoniter.Iterator, ) (eq ExpressionQuery, err error) { referenceVar := "" eq.RefID = common.RefID eq.QueryType = QueryType(common.GetString("type")) if eq.QueryType == "" { return eq, fmt.Errorf("missing type") } switch eq.QueryType { case QueryTypeMath: q := &MathQuery{} err = iter.ReadVal(q) if err == nil { eq.Command, err = NewMathCommand(common.RefID, q.Expression) eq.Properties = q } case QueryTypeReduce: var mapper mathexp.ReduceMapper = nil q := &ReduceQuery{} err = iter.ReadVal(q) if err == nil { referenceVar, err = getReferenceVar(q.Expression, common.RefID) eq.Properties = q } if err == nil && q.Settings != nil { switch q.Settings.Mode { case ReduceModeStrict: mapper = nil case ReduceModeDrop: mapper = mathexp.DropNonNumber{} case ReduceModeReplace: if q.Settings.ReplaceWithValue == nil { err = fmt.Errorf("setting replaceWithValue must be specified when mode is '%s'", q.Settings.Mode) } mapper = mathexp.ReplaceNonNumberWithValue{Value: *q.Settings.ReplaceWithValue} default: err = fmt.Errorf("unsupported reduce mode") } } if err == nil { eq.Properties = q eq.Command, err = NewReduceCommand(common.RefID, q.Reducer, referenceVar, mapper) } case QueryTypeResample: q := &ResampleQuery{} err = iter.ReadVal(q) if err == nil && common.TimeRange == nil { err = fmt.Errorf("missing time range in query") } if err == nil { referenceVar, err = getReferenceVar(q.Expression, common.RefID) } if err == nil { tr := gtime.NewTimeRange(common.TimeRange.From, common.TimeRange.To) eq.Properties = q eq.Command, err = NewResampleCommand(common.RefID, q.Window, referenceVar, q.Downsampler, q.Upsampler, AbsoluteTimeRange{ From: tr.GetFromAsTimeUTC(), To: tr.GetToAsTimeUTC(), }, ) } case QueryTypeClassic: q := &ClassicQuery{} err = iter.ReadVal(q) if err == nil { eq.Properties = q eq.Command, err = classic.NewConditionCmd(common.RefID, q.Conditions) } case QueryTypeSQL: enabled := enableSqlExpressions(h) if !enabled { return eq, fmt.Errorf("sqlExpressions is not implemented") } q := &SQLExpression{} err = iter.ReadVal(q) if err == nil { eq.Properties = q eq.Command, err = NewSQLCommand(common.RefID, q.Expression) } case QueryTypeThreshold: q := &ThresholdQuery{} err = iter.ReadVal(q) if err == nil { referenceVar, err = getReferenceVar(q.Expression, common.RefID) } if err == nil { // we only support one condition for now, we might want to turn this in to "OR" expressions later if len(q.Conditions) != 1 { return eq, fmt.Errorf("threshold expression requires exactly one condition") } firstCondition := q.Conditions[0] threshold, err := NewThresholdCommand(common.RefID, referenceVar, firstCondition.Evaluator.Type, firstCondition.Evaluator.Params) if err != nil { return eq, fmt.Errorf("invalid condition: %w", err) } eq.Command = threshold eq.Properties = q if firstCondition.UnloadEvaluator != nil && h.features.IsEnabledGlobally(featuremgmt.FlagRecoveryThreshold) { unloading, err := NewThresholdCommand(common.RefID, referenceVar, firstCondition.UnloadEvaluator.Type, firstCondition.UnloadEvaluator.Params) unloading.Invert = true if err != nil { return eq, fmt.Errorf("invalid unloadCondition: %w", err) } var d Fingerprints if firstCondition.LoadedDimensions != nil { d, err = FingerprintsFromFrame(firstCondition.LoadedDimensions) if err != nil { return eq, fmt.Errorf("failed to parse loaded dimensions: %w", err) } } eq.Command, err = NewHysteresisCommand(common.RefID, referenceVar, *threshold, *unloading, d) if err != nil { return eq, err } } } default: err = fmt.Errorf("unknown query type (%s)", common.QueryType) } return eq, err } func getReferenceVar(exp string, refId string) (string, error) { exp = strings.TrimPrefix(exp, "$") if exp == "" { return "", fmt.Errorf("no variable specified to reference for refId %v", refId) } return exp, nil } func enableSqlExpressions(h *ExpressionQueryReader) bool { enabled := !h.features.IsEnabledGlobally(featuremgmt.FlagSqlExpressions) if enabled { return false } return false }