AlertingNG: remove warn/crit from eval prototype (#28334)

and misc cleanup
This commit is contained in:
Kyle Brandt
2020-10-16 12:33:57 -04:00
committed by GitHub
parent 4c8e4f35d1
commit 44a795cb17
4 changed files with 54 additions and 46 deletions

View File

@ -355,7 +355,7 @@ func (hs *HTTPServer) registerRoutes() {
if hs.Cfg.IsNgAlertEnabled() { if hs.Cfg.IsNgAlertEnabled() {
apiRoute.Group("/alert-definitions", func(alertDefinitions routing.RouteRegister) { apiRoute.Group("/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Get("/eval/:dashboardID/:panelID/:refID", reqEditorRole, Wrap(hs.AlertDefinitionEval)) alertDefinitions.Get("/eval/:dashboardID/:panelID/:refID", reqEditorRole, Wrap(hs.AlertDefinitionEval))
alertDefinitions.Post("/eval", reqEditorRole, bind(dtos.EvalAlertConditionsCommand{}), Wrap(hs.ConditionsEval)) alertDefinitions.Post("/eval", reqEditorRole, bind(dtos.EvalAlertConditionCommand{}), Wrap(hs.ConditionEval))
}) })
} }

View File

@ -6,7 +6,7 @@ import (
eval "github.com/grafana/grafana/pkg/services/ngalert" eval "github.com/grafana/grafana/pkg/services/ngalert"
) )
type EvalAlertConditionsCommand struct { type EvalAlertConditionCommand struct {
Conditions eval.Conditions `json:"conditions"` Condition eval.Condition `json:"condition"`
Now time.Time `json:"now"` Now time.Time `json:"now"`
} }

View File

@ -13,7 +13,7 @@ import (
) )
// POST /api/alert-definitions/eval // POST /api/alert-definitions/eval
func (hs *HTTPServer) ConditionsEval(c *models.ReqContext, dto dtos.EvalAlertConditionsCommand) Response { func (hs *HTTPServer) ConditionEval(c *models.ReqContext, dto dtos.EvalAlertConditionCommand) Response {
alertCtx, cancelFn := context.WithTimeout(context.Background(), setting.AlertingEvaluationTimeout) alertCtx, cancelFn := context.WithTimeout(context.Background(), setting.AlertingEvaluationTimeout)
defer cancelFn() defer cancelFn()
@ -29,7 +29,7 @@ func (hs *HTTPServer) ConditionsEval(c *models.ReqContext, dto dtos.EvalAlertCon
toStr = "now" toStr = "now"
} }
execResult, err := dto.Conditions.Execute(alertExecCtx, fromStr, toStr) execResult, err := dto.Condition.Execute(alertExecCtx, fromStr, toStr)
if err != nil { if err != nil {
return Error(400, "Failed to execute conditions", err) return Error(400, "Failed to execute conditions", err)
} }
@ -67,7 +67,7 @@ func (hs *HTTPServer) AlertDefinitionEval(c *models.ReqContext) Response {
toStr = "now" toStr = "now"
} }
conditions, err := hs.AlertNG.LoadAlertConditions(dashboardID, panelID, conditionRefID, c.SignedInUser, c.SkipCache) conditions, err := hs.AlertNG.LoadAlertCondition(dashboardID, panelID, conditionRefID, c.SignedInUser, c.SkipCache)
if err != nil { if err != nil {
return Error(400, "Failed to load conditions", err) return Error(400, "Failed to load conditions", err)
} }

View File

@ -1,3 +1,5 @@
// Package eval executes the condition for an alert definition, evaluates the condition results, and
// returns the alert instance states.
package eval package eval
import ( import (
@ -24,6 +26,7 @@ type minimalDashboard struct {
} `json:"panels"` } `json:"panels"`
} }
// AlertNG is the service for evaluating the condition of an alert definition.
type AlertNG struct { type AlertNG struct {
DatasourceCache datasources.CacheService `inject:""` DatasourceCache datasources.CacheService `inject:""`
} }
@ -33,10 +36,11 @@ func init() {
} }
// Init initializes the AlertingService. // Init initializes the AlertingService.
func (e *AlertNG) Init() error { func (ng *AlertNG) Init() error {
return nil return nil
} }
// AlertExecCtx is the context provided for executing an alert condition.
type AlertExecCtx struct { type AlertExecCtx struct {
AlertDefitionID int64 AlertDefitionID int64
SignedInUser *models.SignedInUser SignedInUser *models.SignedInUser
@ -44,55 +48,59 @@ type AlertExecCtx struct {
Ctx context.Context Ctx context.Context
} }
// At least Warn or Crit condition must be non-empty // Condition contains backend expressions and queries and the RefID
type Conditions struct { // of the query or expression that will be evaluated.
Condition string `json:"condition"` type Condition struct {
RefID string `json:"refId"`
QueriesAndExpressions []tsdb.Query `json:"queriesAndExpressions"` QueriesAndExpressions []tsdb.Query `json:"queriesAndExpressions"`
} }
type ExecutionResult struct { // ExecutionResults contains the unevaluated results from executing
AlertDefinitionId int64 // a condition.
type ExecutionResults struct {
AlertDefinitionID int64
Error error Error error
Results data.Frames Results data.Frames
} }
type EvalResults []EvalResult // Results is a slice of evaluated alert instances states.
type Results []Result
type EvalResult struct { // Result contains the evaluated state of an alert instance
// identified by its labels.
type Result struct {
Instance data.Labels Instance data.Labels
State State // Enum State State // Enum
} }
// State is an enum of the evaluation state for an alert instance.
type State int type State int
const ( const (
// Normal is the eval state for an alert instance condition
// that evaluated to false.
Normal State = iota Normal State = iota
Warning
Critical // Alerting is the eval state for an alert instance condition
Error // that evaluated to false.
Alerting
) )
func (s State) String() string { func (s State) String() string {
return [...]string{"Normal", "Warning", "Critical", "Error"}[s] return [...]string{"Normal", "Alerting"}[s]
} }
// IsValid checks the conditions validity // IsValid checks the conditions validity
func (c Conditions) IsValid() bool { func (c Condition) IsValid() bool {
/*
if c.WarnCondition == "" && c.CritCondition == "" {
return false
}
*/
// TODO search for refIDs in QueriesAndExpressions // TODO search for refIDs in QueriesAndExpressions
return len(c.QueriesAndExpressions) != 0 return len(c.QueriesAndExpressions) != 0
} }
// LoadAlertConditions returns a Conditions object for the given alertDefintionId. // LoadAlertCondition returns a Condition object for the given alertDefintionId.
func (ng *AlertNG) LoadAlertConditions(dashboardID int64, panelID int64, conditionRefID string, signedInUser *models.SignedInUser, skipCache bool) (*Conditions, error) { func (ng *AlertNG) LoadAlertCondition(dashboardID int64, panelID int64, conditionRefID string, signedInUser *models.SignedInUser, skipCache bool) (*Condition, error) {
// get queries from the dashboard (because GEL expressions cannot be stored in alerts so far) // get queries from the dashboard (because GEL expressions cannot be stored in alerts so far)
getDashboardQuery := models.GetDashboardQuery{Id: dashboardID} getDashboardQuery := models.GetDashboardQuery{Id: dashboardID}
if err := bus.Dispatch(&getDashboardQuery); err != nil { if err := bus.Dispatch(&getDashboardQuery); err != nil {
@ -109,7 +117,7 @@ func (ng *AlertNG) LoadAlertConditions(dashboardID int64, panelID int64, conditi
return nil, errors.New("Failed to unmarshal dashboard JSON") return nil, errors.New("Failed to unmarshal dashboard JSON")
} }
conditions := Conditions{} condition := Condition{}
for _, p := range dash.Panels { for _, p := range dash.Panels {
if p.ID == panelID { if p.ID == panelID {
panelDatasource := p.Datasource panelDatasource := p.Datasource
@ -148,8 +156,8 @@ func (ng *AlertNG) LoadAlertConditions(dashboardID int64, panelID int64, conditi
} }
if query.Get("orgId").MustString() == "" { // GEL requires orgID inside the query JSON if query.Get("orgId").MustString() == "" { // GEL requires orgID inside the query JSON
// need to decide which organisation id is expected there // need to decide which organization id is expected there
// in grafana queries is passed the signed in user organisation id: // in grafana queries is passed the signed in user organization id:
// https://github.com/grafana/grafana/blob/34a355fe542b511ed02976523aa6716aeb00bde6/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts#L60 // https://github.com/grafana/grafana/blob/34a355fe542b511ed02976523aa6716aeb00bde6/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts#L60
// but I think that it should be datasource org id instead // but I think that it should be datasource org id instead
query.Set("orgId", 0) query.Set("orgId", 0)
@ -165,7 +173,7 @@ func (ng *AlertNG) LoadAlertConditions(dashboardID int64, panelID int64, conditi
query.Set("intervalMs", 1000) query.Set("intervalMs", 1000)
} }
conditions.QueriesAndExpressions = append(conditions.QueriesAndExpressions, tsdb.Query{ condition.QueriesAndExpressions = append(condition.QueriesAndExpressions, tsdb.Query{
RefId: refID, RefId: refID,
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100), MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
IntervalMs: query.Get("intervalMs").MustInt64(1000), IntervalMs: query.Get("intervalMs").MustInt64(1000),
@ -176,14 +184,14 @@ func (ng *AlertNG) LoadAlertConditions(dashboardID int64, panelID int64, conditi
} }
} }
} }
conditions.Condition = conditionRefID condition.RefID = conditionRefID
return &conditions, nil return &condition, nil
} }
// Execute runs the WarnCondition and CritCondtion expressions or queries. // Execute runs the Condition's expressions or queries.
func (conditions *Conditions) Execute(ctx AlertExecCtx, fromStr, toStr string) (*ExecutionResult, error) { func (c *Condition) Execute(ctx AlertExecCtx, fromStr, toStr string) (*ExecutionResults, error) {
result := ExecutionResult{} result := ExecutionResults{}
if !conditions.IsValid() { if !c.IsValid() {
return nil, fmt.Errorf("Invalid conditions") return nil, fmt.Errorf("Invalid conditions")
} }
@ -192,8 +200,8 @@ func (conditions *Conditions) Execute(ctx AlertExecCtx, fromStr, toStr string) (
Debug: true, Debug: true,
User: ctx.SignedInUser, User: ctx.SignedInUser,
} }
for i := range conditions.QueriesAndExpressions { for i := range c.QueriesAndExpressions {
request.Queries = append(request.Queries, &conditions.QueriesAndExpressions[i]) request.Queries = append(request.Queries, &c.QueriesAndExpressions[i])
} }
resp, err := plugins.Transform.Transform(ctx.Ctx, request) resp, err := plugins.Transform.Transform(ctx.Ctx, request)
@ -202,7 +210,7 @@ func (conditions *Conditions) Execute(ctx AlertExecCtx, fromStr, toStr string) (
return &result, err return &result, err
} }
conditionResult := resp.Results[conditions.Condition] conditionResult := resp.Results[c.RefID]
if conditionResult == nil { if conditionResult == nil {
err = fmt.Errorf("No GEL results") err = fmt.Errorf("No GEL results")
result.Error = err result.Error = err
@ -220,8 +228,8 @@ func (conditions *Conditions) Execute(ctx AlertExecCtx, fromStr, toStr string) (
// EvaluateExecutionResult takes the ExecutionResult, and returns a frame where // EvaluateExecutionResult takes the ExecutionResult, and returns a frame where
// each column is a string type that holds a string representing its state. // each column is a string type that holds a string representing its state.
func EvaluateExecutionResult(results *ExecutionResult) (EvalResults, error) { func EvaluateExecutionResult(results *ExecutionResults) (Results, error) {
evalResults := make([]EvalResult, 0) evalResults := make([]Result, 0)
labels := make(map[string]bool) labels := make(map[string]bool)
for _, f := range results.Results { for _, f := range results.Results {
rowLen, err := f.RowLen() rowLen, err := f.RowLen()
@ -250,10 +258,10 @@ func EvaluateExecutionResult(results *ExecutionResult) (EvalResults, error) {
state := Normal state := Normal
val, err := f.Fields[0].FloatAt(0) val, err := f.Fields[0].FloatAt(0)
if err != nil || val != 0 { if err != nil || val != 0 {
state = Critical state = Alerting
} }
evalResults = append(evalResults, EvalResult{ evalResults = append(evalResults, Result{
Instance: f.Fields[0].Labels, Instance: f.Fields[0].Labels,
State: state, State: state,
}) })
@ -264,7 +272,7 @@ func EvaluateExecutionResult(results *ExecutionResult) (EvalResults, error) {
// 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.
// 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. // 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. // 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 EvalResults) AsDataFrame() data.Frame { func (evalResults Results) AsDataFrame() data.Frame {
fields := make([]*data.Field, 0) fields := make([]*data.Field, 0)
for _, evalResult := range evalResults { for _, evalResult := range evalResults {
fields = append(fields, data.NewField("", evalResult.Instance, []bool{evalResult.State != Normal})) fields = append(fields, data.NewField("", evalResult.Instance, []bool{evalResult.State != Normal}))