From d519913843896ca171255d56e2a0aabb0221e67d Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 7 Apr 2021 08:28:06 -0400 Subject: [PATCH] AlertingNG: Temp endpoint to translate dashboard alert into rule group (#32694) * Set NoData and ExecErr states * make save an option * TODOs * adjust interval * FOR and alertRuleTags not done yet --- go.sum | 1 + pkg/services/ngalert/api/api.go | 3 + pkg/services/ngalert/api/api_ruler.go | 14 ++ pkg/services/ngalert/api/api_trans_dev.go | 204 +++++++++++++++++++++- 4 files changed, 221 insertions(+), 1 deletion(-) diff --git a/go.sum b/go.sum index 227a0dac6e2..77ccddd54f1 100644 --- a/go.sum +++ b/go.sum @@ -1430,6 +1430,7 @@ github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17 h1:VN3p3Nb github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17/go.mod h1:dv3B1syqmkrkmo665MPCU6L8PbTXIiUeg/OEQULLNxA= github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 h1:eL7x4/zMnlquMxYe7V078BD7MGskZ0daGln+SJCVzuY= github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM= github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index b3458e83386..3a953c86688 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -98,6 +98,9 @@ func (api *API) RegisterAPIEndpoints() { api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { alertDefinitions.Get("/oldByID/:id", middleware.ReqSignedIn, routing.Wrap(api.conditionOldEndpointByID)) }) + api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { + alertDefinitions.Get("/ruleGroupByOldID/:id", middleware.ReqSignedIn, routing.Wrap(api.ruleGroupByOldID)) + }) } api.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) { diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 48b63765207..0ef50a9135e 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -214,3 +214,17 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule) apimodels.GettableExtended }, } } + +func toPostableExtendedRuleNode(r ngmodels.AlertRule) apimodels.PostableExtendedRuleNode { + return apimodels.PostableExtendedRuleNode{ + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + OrgID: r.OrgID, + Title: r.Title, + Condition: r.Condition, + Data: r.Data, + UID: r.UID, + NoDataState: apimodels.NoDataState(r.NoDataState), + ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState), + }, + } +} diff --git a/pkg/services/ngalert/api/api_trans_dev.go b/pkg/services/ngalert/api/api_trans_dev.go index b0dd27c8920..ac0cb1fa2cf 100644 --- a/pkg/services/ngalert/api/api_trans_dev.go +++ b/pkg/services/ngalert/api/api_trans_dev.go @@ -2,14 +2,19 @@ package api import ( "fmt" + "time" + apimodels "github.com/grafana/alerting-api/pkg/api" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/expr/translate" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ngalert/eval" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" + "github.com/prometheus/common/model" ) // conditionEvalEndpoint handles POST /api/alert-definitions/evalOld. @@ -98,7 +103,7 @@ func (api *API) conditionEvalOldEndpointByID(c *models.ReqContext) response.Resp }) } -// conditionEvalEndpoint handles POST /api/alert-definitions/evalOld. +// conditionEvalEndpoint handles POST /api/alert-definitions/oldByID. func (api *API) conditionOldEndpointByID(c *models.ReqContext) response.Response { id := c.ParamsInt64("id") if id == 0 { @@ -131,3 +136,200 @@ func (api *API) conditionOldEndpointByID(c *models.ReqContext) response.Response return response.JSON(200, evalCond) } + +// ruleGroupByOldID handles POST /api/alert-definitions/ruleGroupByOldID. +func (api *API) ruleGroupByOldID(c *models.ReqContext) response.Response { + id := c.ParamsInt64("id") + if id == 0 { + return response.Error(400, "missing id", nil) + } + + save := c.Query("save") == "true" + + // Get dashboard alert definition from database. + oldAlert, status, err := transGetAlertById(id, *c.SignedInUser) + if err != nil { + return response.Error(status, "failed to get alert", fmt.Errorf("failed to get alert for alert id %v: %w", id, err)) + } + + // Translate the dashboard's alerts conditions into SSE queries and conditions. + sseCond, err := transToSSECondition(oldAlert, *c.SignedInUser) + if err != nil { + return response.Error(400, "failed to translate alert conditions", + fmt.Errorf("failed to translate alert conditions for alert id %v: %w", id, err)) + } + + // Get the dashboard that contains the dashboard Alert. + oldAlertsDash, status, err := transGetAlertsDashById(oldAlert.DashboardId, *c.SignedInUser) + if err != nil { + return response.Error(status, "failed to get alert's dashboard", fmt.Errorf("failed to get dashboard for alert id %v, %w", id, err)) + } + + isGeneralFolder := oldAlertsDash.FolderId == 0 && !oldAlertsDash.IsFolder + + var namespaceUID string + + if isGeneralFolder { + namespaceUID = "General" + } else { + // Get the folder that contains the dashboard that contains the dashboard alert. + getFolder := &models.GetDashboardQuery{ + Id: oldAlertsDash.FolderId, + OrgId: oldAlertsDash.OrgId, + } + if err := bus.Dispatch(getFolder); err != nil { + return response.Error(400, fmt.Sprintf("could find folder %v for alert with id %v", getFolder.Id, id), err) + } + + namespaceUID = getFolder.Result.Uid + } + + noDataSetting, execErrSetting, err := transNoDataExecSettings(oldAlert, *c.SignedInUser) + if err != nil { + return response.Error(400, "unable to translate nodata/exec error settings", + fmt.Errorf("unable to translate nodata/exec error settings for alert id %v: %w", id, err)) + } + + // TODO: What to do with Rule Tags + // ruleTags := map[string]string{} + + // for k, v := range oldAlert.Settings.Get("alertRuleTags").MustMap() { + // sV, ok := v.(string) + // if !ok { + // return response.Error(400, "unable to unmarshal rule tags", + // fmt.Errorf("unexpected type %T for tag %v", v, k)) + // } + // ruleTags[k] = sV + // } + + // TODO: Need place to put FOR duration + + rule := ngmodels.AlertRule{ + Title: oldAlert.Name, + Data: sseCond.Data, + Condition: sseCond.Condition, + NoDataState: *noDataSetting, + ExecErrState: *execErrSetting, + } + + rgc := apimodels.PostableRuleGroupConfig{ + // TODO? Generate new name on conflict? + Name: oldAlert.Name, + Interval: transAdjustInterval(oldAlert.Frequency), + Rules: []apimodels.PostableExtendedRuleNode{ + toPostableExtendedRuleNode(rule), + }, + } + + cmd := store.UpdateRuleGroupCmd{ + OrgID: oldAlert.OrgId, + NamespaceUID: namespaceUID, + RuleGroupConfig: rgc, + } + + if !save { + return response.JSON(200, cmd) + } + + // note: Update rule group will set the Interval within the grafana_alert from + // the interval of the group. + err = api.RuleStore.UpdateRuleGroup(cmd) + + if err != nil { + return response.JSON(400, util.DynMap{ + "message:": "failed to save alert rule", + "error": err.Error(), + "cmd": cmd, + }) + } + + return response.JSON(200, cmd) +} + +func transAdjustInterval(freq int64) model.Duration { + // 10 corresponds to the SchedulerCfg, but TODO not worrying about fetching for now. + var baseFreq int64 = 10 + if freq <= baseFreq { + return model.Duration(time.Second * 10) + } + return model.Duration(time.Duration((freq - (freq % baseFreq))) * time.Second) +} + +func transGetAlertById(id int64, user models.SignedInUser) (*models.Alert, int, error) { + getAlert := &models.GetAlertByIdQuery{ + Id: id, + } + + if err := bus.Dispatch(getAlert); err != nil { + return nil, 400, fmt.Errorf("could find alert with id %v: %w", id, err) + } + + if getAlert.Result.OrgId != user.OrgId { + return nil, 403, fmt.Errorf("alert does not match organization of user") + } + + return getAlert.Result, 0, nil +} + +func transGetAlertsDashById(dashboardId int64, user models.SignedInUser) (*models.Dashboard, int, error) { + getDash := &models.GetDashboardQuery{ + Id: dashboardId, + OrgId: user.OrgId, + } + if err := bus.Dispatch(getDash); err != nil { + return nil, 400, fmt.Errorf("could find dashboard with id %v: %w", dashboardId, err) + } + return getDash.Result, 0, nil +} + +func transToSSECondition(m *models.Alert, user models.SignedInUser) (*ngmodels.Condition, error) { + sb, err := m.Settings.ToDB() + if err != nil { + return nil, fmt.Errorf("failed to marshal alert settings: %w", err) + } + + evalCond, err := translate.DashboardAlertConditions(sb, user.OrgId) + if err != nil { + return nil, fmt.Errorf("failed to translate dashboard alert to SSE conditions: %w", err) + } + return evalCond, nil +} + +func transNoDataExecSettings(m *models.Alert, user models.SignedInUser) (*ngmodels.NoDataState, *ngmodels.ExecutionErrorState, error) { + oldNoData := m.Settings.Get("noDataState").MustString() + noDataSetting, err := transNoData(oldNoData) + if err != nil { + return nil, nil, err + } + + oldExecErr := m.Settings.Get("executionErrorState").MustString() + execErrSetting, err := transExecErr(oldExecErr) + if err != nil { + return nil, nil, err + } + return &noDataSetting, &execErrSetting, nil +} + +func transNoData(s string) (ngmodels.NoDataState, error) { + switch s { + case "ok": + return ngmodels.OK, nil + case "no_data": + return ngmodels.NoData, nil + case "alerting": + return ngmodels.Alerting, nil + case "keep_state": + return ngmodels.KeepLastState, nil + } + return ngmodels.NoData, fmt.Errorf("unrecognized No Data setting %v", s) +} + +func transExecErr(s string) (ngmodels.ExecutionErrorState, error) { + switch s { + case "alerting": + return ngmodels.AlertingErrState, nil + case "KeepLastState": + return ngmodels.KeepLastStateErrState, nil + } + return ngmodels.AlertingErrState, fmt.Errorf("unrecognized Execution Error setting %v", s) +}