// Package eval executes the condition for an alert definition, evaluates the condition results, and // returns the alert instance states. package eval import ( "context" "fmt" "time" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr" ) const alertingEvaluationTimeout = 30 * time.Second type Evaluator struct { Cfg *setting.Cfg } // invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results. type invalidEvalResultFormatError struct { refID string reason string err error } func (e *invalidEvalResultFormatError) Error() string { s := fmt.Sprintf("invalid format of evaluation results for the alert definition %s: %s", e.refID, e.reason) if e.err != nil { s = fmt.Sprintf("%s: %s", s, e.err.Error()) } return s } func (e *invalidEvalResultFormatError) Unwrap() error { return e.err } // Condition contains backend expressions and queries and the RefID // of the query or expression that will be evaluated. type Condition struct { RefID string `json:"refId"` OrgID int64 `json:"-"` QueriesAndExpressions []AlertQuery `json:"queriesAndExpressions"` } // ExecutionResults contains the unevaluated results from executing // a condition. type ExecutionResults struct { AlertDefinitionID int64 Error error Results data.Frames } // Results is a slice of evaluated alert instances states. type Results []result // result contains the evaluated state of an alert instance // identified by its labels. type result struct { Instance data.Labels State state // Enum } // state is an enum of the evaluation state for an alert instance. type state int const ( // Normal is the eval state for an alert instance condition // that evaluated to false. Normal state = iota // Alerting is the eval state for an alert instance condition // that evaluated to false. Alerting ) func (s state) String() string { return [...]string{"Normal", "Alerting"}[s] } // IsValid checks the condition's validity. func (c Condition) IsValid() bool { // TODO search for refIDs in QueriesAndExpressions return len(c.QueriesAndExpressions) != 0 } // AlertExecCtx is the context provided for executing an alert condition. type AlertExecCtx struct { OrgID int64 ExpressionsEnabled bool Ctx context.Context } // execute runs the Condition's expressions or queries. func (c *Condition) execute(ctx AlertExecCtx, now time.Time) (*ExecutionResults, error) { result := ExecutionResults{} if !c.IsValid() { return nil, fmt.Errorf("invalid conditions") // TODO: Things probably } queryDataReq := &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ OrgID: ctx.OrgID, }, Queries: []backend.DataQuery{}, } for i := range c.QueriesAndExpressions { q := c.QueriesAndExpressions[i] model, err := q.getModel() if err != nil { return nil, fmt.Errorf("failed to get query model: %w", err) } interval, err := q.getIntervalDuration() if err != nil { return nil, fmt.Errorf("failed to retrieve intervalMs from the model: %w", err) } maxDatapoints, err := q.getMaxDatapoints() if err != nil { return nil, fmt.Errorf("failed to retrieve maxDatapoints from the model: %w", err) } queryDataReq.Queries = append(queryDataReq.Queries, backend.DataQuery{ JSON: model, Interval: interval, RefID: q.RefID, MaxDataPoints: maxDatapoints, QueryType: q.QueryType, TimeRange: q.RelativeTimeRange.toTimeRange(now), }) } exprService := expr.Service{Cfg: &setting.Cfg{ExpressionsEnabled: ctx.ExpressionsEnabled}} pbRes, err := exprService.TransformData(ctx.Ctx, queryDataReq) if err != nil { return &result, err } for refID, res := range pbRes.Responses { if refID != c.RefID { continue } result.Results = res.Frames } if len(result.Results) == 0 { err = fmt.Errorf("no GEL results") result.Error = err return &result, err } return &result, nil } // evaluateExecutionResult takes the ExecutionResult, and returns a frame where // each column is a string type that holds a string representing its state. func evaluateExecutionResult(results *ExecutionResults) (Results, error) { evalResults := make([]result, 0) labels := make(map[string]bool) for _, f := range results.Results { rowLen, err := f.RowLen() if err != nil { return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: "unable to get frame row length", err: err} } if rowLen > 1 { return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("unexpected row length: %d instead of 1", rowLen)} } if len(f.Fields) > 1 { return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("unexpected field length: %d instead of 1", len(f.Fields))} } if f.Fields[0].Type() != data.FieldTypeNullableFloat64 { return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("invalid field type: %d", f.Fields[0].Type())} } labelsStr := f.Fields[0].Labels.String() _, ok := labels[labelsStr] if ok { return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("frame cannot uniquely be identified by its labels: %s", labelsStr)} } labels[labelsStr] = true state := Normal val, err := f.Fields[0].FloatAt(0) if err != nil || val != 0 { state = Alerting } evalResults = append(evalResults, result{ Instance: f.Fields[0].Labels, State: state, }) } return evalResults, nil } // AsDataFrame forms the EvalResults in Frame suitable for displaying in the table panel of the front end. // This may be temporary, as there might be a fair amount we want to display in the frontend, and it might not make sense to store that in data.Frame. // For the first pass, I would expect a Frame with a single row, and a column for each instance with a boolean value. func (evalResults Results) AsDataFrame() data.Frame { fields := make([]*data.Field, 0) for _, evalResult := range evalResults { fields = append(fields, data.NewField("", evalResult.Instance, []bool{evalResult.State != Normal})) } f := data.NewFrame("", fields...) return *f } // ConditionEval executes conditions and evaluates the result. func (e *Evaluator) ConditionEval(condition *Condition, now time.Time) (Results, error) { alertCtx, cancelFn := context.WithTimeout(context.Background(), alertingEvaluationTimeout) defer cancelFn() alertExecCtx := AlertExecCtx{OrgID: condition.OrgID, Ctx: alertCtx, ExpressionsEnabled: e.Cfg.ExpressionsEnabled} execResult, err := condition.execute(alertExecCtx, now) if err != nil { return nil, fmt.Errorf("failed to execute conditions: %w", err) } evalResults, err := evaluateExecutionResult(execResult) if err != nil { return nil, fmt.Errorf("failed to evaluate results: %w", err) } return evalResults, nil }