feat(alerting): progress on alerting UI and model, refactoring of dashboard parser and tests into extractor component, moved tests from sqlstore to alerting package

This commit is contained in:
Torkel Ödegaard
2016-06-11 10:13:33 +02:00
parent 1fa9ae810b
commit 2b4a9954b1
13 changed files with 441 additions and 270 deletions

View File

@@ -0,0 +1,76 @@
package alerting
import (
"fmt"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
)
type AlertRule struct {
Id int64
OrgId int64
DashboardId int64
PanelId int64
Frequency int64
Name string
Description string
State string
Warning Level
Critical Level
Query AlertQuery
Transform string
TransformParams simplejson.Json
Transformer Transformer
}
func NewAlertRuleFromDBModel(ruleDef *m.AlertRuleModel) (*AlertRule, error) {
model := &AlertRule{}
model.Id = ruleDef.Id
model.OrgId = ruleDef.OrgId
model.Name = ruleDef.Name
model.Description = ruleDef.Description
model.State = ruleDef.State
critical := ruleDef.Expression.Get("critical")
model.Critical = Level{
Operator: critical.Get("op").MustString(),
Level: critical.Get("level").MustFloat64(),
}
warning := ruleDef.Expression.Get("warning")
model.Warning = Level{
Operator: warning.Get("op").MustString(),
Level: warning.Get("level").MustFloat64(),
}
model.Frequency = ruleDef.Expression.Get("frequency").MustInt64()
model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
model.TransformParams = *ruleDef.Expression.Get("transform")
if model.Transform == "aggregation" {
model.Transformer = &AggregationTransformer{
Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
}
}
query := ruleDef.Expression.Get("query")
model.Query = AlertQuery{
Query: query.Get("query").MustString(),
DatasourceId: query.Get("datasourceId").MustInt64(),
From: query.Get("from").MustString(),
To: query.Get("to").MustString(),
Aggregator: query.Get("agg").MustString(),
}
if model.Query.Query == "" {
return nil, fmt.Errorf("missing query.query")
}
if model.Query.DatasourceId == 0 {
return nil, fmt.Errorf("missing query.datasourceId")
}
return model, nil
}

View File

@@ -0,0 +1,89 @@
package alerting
import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *m.Dashboard
}
func init() {
bus.AddHandler("alerting", updateDashboardAlerts)
}
func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
saveRulesCmd := m.SaveAlertsCommand{
OrgId: cmd.OrgId,
UserId: cmd.UserId,
}
extractor := NewAlertRuleExtractor(cmd.Dashboard, cmd.OrgId)
rules, err := extractor.GetRuleModels()
if err != nil {
return err
}
saveRulesCmd.Alerts = rules
if bus.Dispatch(&saveRulesCmd); err != nil {
return err
}
return nil
}
func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) {
model := &AlertRule{}
model.Id = ruleDef.Id
model.OrgId = ruleDef.OrgId
model.Name = ruleDef.Name
model.Description = ruleDef.Description
model.State = ruleDef.State
critical := ruleDef.Expression.Get("critical")
model.Critical = Level{
Operator: critical.Get("op").MustString(),
Level: critical.Get("level").MustFloat64(),
}
warning := ruleDef.Expression.Get("warning")
model.Warning = Level{
Operator: warning.Get("op").MustString(),
Level: warning.Get("level").MustFloat64(),
}
model.Frequency = ruleDef.Expression.Get("frequency").MustInt64()
model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
model.TransformParams = *ruleDef.Expression.Get("transform")
if model.Transform == "aggregation" {
model.Transformer = &AggregationTransformer{
Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
}
}
query := ruleDef.Expression.Get("query")
model.Query = AlertQuery{
Query: query.Get("query").MustString(),
DatasourceId: query.Get("datasourceId").MustInt64(),
From: query.Get("from").MustString(),
To: query.Get("to").MustString(),
Aggregator: query.Get("agg").MustString(),
}
if model.Query.Query == "" {
return nil, fmt.Errorf("missing query.query")
}
if model.Query.DatasourceId == 0 {
return nil, fmt.Errorf("missing query.datasourceId")
}
return model, nil
}

View File

@@ -1,135 +0,0 @@
package alerting
import (
"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"
)
func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleModel {
alerts := make([]*m.AlertRuleModel, 0)
for _, rowObj := range cmd.Dashboard.Get("rows").MustArray() {
row := simplejson.NewFromAny(rowObj)
for _, panelObj := range row.Get("panels").MustArray() {
panel := simplejson.NewFromAny(panelObj)
alerting := panel.Get("alerting")
alert := &m.AlertRuleModel{
DashboardId: cmd.Result.Id,
OrgId: cmd.Result.OrgId,
PanelId: panel.Get("id").MustInt64(),
Id: alerting.Get("id").MustInt64(),
Name: alerting.Get("name").MustString(),
Description: alerting.Get("description").MustString(),
}
log.Info("Alertrule: %v", alert.Name)
valueQuery := alerting.Get("query")
valueQueryRef := valueQuery.Get("refId").MustString()
for _, targetsObj := range panel.Get("targets").MustArray() {
target := simplejson.NewFromAny(targetsObj)
if target.Get("refId").MustString() == valueQueryRef {
datsourceName := ""
if target.Get("datasource").MustString() != "" {
datsourceName = target.Get("datasource").MustString()
} else if panel.Get("datasource").MustString() != "" {
datsourceName = panel.Get("datasource").MustString()
}
if datsourceName == "" {
query := &m.GetDataSourcesQuery{OrgId: cmd.OrgId}
if err := bus.Dispatch(query); err == nil {
for _, ds := range query.Result {
if ds.IsDefault {
alerting.SetPath([]string{"query", "datasourceId"}, ds.Id)
}
}
}
} else {
query := &m.GetDataSourceByNameQuery{
Name: panel.Get("datasource").MustString(),
OrgId: cmd.OrgId,
}
bus.Dispatch(query)
alerting.SetPath([]string{"query", "datasourceId"}, query.Result.Id)
}
targetQuery := target.Get("target").MustString()
if targetQuery != "" {
alerting.SetPath([]string{"query", "query"}, targetQuery)
}
}
}
alert.Expression = alerting
_, err := ConvetAlertModelToAlertRule(alert)
if err == nil && alert.ValidToSave() {
alerts = append(alerts, alert)
} else {
log.Error2("Failed to parse model from expression", "error", err)
}
}
}
return alerts
}
func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) {
model := &AlertRule{}
model.Id = ruleDef.Id
model.OrgId = ruleDef.OrgId
model.Name = ruleDef.Name
model.Description = ruleDef.Description
model.State = ruleDef.State
critical := ruleDef.Expression.Get("critical")
model.Critical = Level{
Operator: critical.Get("op").MustString(),
Level: critical.Get("level").MustFloat64(),
}
warning := ruleDef.Expression.Get("warning")
model.Warning = Level{
Operator: warning.Get("op").MustString(),
Level: warning.Get("level").MustFloat64(),
}
model.Frequency = ruleDef.Expression.Get("frequency").MustInt64()
model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
model.TransformParams = *ruleDef.Expression.Get("transform")
if model.Transform == "aggregation" {
model.Transformer = &AggregationTransformer{
Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
}
}
query := ruleDef.Expression.Get("query")
model.Query = AlertQuery{
Query: query.Get("query").MustString(),
DatasourceId: query.Get("datasourceId").MustInt64(),
From: query.Get("from").MustString(),
To: query.Get("to").MustString(),
Aggregator: query.Get("agg").MustString(),
}
if model.Query.Query == "" {
return nil, fmt.Errorf("missing query.query")
}
if model.Query.DatasourceId == 0 {
return nil, fmt.Errorf("missing query.datasourceId")
}
return model, nil
}

View File

@@ -0,0 +1,120 @@
package alerting
import (
"errors"
"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"
)
type AlertRuleExtractor struct {
Dash *m.Dashboard
OrgId int64
log log.Logger
}
func NewAlertRuleExtractor(dash *m.Dashboard, orgId int64) *AlertRuleExtractor {
return &AlertRuleExtractor{
Dash: dash,
OrgId: orgId,
log: log.New("alerting.extractor"),
}
}
func (e *AlertRuleExtractor) lookupDatasourceId(dsName string) (int64, error) {
if dsName == "" {
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
if err := bus.Dispatch(query); err != nil {
return 0, err
} else {
for _, ds := range query.Result {
if ds.IsDefault {
return ds.Id, nil
}
}
}
} else {
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
if err := bus.Dispatch(query); err != nil {
return 0, err
} else {
return query.Result.Id, nil
}
}
return 0, errors.New("Could not find datasource id for " + dsName)
}
func (e *AlertRuleExtractor) GetRuleModels() (m.AlertRules, error) {
rules := make(m.AlertRules, 0)
for _, rowObj := range e.Dash.Data.Get("rows").MustArray() {
row := simplejson.NewFromAny(rowObj)
for _, panelObj := range row.Get("panels").MustArray() {
panel := simplejson.NewFromAny(panelObj)
jsonRule := panel.Get("alerting")
// check if marked for deletion
deleted := jsonRule.Get("deleted").MustBool()
if deleted {
e.log.Info("Deleted alert rule found")
continue
}
ruleModel := &m.AlertRuleModel{
DashboardId: e.Dash.Id,
OrgId: e.OrgId,
PanelId: panel.Get("id").MustInt64(),
Id: jsonRule.Get("id").MustInt64(),
Name: jsonRule.Get("name").MustString(),
Scheduler: jsonRule.Get("scheduler").MustInt64(),
Enabled: jsonRule.Get("enabled").MustBool(),
Description: jsonRule.Get("description").MustString(),
}
valueQuery := jsonRule.Get("query")
valueQueryRef := valueQuery.Get("refId").MustString()
for _, targetsObj := range panel.Get("targets").MustArray() {
target := simplejson.NewFromAny(targetsObj)
if target.Get("refId").MustString() == valueQueryRef {
dsName := ""
if target.Get("datasource").MustString() != "" {
dsName = target.Get("datasource").MustString()
} else if panel.Get("datasource").MustString() != "" {
dsName = panel.Get("datasource").MustString()
}
if datasourceId, err := e.lookupDatasourceId(dsName); err != nil {
return nil, err
} else {
valueQuery.SetPath([]string{"datasourceId"}, datasourceId)
}
targetQuery := target.Get("target").MustString()
if targetQuery != "" {
jsonRule.SetPath([]string{"query", "query"}, targetQuery)
}
}
}
ruleModel.Expression = jsonRule
// validate
_, err := NewAlertRuleFromDBModel(ruleModel)
if err == nil && ruleModel.ValidToSave() {
rules = append(rules, ruleModel)
} else {
e.log.Error("Failed to extract alert rules from dashboard", "error", err)
return nil, errors.New("Failed to extract alert rules from dashboard")
}
}
}
return rules, nil
}

View File

@@ -0,0 +1,222 @@
package alerting
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertRuleExtraction(t *testing.T) {
Convey("Parsing alert rules from dashboard json", t, func() {
Convey("Parsing and validating alerts from dashboards", func() {
json := `{
"id": 57,
"title": "Graphite 4",
"originalTitle": "Graphite 4",
"tags": [
"graphite"
],
"rows": [
{
"panels": [
{
"title": "Active desktop users",
"editable": true,
"type": "graph",
"id": 3,
"targets": [
{
"refId": "A",
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
}
],
"datasource": null,
"alerting": {
"name": "name1",
"description": "desc1",
"scheduler": 1,
"enabled": true,
"critical": {
"level": 20,
"op": ">"
},
"frequency": "60s",
"query": {
"from": "5m",
"refId": "A",
"to": "now"
},
"transform": {
"method": "avg",
"type": "aggregation"
},
"warning": {
"level": 10,
"op": ">"
}
}
},
{
"title": "Active mobile users",
"id": 4,
"targets": [
{
"refId": "A",
"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"
}
],
"datasource": "graphite2",
"alerting": {
"name": "name2",
"description": "desc2",
"scheduler": 0,
"enabled": true,
"critical": {
"level": 20,
"op": ">"
},
"frequency": "60s",
"query": {
"from": "5m",
"refId": "A",
"to": "now"
},
"transform": {
"method": "avg",
"name": "aggregation"
},
"warning": {
"level": 10,
"op": ">"
}
}
}
],
"title": "Row"
},
{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [
{
"datasource": "InfluxDB",
"id": 2,
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "cpu",
"policy": "default",
"query": "SELECT mean(\"value\") FROM \"cpu\" WHERE $timeFilter GROUP BY time($interval) fill(null)",
"refId": "A",
"resultFormat": "table",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": [],
"target": ""
}
],
"title": "Broken influxdb panel",
"transform": "table",
"type": "table",
"alerting": {
"deleted": true
}
}
],
"title": "New row"
}
]
}`
dashJson, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewAlertRuleExtractor(dash, 1)
// mock data
defaultDs := &m.DataSource{Id: 12, OrgId: 2, Name: "I am default", IsDefault: true}
graphite2Ds := &m.DataSource{Id: 15, OrgId: 2, Name: "graphite2"}
bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
return nil
})
bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error {
if query.Name == defaultDs.Name {
query.Result = defaultDs
}
if query.Name == graphite2Ds.Name {
query.Result = graphite2Ds
}
return nil
})
alerts, err := extractor.GetRuleModels()
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
Convey("all properties have been set", func() {
So(len(alerts), ShouldEqual, 2)
for _, v := range alerts {
So(v.DashboardId, ShouldEqual, 57)
So(v.Name, ShouldNotBeEmpty)
So(v.Description, ShouldNotBeEmpty)
}
Convey("should extract scheduler property", func() {
So(alerts[0].Scheduler, ShouldEqual, 1)
So(alerts[1].Scheduler, ShouldEqual, 0)
})
Convey("should extract panel idc", func() {
So(alerts[0].PanelId, ShouldEqual, 3)
So(alerts[1].PanelId, ShouldEqual, 4)
})
Convey("should extract name and desc", func() {
So(alerts[0].Name, ShouldEqual, "name1")
So(alerts[0].Description, ShouldEqual, "desc1")
So(alerts[1].Name, ShouldEqual, "name2")
So(alerts[1].Description, ShouldEqual, "desc2")
})
})
})
})
}

View File

@@ -1,9 +1,5 @@
package alerting
import (
"github.com/grafana/grafana/pkg/components/simplejson"
)
type AlertJob struct {
Offset int64
Delay bool
@@ -21,23 +17,6 @@ type AlertResult struct {
AlertJob *AlertJob
}
type AlertRule struct {
Id int64
OrgId int64
DashboardId int64
PanelId int64
Frequency int64
Name string
Description string
State string
Warning Level
Critical Level
Query AlertQuery
Transform string
TransformParams simplejson.Json
Transformer Transformer
}
type Level struct {
Operator string
Level float64