Merge pull request #11411 from grafana/11247-save-alerts

dashboard: allow alerts to be saved for new/provisioned dashboards
This commit is contained in:
Carl Bergquist
2018-03-28 12:59:14 +02:00
committed by GitHub
5 changed files with 366 additions and 54 deletions

View File

@@ -13,11 +13,7 @@ func init() {
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error { func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
if _, err := extractor.GetAlerts(); err != nil { return extractor.ValidateAlerts()
return err
}
return nil
} }
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error { func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
@@ -29,15 +25,12 @@ func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
if alerts, err := extractor.GetAlerts(); err != nil { alerts, err := extractor.GetAlerts()
if err != nil {
return err return err
} else { }
saveAlerts.Alerts = alerts saveAlerts.Alerts = alerts
}
if err := bus.Dispatch(&saveAlerts); err != nil { return bus.Dispatch(&saveAlerts)
return err
}
return nil
} }

View File

@@ -11,76 +11,78 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )
// DashAlertExtractor extracts alerts from the dashboard json
type DashAlertExtractor struct { type DashAlertExtractor struct {
Dash *m.Dashboard Dash *m.Dashboard
OrgId int64 OrgID int64
log log.Logger log log.Logger
} }
func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor { // NewDashAlertExtractor returns a new DashAlertExtractor
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
return &DashAlertExtractor{ return &DashAlertExtractor{
Dash: dash, Dash: dash,
OrgId: orgId, OrgID: orgID,
log: log.New("alerting.extractor"), log: log.New("alerting.extractor"),
} }
} }
func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (*m.DataSource, error) { func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*m.DataSource, error) {
if dsName == "" { if dsName == "" {
query := &m.GetDataSourcesQuery{OrgId: e.OrgId} query := &m.GetDataSourcesQuery{OrgId: e.OrgID}
if err := bus.Dispatch(query); err != nil { if err := bus.Dispatch(query); err != nil {
return nil, err return nil, err
} else { }
for _, ds := range query.Result { for _, ds := range query.Result {
if ds.IsDefault { if ds.IsDefault {
return ds, nil return ds, nil
} }
} }
}
} else { } else {
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId} query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgID}
if err := bus.Dispatch(query); err != nil { if err := bus.Dispatch(query); err != nil {
return nil, err return nil, err
} else {
return query.Result, nil
} }
return query.Result, nil
} }
return nil, errors.New("Could not find datasource id for " + dsName) return nil, errors.New("Could not find datasource id for " + dsName)
} }
func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Json { func findPanelQueryByRefID(panel *simplejson.Json, refID string) *simplejson.Json {
for _, targetsObj := range panel.Get("targets").MustArray() { for _, targetsObj := range panel.Get("targets").MustArray() {
target := simplejson.NewFromAny(targetsObj) target := simplejson.NewFromAny(targetsObj)
if target.Get("refId").MustString() == refId { if target.Get("refId").MustString() == refID {
return target return target
} }
} }
return nil return nil
} }
func copyJson(in *simplejson.Json) (*simplejson.Json, error) { func copyJSON(in *simplejson.Json) (*simplejson.Json, error) {
rawJson, err := in.MarshalJSON() rawJSON, err := in.MarshalJSON()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return simplejson.NewJson(rawJson) return simplejson.NewJson(rawJSON)
} }
func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) { func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, validateAlertFunc func(*m.Alert) bool) ([]*m.Alert, error) {
alerts := make([]*m.Alert, 0) alerts := make([]*m.Alert, 0)
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() { for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
panel := simplejson.NewFromAny(panelObj) panel := simplejson.NewFromAny(panelObj)
collapsedJson, collapsed := panel.CheckGet("collapsed") collapsedJSON, collapsed := panel.CheckGet("collapsed")
// check if the panel is collapsed // check if the panel is collapsed
if collapsed && collapsedJson.MustBool() { if collapsed && collapsedJSON.MustBool() {
// extract alerts from sub panels for collapsed panels // extract alerts from sub panels for collapsed panels
als, err := e.GetAlertFromPanels(panel) als, err := e.getAlertFromPanels(panel, validateAlertFunc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -95,7 +97,7 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
continue continue
} }
panelId, err := panel.Get("id").Int64() panelID, err := panel.Get("id").Int64()
if err != nil { if err != nil {
return nil, fmt.Errorf("panel id is required. err %v", err) return nil, fmt.Errorf("panel id is required. err %v", err)
} }
@@ -113,8 +115,8 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
alert := &m.Alert{ alert := &m.Alert{
DashboardId: e.Dash.Id, DashboardId: e.Dash.Id,
OrgId: e.OrgId, OrgId: e.OrgID,
PanelId: panelId, PanelId: panelID,
Id: jsonAlert.Get("id").MustInt64(), Id: jsonAlert.Get("id").MustInt64(),
Name: jsonAlert.Get("name").MustString(), Name: jsonAlert.Get("name").MustString(),
Handler: jsonAlert.Get("handler").MustInt64(), Handler: jsonAlert.Get("handler").MustInt64(),
@@ -126,11 +128,11 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
jsonCondition := simplejson.NewFromAny(condition) jsonCondition := simplejson.NewFromAny(condition)
jsonQuery := jsonCondition.Get("query") jsonQuery := jsonCondition.Get("query")
queryRefId := jsonQuery.Get("params").MustArray()[0].(string) queryRefID := jsonQuery.Get("params").MustArray()[0].(string)
panelQuery := findPanelQueryByRefId(panel, queryRefId) panelQuery := findPanelQueryByRefID(panel, queryRefID)
if panelQuery == nil { if panelQuery == nil {
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId) reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefID)
return nil, ValidationError{Reason: reason} return nil, ValidationError{Reason: reason}
} }
@@ -141,12 +143,13 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
dsName = panel.Get("datasource").MustString() dsName = panel.Get("datasource").MustString()
} }
if datasource, err := e.lookupDatasourceId(dsName); err != nil { datasource, err := e.lookupDatasourceID(dsName)
if err != nil {
return nil, err return nil, err
} else {
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
} }
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
if interval, err := panel.Get("interval").String(); err == nil { if interval, err := panel.Get("interval").String(); err == nil {
panelQuery.Set("interval", interval) panelQuery.Set("interval", interval)
} }
@@ -162,21 +165,28 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
return nil, err return nil, err
} }
if alert.ValidToSave() { if !validateAlertFunc(alert) {
alerts = append(alerts, alert)
} else {
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId) e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
return nil, m.ErrDashboardContainsInvalidAlertData return nil, m.ErrDashboardContainsInvalidAlertData
} }
alerts = append(alerts, alert)
} }
return alerts, nil return alerts, nil
} }
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { func validateAlertRule(alert *m.Alert) bool {
e.log.Debug("GetAlerts") return alert.ValidToSave()
}
dashboardJson, err := copyJson(e.Dash.Data) // 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 { if err != nil {
return nil, err return nil, err
} }
@@ -185,11 +195,11 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
// We extract alerts from rows to be backwards compatible // We extract alerts from rows to be backwards compatible
// with the old dashboard json model. // with the old dashboard json model.
rows := dashboardJson.Get("rows").MustArray() rows := dashboardJSON.Get("rows").MustArray()
if len(rows) > 0 { if len(rows) > 0 {
for _, rowObj := range rows { for _, rowObj := range rows {
row := simplejson.NewFromAny(rowObj) row := simplejson.NewFromAny(rowObj)
a, err := e.GetAlertFromPanels(row) a, err := e.getAlertFromPanels(row, validateFunc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -197,7 +207,7 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
alerts = append(alerts, a...) alerts = append(alerts, a...)
} }
} else { } else {
a, err := e.GetAlertFromPanels(dashboardJson) a, err := e.getAlertFromPanels(dashboardJSON, validateFunc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -208,3 +218,10 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts)) e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
return alerts, nil 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
}

View File

@@ -240,5 +240,26 @@ func TestAlertRuleExtraction(t *testing.T) {
So(len(alerts), ShouldEqual, 4) So(len(alerts), ShouldEqual, 4)
}) })
}) })
Convey("Parse and validate dashboard without id and containing an alert", func() {
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
So(err, ShouldBeNil)
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1)
err = extractor.ValidateAlerts()
Convey("Should validate without error", func() {
So(err, ShouldBeNil)
})
Convey("Should fail on save", func() {
_, err := extractor.GetAlerts()
So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData)
})
})
}) })
} }

View File

@@ -0,0 +1,281 @@
{
"title": "Influxdb",
"tags": [
"apa"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [
{
"collapse": false,
"editable": true,
"height": "450px",
"panels": [
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
10
],
"type": "gt"
},
"query": {
"params": [
"B",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"frequency": "3s",
"handler": 1,
"name": "Influxdb",
"noDataState": "no_data",
"notifications": [
{
"id": 6
}
]
},
"alerting": {},
"aliasColors": {
"logins.count.count": "#890F02"
},
"bars": false,
"datasource": "InfluxDB",
"editable": true,
"error": false,
"fill": 1,
"grid": {},
"id": 1,
"interval": ">10s",
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"datacenter"
],
"type": "tag"
},
{
"params": [
"none"
],
"type": "fill"
}
],
"hide": false,
"measurement": "logins.count",
"policy": "default",
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
"rawQuery": true,
"refId": "B",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "count"
}
]
],
"tags": []
},
{
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"hide": true,
"measurement": "cpu",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
],
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "sum"
}
]
],
"tags": []
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 10
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"msResolution": false,
"ordering": "alphabetical",
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"editable": true,
"error": false,
"id": 2,
"isNew": true,
"limit": 10,
"links": [],
"show": "current",
"span": 2,
"stateFilter": [
"alerting"
],
"title": "Alert status",
"type": "alertlist"
}
],
"title": "Row"
}
],
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"now": true,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"schemaVersion": 13,
"version": 120,
"links": [],
"gnetId": null
}