mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 02:23:31 -06:00
This changes the logic for the alert validation in the extractor. Validation is executed twice, once before attempting to save the alerts and once after saving the dashboard while saving the alerts to the db. The first validation will not require that the alert has a dashboard id which allows new dashboards with alerts to be saved. Fixes #11247
228 lines
6.0 KiB
Go
228 lines
6.0 KiB
Go
package alerting
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/log"
|
|
m "github.com/grafana/grafana/pkg/models"
|
|
)
|
|
|
|
// DashAlertExtractor extracts alerts from the dashboard json
|
|
type DashAlertExtractor struct {
|
|
Dash *m.Dashboard
|
|
OrgID int64
|
|
log log.Logger
|
|
}
|
|
|
|
// NewDashAlertExtractor returns a new DashAlertExtractor
|
|
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
|
|
return &DashAlertExtractor{
|
|
Dash: dash,
|
|
OrgID: orgID,
|
|
log: log.New("alerting.extractor"),
|
|
}
|
|
}
|
|
|
|
func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*m.DataSource, error) {
|
|
if dsName == "" {
|
|
query := &m.GetDataSourcesQuery{OrgId: e.OrgID}
|
|
if err := bus.Dispatch(query); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ds := range query.Result {
|
|
if ds.IsDefault {
|
|
return ds, nil
|
|
}
|
|
}
|
|
} else {
|
|
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgID}
|
|
if err := bus.Dispatch(query); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return query.Result, nil
|
|
}
|
|
|
|
return nil, errors.New("Could not find datasource id for " + dsName)
|
|
}
|
|
|
|
func findPanelQueryByRefID(panel *simplejson.Json, refID string) *simplejson.Json {
|
|
for _, targetsObj := range panel.Get("targets").MustArray() {
|
|
target := simplejson.NewFromAny(targetsObj)
|
|
|
|
if target.Get("refId").MustString() == refID {
|
|
return target
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copyJSON(in *simplejson.Json) (*simplejson.Json, error) {
|
|
rawJSON, err := in.MarshalJSON()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return simplejson.NewJson(rawJSON)
|
|
}
|
|
|
|
func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, validateAlertFunc func(*m.Alert) bool) ([]*m.Alert, error) {
|
|
alerts := make([]*m.Alert, 0)
|
|
|
|
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
|
panel := simplejson.NewFromAny(panelObj)
|
|
|
|
collapsedJSON, collapsed := panel.CheckGet("collapsed")
|
|
// check if the panel is collapsed
|
|
if collapsed && collapsedJSON.MustBool() {
|
|
|
|
// extract alerts from sub panels for collapsed panels
|
|
als, err := e.getAlertFromPanels(panel, validateAlertFunc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
alerts = append(alerts, als...)
|
|
continue
|
|
}
|
|
|
|
jsonAlert, hasAlert := panel.CheckGet("alert")
|
|
|
|
if !hasAlert {
|
|
continue
|
|
}
|
|
|
|
panelID, err := panel.Get("id").Int64()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("panel id is required. err %v", err)
|
|
}
|
|
|
|
// backward compatibility check, can be removed later
|
|
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
|
if hasEnabled && enabled.MustBool() == false {
|
|
continue
|
|
}
|
|
|
|
frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
|
|
if err != nil {
|
|
return nil, ValidationError{Reason: "Could not parse frequency"}
|
|
}
|
|
|
|
alert := &m.Alert{
|
|
DashboardId: e.Dash.Id,
|
|
OrgId: e.OrgID,
|
|
PanelId: panelID,
|
|
Id: jsonAlert.Get("id").MustInt64(),
|
|
Name: jsonAlert.Get("name").MustString(),
|
|
Handler: jsonAlert.Get("handler").MustInt64(),
|
|
Message: jsonAlert.Get("message").MustString(),
|
|
Frequency: frequency,
|
|
}
|
|
|
|
for _, condition := range jsonAlert.Get("conditions").MustArray() {
|
|
jsonCondition := simplejson.NewFromAny(condition)
|
|
|
|
jsonQuery := jsonCondition.Get("query")
|
|
queryRefID := jsonQuery.Get("params").MustArray()[0].(string)
|
|
panelQuery := findPanelQueryByRefID(panel, queryRefID)
|
|
|
|
if panelQuery == nil {
|
|
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefID)
|
|
return nil, ValidationError{Reason: reason}
|
|
}
|
|
|
|
dsName := ""
|
|
if panelQuery.Get("datasource").MustString() != "" {
|
|
dsName = panelQuery.Get("datasource").MustString()
|
|
} else if panel.Get("datasource").MustString() != "" {
|
|
dsName = panel.Get("datasource").MustString()
|
|
}
|
|
|
|
datasource, err := e.lookupDatasourceID(dsName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
|
|
|
if interval, err := panel.Get("interval").String(); err == nil {
|
|
panelQuery.Set("interval", interval)
|
|
}
|
|
|
|
jsonQuery.Set("model", panelQuery.Interface())
|
|
}
|
|
|
|
alert.Settings = jsonAlert
|
|
|
|
// validate
|
|
_, err = NewRuleFromDBAlert(alert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !validateAlertFunc(alert) {
|
|
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
|
|
return nil, m.ErrDashboardContainsInvalidAlertData
|
|
}
|
|
|
|
alerts = append(alerts, alert)
|
|
}
|
|
|
|
return alerts, nil
|
|
}
|
|
|
|
func validateAlertRule(alert *m.Alert) bool {
|
|
return alert.ValidToSave()
|
|
}
|
|
|
|
// GetAlerts extracts alerts from the dashboard json and does full validation on the alert json data
|
|
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
|
return e.extractAlerts(validateAlertRule)
|
|
}
|
|
|
|
func (e *DashAlertExtractor) extractAlerts(validateFunc func(alert *m.Alert) bool) ([]*m.Alert, error) {
|
|
dashboardJSON, err := copyJSON(e.Dash.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
alerts := make([]*m.Alert, 0)
|
|
|
|
// We extract alerts from rows to be backwards compatible
|
|
// with the old dashboard json model.
|
|
rows := dashboardJSON.Get("rows").MustArray()
|
|
if len(rows) > 0 {
|
|
for _, rowObj := range rows {
|
|
row := simplejson.NewFromAny(rowObj)
|
|
a, err := e.getAlertFromPanels(row, validateFunc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
alerts = append(alerts, a...)
|
|
}
|
|
} else {
|
|
a, err := e.getAlertFromPanels(dashboardJSON, validateFunc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
alerts = append(alerts, a...)
|
|
}
|
|
|
|
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
|
return alerts, nil
|
|
}
|
|
|
|
// ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id
|
|
// in the first validation pass
|
|
func (e *DashAlertExtractor) ValidateAlerts() error {
|
|
_, err := e.extractAlerts(func(alert *m.Alert) bool { return alert.OrgId != 0 && alert.PanelId != 0 })
|
|
return err
|
|
}
|