mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
d519913843
* Set NoData and ExecErr states * make save an option * TODOs * adjust interval * FOR and alertRuleTags not done yet
336 lines
9.7 KiB
Go
336 lines
9.7 KiB
Go
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.
|
|
func (api *API) conditionEvalOldEndpoint(c *models.ReqContext) response.Response {
|
|
b, err := c.Req.Body().Bytes()
|
|
if err != nil {
|
|
response.Error(400, "failed to read body", err)
|
|
}
|
|
evalCond, err := translate.DashboardAlertConditions(b, c.OrgId)
|
|
if err != nil {
|
|
return response.Error(400, "Failed to translate alert conditions", err)
|
|
}
|
|
|
|
if err := api.validateCondition(*evalCond, c.SignedInUser, c.SkipCache); err != nil {
|
|
return response.Error(400, "invalid condition", err)
|
|
}
|
|
|
|
//now := cmd.Now
|
|
//if now.IsZero() {
|
|
//now := timeNow()
|
|
//}
|
|
|
|
evaluator := eval.Evaluator{Cfg: api.Cfg}
|
|
evalResults, err := evaluator.ConditionEval(evalCond, timeNow(), api.DataService)
|
|
if err != nil {
|
|
return response.Error(400, "Failed to evaluate conditions", err)
|
|
}
|
|
|
|
frame := evalResults.AsDataFrame()
|
|
|
|
return response.JSONStreaming(200, util.DynMap{
|
|
"instances": []*data.Frame{&frame},
|
|
})
|
|
}
|
|
|
|
// conditionEvalEndpoint handles POST /api/alert-definitions/evalOld.
|
|
func (api *API) conditionEvalOldEndpointByID(c *models.ReqContext) response.Response {
|
|
id := c.ParamsInt64("id")
|
|
if id == 0 {
|
|
return response.Error(400, "missing id", nil)
|
|
}
|
|
|
|
getAlert := &models.GetAlertByIdQuery{
|
|
Id: id,
|
|
}
|
|
|
|
if err := bus.Dispatch(getAlert); err != nil {
|
|
return response.Error(400, fmt.Sprintf("could find alert with id %v", id), err)
|
|
}
|
|
|
|
if getAlert.Result.OrgId != c.SignedInUser.OrgId {
|
|
return response.Error(403, "alert does not match organization of user", nil)
|
|
}
|
|
|
|
settings := getAlert.Result.Settings
|
|
|
|
sb, err := settings.ToDB()
|
|
if err != nil {
|
|
return response.Error(400, "failed to marshal alert settings", err)
|
|
}
|
|
|
|
evalCond, err := translate.DashboardAlertConditions(sb, c.OrgId)
|
|
if err != nil {
|
|
return response.Error(400, "Failed to translate alert conditions", err)
|
|
}
|
|
|
|
if err := api.validateCondition(*evalCond, c.SignedInUser, c.SkipCache); err != nil {
|
|
return response.Error(400, "invalid condition", err)
|
|
}
|
|
|
|
//now := cmd.Now
|
|
//if now.IsZero() {
|
|
//now := timeNow()
|
|
//}
|
|
|
|
evaluator := eval.Evaluator{Cfg: api.Cfg}
|
|
evalResults, err := evaluator.ConditionEval(evalCond, timeNow(), api.DataService)
|
|
if err != nil {
|
|
return response.Error(400, "Failed to evaluate conditions", err)
|
|
}
|
|
|
|
frame := evalResults.AsDataFrame()
|
|
|
|
return response.JSONStreaming(200, util.DynMap{
|
|
"instances": []*data.Frame{&frame},
|
|
})
|
|
}
|
|
|
|
// conditionEvalEndpoint handles POST /api/alert-definitions/oldByID.
|
|
func (api *API) conditionOldEndpointByID(c *models.ReqContext) response.Response {
|
|
id := c.ParamsInt64("id")
|
|
if id == 0 {
|
|
return response.Error(400, "missing id", nil)
|
|
}
|
|
|
|
getAlert := &models.GetAlertByIdQuery{
|
|
Id: id,
|
|
}
|
|
|
|
if err := bus.Dispatch(getAlert); err != nil {
|
|
return response.Error(400, fmt.Sprintf("could find alert with id %v", id), err)
|
|
}
|
|
|
|
if getAlert.Result.OrgId != c.SignedInUser.OrgId {
|
|
return response.Error(403, "alert does not match organization of user", nil)
|
|
}
|
|
|
|
settings := getAlert.Result.Settings
|
|
|
|
sb, err := settings.ToDB()
|
|
if err != nil {
|
|
return response.Error(400, "failed to marshal alert settings", err)
|
|
}
|
|
|
|
evalCond, err := translate.DashboardAlertConditions(sb, c.OrgId)
|
|
if err != nil {
|
|
return response.Error(400, "Failed to translate alert conditions", err)
|
|
}
|
|
|
|
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)
|
|
}
|