From 29d308e4479945c252d0b96ff1bb00863bb681f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Jul 2016 11:24:27 +0200 Subject: [PATCH 01/22] feat(alerting): refactoring alert model to use conditions concept --- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 103 +++++----- .../panel/graph/partials/tab_alerting.html | 186 ++++++++---------- 2 files changed, 137 insertions(+), 152 deletions(-) diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 2578f17ffca..23d57638856 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -19,27 +19,23 @@ var alertQueryDef = new QueryPartDef({ defaultParams: ['#A', '5m', 'now', 'avg'] }); +var reducerAvgDef = new QueryPartDef({ + type: 'avg', + params: [], + defaultParams: [] +}); + export class AlertTabCtrl { panel: any; panelCtrl: any; metricTargets; handlers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}]; - transforms = [ - { - text: 'Aggregation', - type: 'aggregation', - }, - { - text: 'Linear Forecast', - type: 'forecast', - }, + conditionTypes = [ + {text: 'Query', value: 'query'}, + {text: 'Alert state', value: 'alert_state'}, ]; - aggregators = ['avg', 'sum', 'min', 'max', 'last']; alert: any; - thresholds: any; - query: any; - queryParams: any; - transformDef: any; + conditionModels: any; levelOpList = [ {text: '>', value: '>'}, {text: '<', value: '<'}, @@ -76,14 +72,10 @@ export class AlertTabCtrl { alert.warn = this.getThresholdWithDefaults(alert.warn); alert.crit = this.getThresholdWithDefaults(alert.crit); - alert.query = alert.query || {}; - alert.query.refId = alert.query.refId || 'A'; - alert.query.from = alert.query.from || '5m'; - alert.query.to = alert.query.to || 'now'; - - alert.transform = alert.transform || {}; - alert.transform.type = alert.transform.type || 'aggregation'; - alert.transform.method = alert.transform.method || 'avg'; + alert.conditions = alert.conditions || []; + if (alert.conditions.length === 0) { + alert.conditions.push(this.buildDefaultCondition()); + } alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; @@ -93,42 +85,55 @@ export class AlertTabCtrl { alert.name = alert.name || defaultName; alert.description = alert.description || defaultName; - // great temp working model - this.queryParams = { - params: [alert.query.refId, alert.query.from, alert.query.to] - }; - - // init the query part components model - this.query = new QueryPart(this.queryParams, alertQueryDef); - this.transformDef = _.findWhere(this.transforms, {type: alert.transform.type}); + this.conditionModels = _.reduce(alert.conditions, (memo, value) => { + memo.push(this.buildConditionModel(value)); + return memo; + }, []); this.panelCtrl.editingAlert = true; this.panelCtrl.render(); } - queryUpdated() { - this.alert.query = { - refId: this.query.params[0], - from: this.query.params[1], - to: this.query.params[2], + buildDefaultCondition() { + return { + type: 'query', + refId: 'A', + from: '5m', + to: 'now', + reducer: 'avg', + reducerParams: [], }; } - transformChanged() { - // clear model - this.alert.transform = {type: this.alert.transform.type}; - this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type}); + buildConditionModel(source) { + var cm: any = {source: source, type: source.type}; - switch (this.alert.transform.type) { - case 'aggregation': { - this.alert.transform.method = 'avg'; - break; - } - case "forecast": { - this.alert.transform.timespan = '7d'; - break; - } - } + var queryPartModel = { + params: [source.refId, source.from, source.to] + }; + + cm.queryPart = new QueryPart(queryPartModel, alertQueryDef); + cm.reducerPart = new QueryPart({params: []}, reducerAvgDef); + return cm; + } + + queryPartUpdated(conditionModel) { + conditionModel.source.refId = conditionModel.queryPart.params[0]; + conditionModel.source.from = conditionModel.queryPart.params[1]; + conditionModel.source.to = conditionModel.queryPart.params[2]; + } + + addCondition(type) { + var condition = this.buildDefaultCondition(); + // add to persited model + this.alert.conditions.push(condition); + // add to view model + this.conditionModels.push(this.buildConditionModel(condition)); + } + + removeCondition(index) { + this.alert.conditions.splice(index, 1); + this.conditionModels.splice(index, 1); } delete() { diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 4b76648a845..8e0d1c565af 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -24,114 +24,94 @@
-
-
-
Alert Query
-
-
- - -
-
- Transform using -
- -
-
-
- Method -
- -
-
-
- Timespan - -
-
-
- -
-
Thresholds
-
-
- - - Critcal if - - - -
-
- - - Warn if - - - -
-
-
-
- -
-
-
Execution
-
-
- Handler -
- -
-
-
- Evaluate every - -
-
-
-
-
Notifications
-
-
- Groups - - -
-
-
-
- -
-
Information
-
- Alert name - -
+
+
Alert Rule
- Alert description + Name +
- + Handler +
+ +
+
+ Evaluate every + +
+
+
+ Notifications + + +
+
+ +
+
Conditions
+
+
+ {{$index+1}} +
+
+ + +
+
+ Reduce + + +
+
+ + + Critcal if + + + +
+
+ + + Warn if + + + + + +
+
+
+ +
+
From f38c954639f8c8acd47d3011fb788a40419e6f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Jul 2016 11:54:41 +0200 Subject: [PATCH 02/22] feat(alerting): more work on alert conditions --- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 10 +-- .../panel/graph/partials/tab_alerting.html | 77 ++++++++++--------- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 23d57638856..5779fff73c6 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -32,7 +32,9 @@ export class AlertTabCtrl { handlers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}]; conditionTypes = [ {text: 'Query', value: 'query'}, - {text: 'Alert state', value: 'alert_state'}, + {text: 'Other alert', value: 'other_alert'}, + {text: 'Time of day', value: 'time_of_day'}, + {text: 'Day of week', value: 'day_of_week'}, ]; alert: any; conditionModels: any; @@ -68,10 +70,6 @@ export class AlertTabCtrl { initModel() { var alert = this.alert = this.panel.alert = this.panel.alert || {}; - // set threshold defaults - alert.warn = this.getThresholdWithDefaults(alert.warn); - alert.crit = this.getThresholdWithDefaults(alert.crit); - alert.conditions = alert.conditions || []; if (alert.conditions.length === 0) { alert.conditions.push(this.buildDefaultCondition()); @@ -102,6 +100,8 @@ export class AlertTabCtrl { to: 'now', reducer: 'avg', reducerParams: [], + warn: this.getThresholdWithDefaults({}), + crit: this.getThresholdWithDefaults({}), }; } diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 8e0d1c565af..6d07173fe24 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,27 +1,27 @@ -
-
-
Visual Thresholds
-
-
- - - Critcal if - - - -
-
- - - Warn if - - - -
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + +
@@ -69,7 +69,7 @@
- Reduce + Reducer
-
-
+
- - From 20fcffb71eed9fb959f05e025fd52666e8c5607e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Jul 2016 16:15:26 +0200 Subject: [PATCH 03/22] feat(alerting): working on alerting conditions model --- pkg/services/alerting/alert_rule.go | 79 ++++++++----------- pkg/services/alerting/alert_rule_test.go | 39 ++++----- pkg/services/alerting/conditions.go | 32 ++++++++ pkg/services/alerting/result_handler_test.go | 8 +- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 43 ++++------ .../panel/graph/partials/tab_alerting.html | 55 ++++++------- 6 files changed, 125 insertions(+), 131 deletions(-) create mode 100644 pkg/services/alerting/conditions.go diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index cc5968eeb70..8a448fb885f 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -4,7 +4,6 @@ import ( "fmt" "regexp" "strconv" - "strings" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/alerting/transformers" @@ -31,6 +30,19 @@ type AlertRule struct { NotificationGroups []int64 } +type AlertRule2 struct { + Id int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Frequency int64 + Name string + Description string + State string + Conditions []AlertCondition + Notifications []int64 +} + var ( ValueFormatRegex = regexp.MustCompile("^\\d+") UnitFormatRegex = regexp.MustCompile("\\w{1}$") @@ -56,7 +68,11 @@ func getTimeDurationStringToSeconds(str string) int64 { } func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { - model := &AlertRule{} + return nil, nil +} + +func NewAlertRuleFromDBModel2(ruleDef *m.Alert) (*AlertRule2, error) { + model := &AlertRule2{} model.Id = ruleDef.Id model.OrgId = ruleDef.OrgId model.Name = ruleDef.Name @@ -64,55 +80,26 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { model.State = ruleDef.State model.Frequency = ruleDef.Frequency - ngs := ruleDef.Settings.Get("notificationGroups").MustString() - var ids []int64 - for _, v := range strings.Split(ngs, ",") { - id, err := strconv.Atoi(v) - if err == nil { - ids = append(ids, int64(id)) + for _, v := range ruleDef.Settings.Get("notifications").MustArray() { + if id, ok := v.(int64); ok { + model.Notifications = append(model.Notifications, int64(id)) } } - model.NotificationGroups = ids - - critical := ruleDef.Settings.Get("crit") - model.Critical = Level{ - Operator: critical.Get("op").MustString(), - Value: critical.Get("value").MustFloat64(), + for _, condition := range ruleDef.Settings.Get("conditions").MustArray() { + conditionModel := simplejson.NewFromAny(condition) + switch conditionModel.Get("type").MustString() { + case "query": + queryCondition, err := NewQueryCondition(conditionModel) + if err != nil { + return nil, err + } + model.Conditions = append(model.Conditions, queryCondition) + } } - warning := ruleDef.Settings.Get("warn") - model.Warning = Level{ - Operator: warning.Get("op").MustString(), - Value: warning.Get("value").MustFloat64(), - } - - model.Transform = ruleDef.Settings.Get("transform").Get("type").MustString() - if model.Transform == "" { - return nil, fmt.Errorf("missing transform") - } - - model.TransformParams = *ruleDef.Settings.Get("transform") - - if model.Transform == "aggregation" { - method := ruleDef.Settings.Get("transform").Get("method").MustString() - model.Transformer = transformers.NewAggregationTransformer(method) - } - - query := ruleDef.Settings.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(), - } - - if model.Query.Query == "" { - return nil, fmt.Errorf("missing query.query") - } - - if model.Query.DatasourceId == 0 { - return nil, fmt.Errorf("missing query.datasourceId") + if len(model.Conditions) == 0 { + return nil, fmt.Errorf("Alert is missing conditions") } return model, nil diff --git a/pkg/services/alerting/alert_rule_test.go b/pkg/services/alerting/alert_rule_test.go index 8050dd46aa9..9b75105579d 100644 --- a/pkg/services/alerting/alert_rule_test.go +++ b/pkg/services/alerting/alert_rule_test.go @@ -38,26 +38,19 @@ func TestAlertRuleModel(t *testing.T) { "description": "desc2", "handler": 0, "enabled": true, - "crit": { - "value": 20, - "op": ">" - }, - "warn": { - "value": 10, - "op": ">" - }, "frequency": "60s", - "query": { - "from": "5m", - "refId": "A", - "to": "now", - "query": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)", - "datasourceId": 1 - }, - "transform": { - "type": "avg", - "name": "aggregation" - } + "conditions": [ + { + "type": "query", + "query": { + "params": ["A", "5m", "now"], + "datasourceId": 1, + "query": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)" + }, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] } ` @@ -72,15 +65,11 @@ func TestAlertRuleModel(t *testing.T) { Settings: alertJSON, } - alertRule, err := NewAlertRuleFromDBModel(alert) + alertRule, err := NewAlertRuleFromDBModel2(alert) So(err, ShouldBeNil) - So(alertRule.Warning.Operator, ShouldEqual, ">") - So(alertRule.Warning.Value, ShouldEqual, 10) - - So(alertRule.Critical.Operator, ShouldEqual, ">") - So(alertRule.Critical.Value, ShouldEqual, 20) + So(alertRule.Conditions, ShouldHaveLength, 1) }) }) } diff --git a/pkg/services/alerting/conditions.go b/pkg/services/alerting/conditions.go new file mode 100644 index 00000000000..5fea9c81bd6 --- /dev/null +++ b/pkg/services/alerting/conditions.go @@ -0,0 +1,32 @@ +package alerting + +import "github.com/grafana/grafana/pkg/components/simplejson" + +type AlertCondition interface { + Eval() +} + +type QueryCondition struct { + Query AlertQuery + Reducer AlertReducerModel + Evaluator AlertEvaluatorModel +} + +func (c *QueryCondition) Eval() { +} + +type AlertReducerModel struct { + Type string + Params []interface{} +} + +type AlertEvaluatorModel struct { + Type string + Params []interface{} +} + +func NewQueryCondition(model *simplejson.Json) (*QueryCondition, error) { + condition := QueryCondition{} + + return &condition, nil +} diff --git a/pkg/services/alerting/result_handler_test.go b/pkg/services/alerting/result_handler_test.go index f44049ecb6d..99879e52202 100644 --- a/pkg/services/alerting/result_handler_test.go +++ b/pkg/services/alerting/result_handler_test.go @@ -38,7 +38,7 @@ func TestAlertResultHandler(t *testing.T) { Convey("alert state have changed", func() { mockAlertState = &m.AlertState{ - NewState: alertstates.Critical, + State: alertstates.Critical, } mockResult.State = alertstates.Ok So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) @@ -47,11 +47,11 @@ func TestAlertResultHandler(t *testing.T) { Convey("last alert state was 15min ago", func() { now := time.Now() mockAlertState = &m.AlertState{ - NewState: alertstates.Critical, - Created: now.Add(time.Minute * -30), + State: alertstates.Critical, + Created: now.Add(time.Minute * -30), } mockResult.State = alertstates.Critical - mockResult.ExeuctionTime = time.Now() + mockResult.StartTime = time.Now() So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) }) }) diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 5779fff73c6..211e08675dd 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -38,11 +38,15 @@ export class AlertTabCtrl { ]; alert: any; conditionModels: any; - levelOpList = [ + evalFunctions = [ {text: '>', value: '>'}, {text: '<', value: '<'}, - {text: '=', value: '='}, ]; + severityLevels = [ + {text: 'Critical', value: 'critical'}, + {text: 'Warning', value: 'warning'}, + ]; + /** @ngInject */ constructor($scope, private $timeout) { @@ -60,21 +64,15 @@ export class AlertTabCtrl { }); } - getThresholdWithDefaults(threshold) { - threshold = threshold || {}; - threshold.op = threshold.op || '>'; - threshold.value = threshold.value || undefined; - return threshold; - } - initModel() { var alert = this.alert = this.panel.alert = this.panel.alert || {}; - alert.conditions = alert.conditions || []; + alert.conditions = []; if (alert.conditions.length === 0) { alert.conditions.push(this.buildDefaultCondition()); } + alert.severity = alert.severity || 'critical'; alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; @@ -95,32 +93,23 @@ export class AlertTabCtrl { buildDefaultCondition() { return { type: 'query', - refId: 'A', - from: '5m', - to: 'now', - reducer: 'avg', - reducerParams: [], - warn: this.getThresholdWithDefaults({}), - crit: this.getThresholdWithDefaults({}), + query: {params: ['A', '5m', 'now']}, + reducer: {type: 'avg', params: []}, + evaluator: {type: '>', params: [null]}, }; } buildConditionModel(source) { var cm: any = {source: source, type: source.type}; - var queryPartModel = { - params: [source.refId, source.from, source.to] - }; - - cm.queryPart = new QueryPart(queryPartModel, alertQueryDef); + cm.queryPart = new QueryPart(source.query, alertQueryDef); cm.reducerPart = new QueryPart({params: []}, reducerAvgDef); + cm.evaluator = source.evaluator; + return cm; } queryPartUpdated(conditionModel) { - conditionModel.source.refId = conditionModel.queryPart.params[0]; - conditionModel.source.from = conditionModel.queryPart.params[1]; - conditionModel.source.to = conditionModel.queryPart.params[2]; } addCondition(type) { @@ -138,10 +127,6 @@ export class AlertTabCtrl { delete() { this.alert.enabled = false; - this.alert.warn.value = undefined; - this.alert.crit.value = undefined; - - // reset model but keep thresholds instance this.initModel(); } diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 6d07173fe24..377c6aad3ea 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -27,31 +27,42 @@
Alert Rule
-
+
Name
-
- Handler -
- -
-
+ + + + + + + + +
Evaluate every
-
- Notifications - +
+
+ Notifications + +
+
+ Severity +
+ +
+
@@ -59,7 +70,7 @@
Conditions
- {{$index+1}} + AND
- - - Critcal if - - - + When Value + +
- - - Warn if - - - -
+ + @@ -123,6 +127,10 @@
+
+ Evaluating rule +
+
+
+
+{{ctrl.testResult}}
+  
+
+
-
-{{ctrl.testResult}}
-  
+
diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 194d7a5487c..77a88efcf7a 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -71,6 +71,7 @@ @import "components/query_editor"; @import "components/tabbed_view"; @import "components/query_part"; +@import "components/jsontree"; // PAGES @import "pages/login"; diff --git a/public/sass/components/_jsontree.scss b/public/sass/components/_jsontree.scss new file mode 100644 index 00000000000..668382180f7 --- /dev/null +++ b/public/sass/components/_jsontree.scss @@ -0,0 +1,61 @@ +/* Structure */ +json-tree { + .json-tree-key { + vertical-align: middle; + } + .expandable { + position: relative; + &::before { + pointer-events: none; + } + &::before, & > .key { + cursor: pointer; + } + } + .json-tree-branch-preview { + display: inline-block; + vertical-align: middle; + } +} + +/* Looks */ +json-tree { + ul { + padding-left: $spacer; + } + li, ul { + list-style: none; + } + li { + line-height: 1.3rem; + } + .json-tree-key { + color: $variable; + padding: 5px 10px 5px 15px; + &::after { + content: ':'; + } + } + json-node.expandable { + &::before { + content: '\25b6'; + position: absolute; + left: 0px; + font-size: 10px; + transition: transform .1s ease; + } + &.expanded::before { + transform: rotate(90deg); + } + } + .json-tree-leaf-value, .json-tree-branch-preview { + word-break: break-all; + } + .json-tree-branch-preview { + overflow: hidden; + font-style: italic; + max-width: 40%; + height: 1.5em; + opacity: .7; + } +} From f0fc336e88a556feaf3c006784da3f8198acfb2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 21 Jul 2016 16:19:28 +0200 Subject: [PATCH 13/22] feat(alerting): worked on alert condition eval tests --- pkg/services/alerting/conditions_test.go | 86 ++++++++++++++++-------- pkg/services/alerting/handler.go | 1 - pkg/services/alerting/handler_test.go | 16 ++--- public/sass/components/_jsontree.scss | 4 +- 4 files changed, 67 insertions(+), 40 deletions(-) diff --git a/pkg/services/alerting/conditions_test.go b/pkg/services/alerting/conditions_test.go index bbeb16597c6..a9f340c37a8 100644 --- a/pkg/services/alerting/conditions_test.go +++ b/pkg/services/alerting/conditions_test.go @@ -14,51 +14,79 @@ func TestQueryCondition(t *testing.T) { Convey("when evaluating query condition", t, func() { - bus.AddHandler("test", func(query *m.GetDataSourceByIdQuery) error { - query.Result = &m.DataSource{Id: 1, Type: "graphite"} - return nil + queryConditionScenario("Given avg() and > 100", func(ctx *queryConditionTestContext) { + + ctx.reducer = `{"type": "avg"}` + ctx.evaluator = `{"type": ">", "params": [100]}` + + Convey("should trigger when avg is above 100", func() { + ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{120, 0}})} + ctx.exec() + + So(ctx.result.Error, ShouldBeNil) + So(ctx.result.Triggered, ShouldBeTrue) + }) + + Convey("Should not trigger when avg is below 100", func() { + ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{90, 0}})} + ctx.exec() + + So(ctx.result.Error, ShouldBeNil) + So(ctx.result.Triggered, ShouldBeFalse) + }) }) + }) +} - Convey("Given avg() and > 100", func() { +type queryConditionTestContext struct { + reducer string + evaluator string + series tsdb.TimeSeriesSlice + result *AlertResultContext +} - jsonModel, err := simplejson.NewJson([]byte(`{ +type queryConditionScenarioFunc func(c *queryConditionTestContext) + +func (ctx *queryConditionTestContext) exec() { + jsonModel, err := simplejson.NewJson([]byte(`{ "type": "query", "query": { "params": ["A", "5m", "now"], "datasourceId": 1, "model": {"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} }, - "reducer": {"type": "avg", "params": []}, - "evaluator": {"type": ">", "params": [100]} + "reducer":` + ctx.reducer + `, + "evaluator":` + ctx.evaluator + ` }`)) - So(err, ShouldBeNil) + So(err, ShouldBeNil) - condition, err := NewQueryCondition(jsonModel) - So(err, ShouldBeNil) + condition, err := NewQueryCondition(jsonModel) + So(err, ShouldBeNil) - Convey("Should set result to triggered when avg is above 100", func() { - context := &AlertResultContext{ - Rule: &AlertRule{}, - } + condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) { + return &tsdb.Response{ + Results: map[string]*tsdb.QueryResult{ + "A": {Series: ctx.series}, + }, + }, nil + } - condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) { - return &tsdb.Response{ - Results: map[string]*tsdb.QueryResult{ - "A": &tsdb.QueryResult{ - Series: tsdb.TimeSeriesSlice{ - tsdb.NewTimeSeries("test1", [][2]float64{{120, 0}}), - }, - }, - }, - }, nil - } + condition.Eval(ctx.result) +} - condition.Eval(context) +func queryConditionScenario(desc string, fn queryConditionScenarioFunc) { + Convey(desc, func() { - So(context.Error, ShouldBeNil) - So(context.Triggered, ShouldBeTrue) - }) + bus.AddHandler("test", func(query *m.GetDataSourceByIdQuery) error { + query.Result = &m.DataSource{Id: 1, Type: "graphite"} + return nil }) + ctx := &queryConditionTestContext{} + ctx.result = &AlertResultContext{ + Rule: &AlertRule{}, + } + + fn(ctx) }) } diff --git a/pkg/services/alerting/handler.go b/pkg/services/alerting/handler.go index 77218d473ff..b54f74563c5 100644 --- a/pkg/services/alerting/handler.go +++ b/pkg/services/alerting/handler.go @@ -55,7 +55,6 @@ func (e *HandlerImpl) eval(context *AlertResultContext) { } context.EndTime = time.Now() - context.DoneChan <- true } // func (e *HandlerImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) { diff --git a/pkg/services/alerting/handler_test.go b/pkg/services/alerting/handler_test.go index 68389891431..2841ca0b117 100644 --- a/pkg/services/alerting/handler_test.go +++ b/pkg/services/alerting/handler_test.go @@ -19,26 +19,26 @@ func TestAlertingExecutor(t *testing.T) { handler := NewHandler() Convey("Show return triggered with single passing condition", func() { - rule := &AlertRule{ + context := NewAlertResultContext(&AlertRule{ Conditions: []AlertCondition{&conditionStub{ triggered: true, }}, - } + }) - result := handler.eval(rule) - So(result.Triggered, ShouldEqual, true) + handler.eval(context) + So(context.Triggered, ShouldEqual, true) }) Convey("Show return false with not passing condition", func() { - rule := &AlertRule{ + context := NewAlertResultContext(&AlertRule{ Conditions: []AlertCondition{ &conditionStub{triggered: true}, &conditionStub{triggered: false}, }, - } + }) - result := handler.eval(rule) - So(result.Triggered, ShouldEqual, false) + handler.eval(context) + So(context.Triggered, ShouldEqual, false) }) // Convey("Show return critical since below 2", func() { diff --git a/public/sass/components/_jsontree.scss b/public/sass/components/_jsontree.scss index 668382180f7..011566f8731 100644 --- a/public/sass/components/_jsontree.scss +++ b/public/sass/components/_jsontree.scss @@ -8,7 +8,7 @@ json-tree { &::before { pointer-events: none; } - &::before, & > .key { + &::before, & > .json-tree-key { cursor: pointer; } } @@ -41,7 +41,7 @@ json-tree { content: '\25b6'; position: absolute; left: 0px; - font-size: 10px; + font-size: 8px; transition: transform .1s ease; } &.expanded::before { From b073fe0ebadb0d3884eac43d12df27ca544a6880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 21 Jul 2016 17:31:46 +0200 Subject: [PATCH 14/22] feat(alerting): more work on handling result and saving state --- pkg/services/alerting/engine.go | 18 ++-- pkg/services/alerting/handler.go | 1 + pkg/services/alerting/models.go | 3 + pkg/services/alerting/result_handler.go | 93 +++++++++++-------- pkg/services/alerting/result_handler_test.go | 27 ++++-- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 4 +- 6 files changed, 88 insertions(+), 58 deletions(-) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index c4b52390de3..27baad3c07a 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -71,23 +71,25 @@ func (e *Engine) alertingTicker() { } func (e *Engine) execDispatch() { - defer func() { - if err := recover(); err != nil { - e.log.Error("Scheduler Panic: stopping executor", "error", err, "stack", log.Stack(1)) - } - }() - for job := range e.execQueue { - log.Trace("Alerting: engine:execDispatch() starting job %s", job.Rule.Name) - e.executeJob(job) + e.log.Debug("Starting executing alert rule %s", job.Rule.Name) + go e.executeJob(job) } } func (e *Engine) executeJob(job *AlertJob) { + defer func() { + if err := recover(); err != nil { + e.log.Error("Execute Alert Panic", "error", err, "stack", log.Stack(1)) + } + }() + job.Running = true context := NewAlertResultContext(job.Rule) e.handler.Execute(context) job.Running = false + + e.resultQueue <- context } func (e *Engine) resultHandler() { diff --git a/pkg/services/alerting/handler.go b/pkg/services/alerting/handler.go index b54f74563c5..77218d473ff 100644 --- a/pkg/services/alerting/handler.go +++ b/pkg/services/alerting/handler.go @@ -55,6 +55,7 @@ func (e *HandlerImpl) eval(context *AlertResultContext) { } context.EndTime = time.Now() + context.DoneChan <- true } // func (e *HandlerImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) { diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index c1da88bdc38..837111b7f7a 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -4,6 +4,7 @@ import ( "time" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" ) type AlertJob struct { @@ -38,6 +39,7 @@ type AlertResultContext struct { Rule *AlertRule DoneChan chan bool CancelChan chan bool + log log.Logger } func (a *AlertResultContext) GetDurationSeconds() float64 { @@ -51,6 +53,7 @@ func NewAlertResultContext(rule *AlertRule) *AlertResultContext { Logs: make([]*AlertResultLogEntry, 0), DoneChan: make(chan bool, 1), CancelChan: make(chan bool, 1), + log: log.New("alerting.engine"), } } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 3f48cfd4f76..44c7b43da8f 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -1,6 +1,14 @@ package alerting -import "github.com/grafana/grafana/pkg/log" +import ( + "time" + + "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" + "github.com/grafana/grafana/pkg/services/alerting/alertstates" +) type ResultHandler interface { Handle(result *AlertResultContext) @@ -19,44 +27,51 @@ func NewResultHandler() *ResultHandlerImpl { } func (handler *ResultHandlerImpl) Handle(result *AlertResultContext) { - // if handler.shouldUpdateState(result) { - // cmd := &m.UpdateAlertStateCommand{ - // AlertId: result.Rule.Id, - // State: result.Rule.Severity, - // Info: result.Description, - // OrgId: result.Rule.OrgId, - // TriggeredAlerts: simplejson.NewFromAny(result.Details), - // } - // - // if err := bus.Dispatch(cmd); err != nil { - // handler.log.Error("Failed to save state", "error", err) - // } - // - // handler.log.Debug("will notify about new state", "new state", result.State) - // handler.notifier.Notify(result) - // } + newState := alertstates.Ok + if result.Triggered { + newState = result.Rule.Severity + } + + handler.log.Info("Handle result", "newState", newState) + handler.log.Info("Handle result", "triggered", result.Triggered) + + if handler.shouldUpdateState(result, newState) { + cmd := &m.UpdateAlertStateCommand{ + AlertId: result.Rule.Id, + Info: result.Description, + OrgId: result.Rule.OrgId, + State: newState, + TriggeredAlerts: simplejson.NewFromAny(result.Details), + } + + if err := bus.Dispatch(cmd); err != nil { + handler.log.Error("Failed to save state", "error", err) + } + + //handler.log.Debug("will notify about new state", "new state", result.State) + //handler.notifier.Notify(result) + } } -func (handler *ResultHandlerImpl) shouldUpdateState(result *AlertResultContext) bool { - // query := &m.GetLastAlertStateQuery{ - // AlertId: result.AlertJob.Rule.Id, - // OrgId: result.AlertJob.Rule.OrgId, - // } - // - // if err := bus.Dispatch(query); err != nil { - // log.Error2("Failed to read last alert state", "error", err) - // return false - // } - // - // if query.Result == nil { - // return true - // } - // - // lastExecution := query.Result.Created - // asdf := result.StartTime.Add(time.Minute * -15) - // olderThen15Min := lastExecution.Before(asdf) - // changedState := query.Result.State != result.State - // - // return changedState || olderThen15Min - return false +func (handler *ResultHandlerImpl) shouldUpdateState(result *AlertResultContext, newState string) bool { + query := &m.GetLastAlertStateQuery{ + AlertId: result.Rule.Id, + OrgId: result.Rule.OrgId, + } + + if err := bus.Dispatch(query); err != nil { + log.Error2("Failed to read last alert state", "error", err) + return false + } + + if query.Result == nil { + return true + } + + lastExecution := query.Result.Created + asdf := result.StartTime.Add(time.Minute * -15) + olderThen15Min := lastExecution.Before(asdf) + changedState := query.Result.State != newState + + return changedState || olderThen15Min } diff --git a/pkg/services/alerting/result_handler_test.go b/pkg/services/alerting/result_handler_test.go index 368a63345f4..32589bef172 100644 --- a/pkg/services/alerting/result_handler_test.go +++ b/pkg/services/alerting/result_handler_test.go @@ -1,15 +1,24 @@ package alerting +// import ( +// "testing" +// "time" +// +// "github.com/grafana/grafana/pkg/bus" +// m "github.com/grafana/grafana/pkg/models" +// "github.com/grafana/grafana/pkg/services/alerting/alertstates" +// +// . "github.com/smartystreets/goconvey/convey" +// ) +// // func TestAlertResultHandler(t *testing.T) { // Convey("Test result Handler", t, func() { // resultHandler := ResultHandlerImpl{} -// mockResult := &AlertResult{ -// State: alertstates.Ok, -// AlertJob: &AlertJob{ -// Rule: &AlertRule{ -// Id: 1, -// OrgId: 1, -// }, +// mockResult := &AlertResultContext{ +// Triggered: false, +// Rule: &AlertRule{ +// Id: 1, +// OrgId 1, // }, // } // mockAlertState := &m.AlertState{} @@ -30,7 +39,7 @@ package alerting // mockAlertState = &m.AlertState{ // State: alertstates.Critical, // } -// mockResult.State = alertstates.Ok +// mockResult.Triggered = false // So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) // }) // @@ -40,7 +49,7 @@ package alerting // State: alertstates.Critical, // Created: now.Add(time.Minute * -30), // } -// mockResult.State = alertstates.Critical +// mockResult.Triggered = true // mockResult.StartTime = time.Now() // So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) // }) diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 559e9c6b78e..6ac835bda47 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -46,8 +46,8 @@ export class AlertTabCtrl { {text: '<', value: '<'}, ]; severityLevels = [ - {text: 'Critical', value: 'critical'}, - {text: 'Warning', value: 'warning'}, + {text: 'Critical', value: 'CRITICAL'}, + {text: 'Warning', value: 'WARN'}, ]; /** @ngInject */ From 783d69752973fe3772cc16c00714cac7bf7f8aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 21 Jul 2016 21:54:12 +0200 Subject: [PATCH 15/22] feat(alerting): more output when testing alert --- pkg/api/dtos/alerting.go | 5 ++ pkg/models/alert_state.go | 9 ++-- pkg/services/alerting/alert_rule.go | 4 +- pkg/services/alerting/conditions.go | 17 ++++++- pkg/services/alerting/conditions_test.go | 2 +- pkg/services/alerting/handler.go | 2 +- pkg/services/alerting/models.go | 5 +- pkg/services/alerting/result_handler.go | 10 ++-- pkg/services/sqlstore/alert_state.go | 11 ++--- .../app/features/alerting/alert_log_ctrl.ts | 2 +- .../features/alerting/partials/alert_log.html | 49 ------------------- 11 files changed, 41 insertions(+), 75 deletions(-) diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 2276d87e545..f503b330799 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -50,3 +50,8 @@ type AlertTestResultLog struct { Message string `json:"message"` Data interface{} `json:"data"` } + +type AlertEvent struct { + Metric string `json:"metric"` + Value float64 `json:"value"` +} diff --git a/pkg/models/alert_state.go b/pkg/models/alert_state.go index 679da91f22f..b32a0dc8aec 100644 --- a/pkg/models/alert_state.go +++ b/pkg/models/alert_state.go @@ -29,11 +29,10 @@ func (this *UpdateAlertStateCommand) IsValidState() bool { // Commands type UpdateAlertStateCommand struct { - AlertId int64 `json:"alertId" binding:"Required"` - OrgId int64 `json:"orgId" binding:"Required"` - State string `json:"state" binding:"Required"` - Info string `json:"info"` - TriggeredAlerts *simplejson.Json `json:"triggeredAlerts"` + AlertId int64 `json:"alertId" binding:"Required"` + OrgId int64 `json:"orgId" binding:"Required"` + State string `json:"state" binding:"Required"` + Info string `json:"info"` Result *Alert } diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index e9942e76a8a..138884276bd 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -70,11 +70,11 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { } } - for _, condition := range ruleDef.Settings.Get("conditions").MustArray() { + for index, condition := range ruleDef.Settings.Get("conditions").MustArray() { conditionModel := simplejson.NewFromAny(condition) switch conditionModel.Get("type").MustString() { case "query": - queryCondition, err := NewQueryCondition(conditionModel) + queryCondition, err := NewQueryCondition(conditionModel, index) if err != nil { return nil, err } diff --git a/pkg/services/alerting/conditions.go b/pkg/services/alerting/conditions.go index a77cfdaf686..a72eff3a7a6 100644 --- a/pkg/services/alerting/conditions.go +++ b/pkg/services/alerting/conditions.go @@ -11,6 +11,7 @@ import ( ) type QueryCondition struct { + Index int Query AlertQuery Reducer QueryReducer Evaluator AlertEvaluator @@ -27,7 +28,18 @@ func (c *QueryCondition) Eval(context *AlertResultContext) { for _, series := range seriesList { reducedValue := c.Reducer.Reduce(series) pass := c.Evaluator.Eval(series, reducedValue) + + if context.IsTestRun { + context.Logs = append(context.Logs, &AlertResultLogEntry{ + Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, pass, series.Name, reducedValue), + }) + } + if pass { + context.Events = append(context.Events, &AlertEvent{ + Metric: series.Name, + Value: reducedValue, + }) context.Triggered = true break } @@ -61,7 +73,7 @@ func (c *QueryCondition) executeQuery(context *AlertResultContext) (tsdb.TimeSer if context.IsTestRun { context.Logs = append(context.Logs, &AlertResultLogEntry{ - Message: "Query Condition Query Result", + Message: fmt.Sprintf("Condition[%d]: Query Result", c.Index), Data: v.Series, }) } @@ -93,8 +105,9 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb. return req } -func NewQueryCondition(model *simplejson.Json) (*QueryCondition, error) { +func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, error) { condition := QueryCondition{} + condition.Index = index condition.HandleRequest = tsdb.HandleRequest queryJson := model.Get("query") diff --git a/pkg/services/alerting/conditions_test.go b/pkg/services/alerting/conditions_test.go index a9f340c37a8..89a50cedd20 100644 --- a/pkg/services/alerting/conditions_test.go +++ b/pkg/services/alerting/conditions_test.go @@ -60,7 +60,7 @@ func (ctx *queryConditionTestContext) exec() { }`)) So(err, ShouldBeNil) - condition, err := NewQueryCondition(jsonModel) + condition, err := NewQueryCondition(jsonModel, 0) So(err, ShouldBeNil) condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) { diff --git a/pkg/services/alerting/handler.go b/pkg/services/alerting/handler.go index 77218d473ff..dbc1deb8090 100644 --- a/pkg/services/alerting/handler.go +++ b/pkg/services/alerting/handler.go @@ -33,7 +33,7 @@ func (e *HandlerImpl) Execute(context *AlertResultContext) { context.EndTime = time.Now() e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id) case <-context.DoneChan: - e.log.Debug("Job Execution done", "timing", context.GetDurationSeconds(), "alertId", context.Rule.Id) + e.log.Debug("Job Execution done", "timing", context.GetDurationSeconds(), "alertId", context.Rule.Id, "triggered", context.Triggered) } } diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 837111b7f7a..598dd4fcdf4 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -30,7 +30,7 @@ func (aj *AlertJob) IncRetry() { type AlertResultContext struct { Triggered bool IsTestRun bool - Details []*AlertResultDetail + Events []*AlertEvent Logs []*AlertResultLogEntry Error error Description string @@ -51,6 +51,7 @@ func NewAlertResultContext(rule *AlertRule) *AlertResultContext { StartTime: time.Now(), Rule: rule, Logs: make([]*AlertResultLogEntry, 0), + Events: make([]*AlertEvent, 0), DoneChan: make(chan bool, 1), CancelChan: make(chan bool, 1), log: log.New("alerting.engine"), @@ -62,7 +63,7 @@ type AlertResultLogEntry struct { Data interface{} } -type AlertResultDetail struct { +type AlertEvent struct { Value float64 Metric string State string diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 44c7b43da8f..669f1a63ef7 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -4,7 +4,6 @@ import ( "time" "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" "github.com/grafana/grafana/pkg/services/alerting/alertstates" @@ -37,11 +36,10 @@ func (handler *ResultHandlerImpl) Handle(result *AlertResultContext) { if handler.shouldUpdateState(result, newState) { cmd := &m.UpdateAlertStateCommand{ - AlertId: result.Rule.Id, - Info: result.Description, - OrgId: result.Rule.OrgId, - State: newState, - TriggeredAlerts: simplejson.NewFromAny(result.Details), + AlertId: result.Rule.Id, + Info: result.Description, + OrgId: result.Rule.OrgId, + State: newState, } if err := bus.Dispatch(cmd); err != nil { diff --git a/pkg/services/sqlstore/alert_state.go b/pkg/services/sqlstore/alert_state.go index d335f7402a7..2d591003292 100644 --- a/pkg/services/sqlstore/alert_state.go +++ b/pkg/services/sqlstore/alert_state.go @@ -51,12 +51,11 @@ func SetNewAlertState(cmd *m.UpdateAlertStateCommand) error { sess.Id(alert.Id).Update(&alert) alertState := m.AlertState{ - AlertId: cmd.AlertId, - OrgId: cmd.OrgId, - State: cmd.State, - Info: cmd.Info, - Created: time.Now(), - TriggeredAlerts: cmd.TriggeredAlerts, + AlertId: cmd.AlertId, + OrgId: cmd.OrgId, + State: cmd.State, + Info: cmd.Info, + Created: time.Now(), } sess.Insert(&alertState) diff --git a/public/app/features/alerting/alert_log_ctrl.ts b/public/app/features/alerting/alert_log_ctrl.ts index 2727f486604..a9a3788c686 100644 --- a/public/app/features/alerting/alert_log_ctrl.ts +++ b/public/app/features/alerting/alert_log_ctrl.ts @@ -22,7 +22,7 @@ export class AlertLogCtrl { loadAlertLogs(alertId: number) { this.backendSrv.get(`/api/alerts/${alertId}/states`).then(result => { this.alertLogs = _.map(result, log => { - log.iconCss = alertDef.getCssForState(log.newState); + log.iconCss = alertDef.getCssForState(log.state); log.humanTime = moment(log.created).format("YYYY-MM-DD HH:mm:ss"); return log; }); diff --git a/public/app/features/alerting/partials/alert_log.html b/public/app/features/alerting/partials/alert_log.html index 5f0ef080fe9..921f748da76 100644 --- a/public/app/features/alerting/partials/alert_log.html +++ b/public/app/features/alerting/partials/alert_log.html @@ -6,55 +6,6 @@

Alert history for {{ctrl.alert.title}}

-
-
Thresholds
-
- - - Warn level - -
- {{ctrl.alert.warnOperator}} -
-
- {{ctrl.alert.warnLevel}} -
-
-
- - - Critical level - -
- {{ctrl.alert.critOperator}} -
-
- {{ctrl.alert.critLevel}} -
-
-
- -
-
Aggregators
-
- - Aggregator - -
- {{ctrl.alert.aggregator}} -
-
-
- Query range (seconds) - {{ctrl.alert.queryRange}} -
- -
- Frequency (seconds) - {{ctrl.alert.frequency}} -
-
- From 7eb2d2cf4772f09dd87e7aee3ecda67c497e91ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Jul 2016 13:14:09 +0200 Subject: [PATCH 16/22] feat(alerting): working on state management --- pkg/api/alerting.go | 73 +++---- pkg/api/api.go | 2 +- pkg/api/dtos/alerting.go | 31 ++- pkg/models/alert.go | 63 +++--- pkg/models/alert_state.go | 97 ++++----- pkg/services/alerting/alert_rule.go | 4 +- pkg/services/alerting/alertstates/states.go | 16 -- pkg/services/alerting/conditions.go | 2 +- pkg/services/alerting/conditions_test.go | 8 +- pkg/services/alerting/engine.go | 2 +- pkg/services/alerting/extractor.go | 9 +- pkg/services/alerting/handler.go | 4 +- pkg/services/alerting/handler_test.go | 14 +- pkg/services/alerting/models.go | 2 +- pkg/services/alerting/result_handler.go | 50 ++--- pkg/services/sqlstore/alert.go | 64 ++---- pkg/services/sqlstore/alert_state.go | 150 +++++++------- pkg/services/sqlstore/alert_state_test.go | 196 +++++++++--------- public/app/features/alerting/alert_def.ts | 15 +- public/app/features/alerting/alerts_ctrl.ts | 9 +- .../alerting/partials/alert_list.html | 13 +- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 4 +- public/sass/components/_tags.scss | 2 +- 23 files changed, 380 insertions(+), 450 deletions(-) delete mode 100644 pkg/services/alerting/alertstates/states.go diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 06c03a3e690..ef845337d7b 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -49,6 +49,7 @@ func GetAlerts(c *middleware.Context) Response { Name: alert.Name, Description: alert.Description, State: alert.State, + Severity: alert.Severity, }) } @@ -92,7 +93,7 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response { res := backendCmd.Result dtoRes := &dtos.AlertTestResult{ - Triggered: res.Triggered, + Firing: res.Firing, } if res.Error != nil { @@ -138,41 +139,41 @@ func DelAlert(c *middleware.Context) Response { return Json(200, resp) } -// GET /api/alerts/events/:id -func GetAlertStates(c *middleware.Context) Response { - alertId := c.ParamsInt64(":alertId") - - query := models.GetAlertsStateQuery{ - AlertId: alertId, - } - - if err := bus.Dispatch(&query); err != nil { - return ApiError(500, "Failed get alert state log", err) - } - - return Json(200, query.Result) -} - -// PUT /api/alerts/events/:id -func PutAlertState(c *middleware.Context, cmd models.UpdateAlertStateCommand) Response { - cmd.AlertId = c.ParamsInt64(":alertId") - cmd.OrgId = c.OrgId - - query := models.GetAlertByIdQuery{Id: cmd.AlertId} - if err := bus.Dispatch(&query); err != nil { - return ApiError(500, "Failed to get alertstate", err) - } - - if query.Result.OrgId != 0 && query.Result.OrgId != c.OrgId { - return ApiError(500, "Alert not found", nil) - } - - if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to set new state", err) - } - - return Json(200, cmd.Result) -} +// // GET /api/alerts/events/:id +// func GetAlertStates(c *middleware.Context) Response { +// alertId := c.ParamsInt64(":alertId") +// +// query := models.GetAlertsStateQuery{ +// AlertId: alertId, +// } +// +// if err := bus.Dispatch(&query); err != nil { +// return ApiError(500, "Failed get alert state log", err) +// } +// +// return Json(200, query.Result) +// } +// +// // PUT /api/alerts/events/:id +// func PutAlertState(c *middleware.Context, cmd models.UpdateAlertStateCommand) Response { +// cmd.AlertId = c.ParamsInt64(":alertId") +// cmd.OrgId = c.OrgId +// +// query := models.GetAlertByIdQuery{Id: cmd.AlertId} +// if err := bus.Dispatch(&query); err != nil { +// return ApiError(500, "Failed to get alertstate", err) +// } +// +// if query.Result.OrgId != 0 && query.Result.OrgId != c.OrgId { +// return ApiError(500, "Alert not found", nil) +// } +// +// if err := bus.Dispatch(&cmd); err != nil { +// return ApiError(500, "Failed to set new state", err) +// } +// +// return Json(200, cmd.Result) +// } func GetAlertNotifications(c *middleware.Context) Response { query := &models.GetAlertNotificationQuery{ diff --git a/pkg/api/api.go b/pkg/api/api.go index f23d3f1a2f8..b95e7dc5459 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -247,7 +247,7 @@ func Register(r *macaron.Macaron) { r.Group("/alerts", func() { r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest)) - r.Get("/:alertId/states", wrap(GetAlertStates)) + //r.Get("/:alertId/states", wrap(GetAlertStates)) //r.Put("/:alertId/state", bind(m.UpdateAlertStateCommand{}), wrap(PutAlertState)) r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert)) //r.Delete("/:alertId", ValidateOrgAlert, wrap(DelAlert)) disabled until we know how to handle it dashboard updates diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index f503b330799..35fc3f9e638 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -4,24 +4,17 @@ import ( "time" "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" ) type AlertRuleDTO struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - Query string `json:"query"` - QueryRefId string `json:"queryRefId"` - WarnLevel float64 `json:"warnLevel"` - CritLevel float64 `json:"critLevel"` - WarnOperator string `json:"warnOperator"` - CritOperator string `json:"critOperator"` - Frequency int64 `json:"frequency"` - Name string `json:"name"` - Description string `json:"description"` - QueryRange int `json:"queryRange"` - Aggregator string `json:"aggregator"` - State string `json:"state"` + Id int64 `json:"id"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + Name string `json:"name"` + Description string `json:"description"` + State m.AlertStateType `json:"state"` + Severity m.AlertSeverityType `json:"severity"` DashbboardUri string `json:"dashboardUri"` } @@ -40,10 +33,10 @@ type AlertTestCommand struct { } type AlertTestResult struct { - Triggered bool `json:"triggerd"` - Timing string `json:"timing"` - Error string `json:"error,omitempty"` - Logs []*AlertTestResultLog `json:"logs,omitempty"` + Firing bool `json:"firing"` + Timing string `json:"timing"` + Error string `json:"error,omitempty"` + Logs []*AlertTestResultLog `json:"logs,omitempty"` } type AlertTestResultLog struct { diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 7e5ba45f14e..e6b57242cac 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -6,6 +6,29 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" ) +type AlertStateType string +type AlertSeverityType string + +const ( + AlertStatePending AlertStateType = "pending" + AlertStateFiring AlertStateType = "firing" + AlertStateOK AlertStateType = "ok" +) + +func (s AlertStateType) IsValid() bool { + return s == AlertStatePending || s == AlertStateFiring || s == AlertStateOK +} + +const ( + AlertSeverityCritical AlertSeverityType = "critical" + AlertSeverityWarning AlertSeverityType = "warning" + AlertSeverityInfo AlertSeverityType = "info" +) + +func (s AlertSeverityType) IsValid() bool { + return s == AlertSeverityCritical || s == AlertSeverityInfo || s == AlertSeverityWarning +} + type Alert struct { Id int64 OrgId int64 @@ -13,8 +36,8 @@ type Alert struct { PanelId int64 Name string Description string - Severity string - State string + Severity AlertSeverityType + State AlertStateType Handler int64 Enabled bool Frequency int64 @@ -32,7 +55,7 @@ func (alert *Alert) ValidToSave() bool { return alert.DashboardId != 0 && alert.OrgId != 0 && alert.PanelId != 0 } -func (alert *Alert) ShouldUpdateState(newState string) bool { +func (alert *Alert) ShouldUpdateState(newState AlertStateType) bool { return alert.State != newState } @@ -74,25 +97,6 @@ type HeartBeatCommand struct { Result AlertingClusterInfo } -type AlertChange struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - AlertId int64 `json:"alertId"` - UpdatedBy int64 `json:"updatedBy"` - NewAlertSettings *simplejson.Json `json:"newAlertSettings"` - Type string `json:"type"` - Created time.Time `json:"created"` -} - -// Commands -type CreateAlertChangeCommand struct { - OrgId int64 - AlertId int64 - UpdatedBy int64 - NewAlertSettings *simplejson.Json - Type string -} - type SaveAlertsCommand struct { DashboardId int64 UserId int64 @@ -101,6 +105,13 @@ type SaveAlertsCommand struct { Alerts []*Alert } +type SetAlertStateCommand struct { + AlertId int64 + OrgId int64 + State AlertStateType + Timestamp time.Time +} + type DeleteAlertCommand struct { AlertId int64 } @@ -124,11 +135,3 @@ type GetAlertByIdQuery struct { Result *Alert } - -type GetAlertChangesQuery struct { - OrgId int64 - Limit int64 - SinceId int64 - - Result []*AlertChange -} diff --git a/pkg/models/alert_state.go b/pkg/models/alert_state.go index b32a0dc8aec..5071efc2171 100644 --- a/pkg/models/alert_state.go +++ b/pkg/models/alert_state.go @@ -1,54 +1,47 @@ package models -import ( - "time" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/alerting/alertstates" -) - -type AlertState struct { - Id int64 `json:"-"` - OrgId int64 `json:"-"` - AlertId int64 `json:"alertId"` - State string `json:"state"` - Created time.Time `json:"created"` - Info string `json:"info"` - TriggeredAlerts *simplejson.Json `json:"triggeredAlerts"` -} - -func (this *UpdateAlertStateCommand) IsValidState() bool { - for _, v := range alertstates.ValidStates { - if this.State == v { - return true - } - } - return false -} - -// Commands - -type UpdateAlertStateCommand struct { - AlertId int64 `json:"alertId" binding:"Required"` - OrgId int64 `json:"orgId" binding:"Required"` - State string `json:"state" binding:"Required"` - Info string `json:"info"` - - Result *Alert -} - -// Queries - -type GetAlertsStateQuery struct { - OrgId int64 `json:"orgId" binding:"Required"` - AlertId int64 `json:"alertId" binding:"Required"` - - Result *[]AlertState -} - -type GetLastAlertStateQuery struct { - AlertId int64 - OrgId int64 - - Result *AlertState -} +// type AlertState struct { +// Id int64 `json:"-"` +// OrgId int64 `json:"-"` +// AlertId int64 `json:"alertId"` +// State string `json:"state"` +// Created time.Time `json:"created"` +// Info string `json:"info"` +// TriggeredAlerts *simplejson.Json `json:"triggeredAlerts"` +// } +// +// func (this *UpdateAlertStateCommand) IsValidState() bool { +// for _, v := range alertstates.ValidStates { +// if this.State == v { +// return true +// } +// } +// return false +// } +// +// // Commands +// +// type UpdateAlertStateCommand struct { +// AlertId int64 `json:"alertId" binding:"Required"` +// OrgId int64 `json:"orgId" binding:"Required"` +// State string `json:"state" binding:"Required"` +// Info string `json:"info"` +// +// Result *Alert +// } +// +// // Queries +// +// type GetAlertsStateQuery struct { +// OrgId int64 `json:"orgId" binding:"Required"` +// AlertId int64 `json:"alertId" binding:"Required"` +// +// Result *[]AlertState +// } +// +// type GetLastAlertStateQuery struct { +// AlertId int64 +// OrgId int64 +// +// Result *AlertState +// } diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index 138884276bd..2402776e4be 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -18,7 +18,8 @@ type AlertRule struct { Frequency int64 Name string Description string - Severity string + State m.AlertStateType + Severity m.AlertSeverityType Conditions []AlertCondition Notifications []int64 } @@ -63,6 +64,7 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { model.Description = ruleDef.Description model.Frequency = ruleDef.Frequency model.Severity = ruleDef.Severity + model.State = ruleDef.State for _, v := range ruleDef.Settings.Get("notifications").MustArray() { if id, ok := v.(int64); ok { diff --git a/pkg/services/alerting/alertstates/states.go b/pkg/services/alerting/alertstates/states.go deleted file mode 100644 index cf2af121062..00000000000 --- a/pkg/services/alerting/alertstates/states.go +++ /dev/null @@ -1,16 +0,0 @@ -package alertstates - -var ( - ValidStates = []string{ - Ok, - Warn, - Critical, - Unknown, - } - - Ok = "OK" - Warn = "WARN" - Critical = "CRITICAL" - Pending = "PENDING" - Unknown = "UNKNOWN" -) diff --git a/pkg/services/alerting/conditions.go b/pkg/services/alerting/conditions.go index a72eff3a7a6..42affee9d57 100644 --- a/pkg/services/alerting/conditions.go +++ b/pkg/services/alerting/conditions.go @@ -40,7 +40,7 @@ func (c *QueryCondition) Eval(context *AlertResultContext) { Metric: series.Name, Value: reducedValue, }) - context.Triggered = true + context.Firing = true break } } diff --git a/pkg/services/alerting/conditions_test.go b/pkg/services/alerting/conditions_test.go index 89a50cedd20..6fbe2ebe93b 100644 --- a/pkg/services/alerting/conditions_test.go +++ b/pkg/services/alerting/conditions_test.go @@ -19,20 +19,20 @@ func TestQueryCondition(t *testing.T) { ctx.reducer = `{"type": "avg"}` ctx.evaluator = `{"type": ">", "params": [100]}` - Convey("should trigger when avg is above 100", func() { + Convey("should fire when avg is above 100", func() { ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{120, 0}})} ctx.exec() So(ctx.result.Error, ShouldBeNil) - So(ctx.result.Triggered, ShouldBeTrue) + So(ctx.result.Firing, ShouldBeTrue) }) - Convey("Should not trigger when avg is below 100", func() { + Convey("Should not fire when avg is below 100", func() { ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{90, 0}})} ctx.exec() So(ctx.result.Error, ShouldBeNil) - So(ctx.result.Triggered, ShouldBeFalse) + So(ctx.result.Firing, ShouldBeFalse) }) }) }) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 27baad3c07a..e9fa1d529d8 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -100,7 +100,7 @@ func (e *Engine) resultHandler() { }() for result := range e.resultQueue { - e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "triggered", result.Triggered) + e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing) if result.Error != nil { e.log.Error("Alert Rule Result Error", "ruleId", result.Rule.Id, "error", result.Error, "retry") diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 1d09f226fe6..88f4998b9cf 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -2,7 +2,6 @@ package alerting import ( "errors" - "fmt" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -90,10 +89,14 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { Handler: jsonAlert.Get("handler").MustInt64(), Enabled: jsonAlert.Get("enabled").MustBool(), Description: jsonAlert.Get("description").MustString(), - Severity: jsonAlert.Get("severity").MustString(), + Severity: m.AlertSeverityType(jsonAlert.Get("severity").MustString()), Frequency: getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()), } + if !alert.Severity.IsValid() { + return nil, AlertValidationError{Reason: "Invalid alert Severity"} + } + for _, condition := range jsonAlert.Get("conditions").MustArray() { jsonCondition := simplejson.NewFromAny(condition) @@ -102,7 +105,7 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { panelQuery := findPanelQueryByRefId(panel, queryRefId) if panelQuery == nil { - return nil, fmt.Errorf("Alert referes to query %s, that could not be found", queryRefId) + return nil, AlertValidationError{Reason: "Alert refes to query that cannot be found"} } dsName := "" diff --git a/pkg/services/alerting/handler.go b/pkg/services/alerting/handler.go index dbc1deb8090..628dc94d9cd 100644 --- a/pkg/services/alerting/handler.go +++ b/pkg/services/alerting/handler.go @@ -33,7 +33,7 @@ func (e *HandlerImpl) Execute(context *AlertResultContext) { context.EndTime = time.Now() e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id) case <-context.DoneChan: - e.log.Debug("Job Execution done", "timing", context.GetDurationSeconds(), "alertId", context.Rule.Id, "triggered", context.Triggered) + e.log.Debug("Job Execution done", "timing", context.GetDurationSeconds(), "alertId", context.Rule.Id, "firing", context.Firing) } } @@ -49,7 +49,7 @@ func (e *HandlerImpl) eval(context *AlertResultContext) { } // break if result has not triggered yet - if context.Triggered == false { + if context.Firing == false { break } } diff --git a/pkg/services/alerting/handler_test.go b/pkg/services/alerting/handler_test.go index 2841ca0b117..10869226dd7 100644 --- a/pkg/services/alerting/handler_test.go +++ b/pkg/services/alerting/handler_test.go @@ -7,11 +7,11 @@ import ( ) type conditionStub struct { - triggered bool + firing bool } func (c *conditionStub) Eval(context *AlertResultContext) { - context.Triggered = c.triggered + context.Firing = c.firing } func TestAlertingExecutor(t *testing.T) { @@ -21,24 +21,24 @@ func TestAlertingExecutor(t *testing.T) { Convey("Show return triggered with single passing condition", func() { context := NewAlertResultContext(&AlertRule{ Conditions: []AlertCondition{&conditionStub{ - triggered: true, + firing: true, }}, }) handler.eval(context) - So(context.Triggered, ShouldEqual, true) + So(context.Firing, ShouldEqual, true) }) Convey("Show return false with not passing condition", func() { context := NewAlertResultContext(&AlertRule{ Conditions: []AlertCondition{ - &conditionStub{triggered: true}, - &conditionStub{triggered: false}, + &conditionStub{firing: true}, + &conditionStub{firing: false}, }, }) handler.eval(context) - So(context.Triggered, ShouldEqual, false) + So(context.Firing, ShouldEqual, false) }) // Convey("Show return critical since below 2", func() { diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 598dd4fcdf4..486f0bc9f98 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -28,7 +28,7 @@ func (aj *AlertJob) IncRetry() { } type AlertResultContext struct { - Triggered bool + Firing bool IsTestRun bool Events []*AlertEvent Logs []*AlertResultLogEntry diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 669f1a63ef7..7aae00dfe7e 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -1,12 +1,9 @@ package alerting import ( - "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting/alertstates" ) type ResultHandler interface { @@ -20,24 +17,27 @@ type ResultHandlerImpl struct { func NewResultHandler() *ResultHandlerImpl { return &ResultHandlerImpl{ - log: log.New("alerting.responseHandler"), - //notifier: NewNotifier(), + log: log.New("alerting.resultHandler"), } } func (handler *ResultHandlerImpl) Handle(result *AlertResultContext) { - newState := alertstates.Ok - if result.Triggered { - newState = result.Rule.Severity + var newState m.AlertStateType + + if result.Error != nil { + handler.log.Error("Alert Rule Result Error", "ruleId", result.Rule.Id, "error", result.Error) + newState = m.AlertStatePending + } else if result.Firing { + newState = m.AlertStateFiring + } else { + newState = m.AlertStateOK } - handler.log.Info("Handle result", "newState", newState) - handler.log.Info("Handle result", "triggered", result.Triggered) + if result.Rule.State != newState { + handler.log.Info("New state change", "alertId", result.Rule.Id, "newState", newState, "oldState", result.Rule.State) - if handler.shouldUpdateState(result, newState) { - cmd := &m.UpdateAlertStateCommand{ + cmd := &m.SetAlertStateCommand{ AlertId: result.Rule.Id, - Info: result.Description, OrgId: result.Rule.OrgId, State: newState, } @@ -46,30 +46,8 @@ func (handler *ResultHandlerImpl) Handle(result *AlertResultContext) { handler.log.Error("Failed to save state", "error", err) } + result.Rule.State = newState //handler.log.Debug("will notify about new state", "new state", result.State) //handler.notifier.Notify(result) } } - -func (handler *ResultHandlerImpl) shouldUpdateState(result *AlertResultContext, newState string) bool { - query := &m.GetLastAlertStateQuery{ - AlertId: result.Rule.Id, - OrgId: result.Rule.OrgId, - } - - if err := bus.Dispatch(query); err != nil { - log.Error2("Failed to read last alert state", "error", err) - return false - } - - if query.Result == nil { - return true - } - - lastExecution := query.Result.Created - asdf := result.StartTime.Add(time.Minute * -15) - olderThen15Min := lastExecution.Before(asdf) - changedState := query.Result.State != newState - - return changedState || olderThen15Min -} diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index c4f88d7cb4b..1a19b17aafa 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -17,52 +17,9 @@ func init() { bus.AddHandler("sql", GetAlertById) bus.AddHandler("sql", DeleteAlertById) bus.AddHandler("sql", GetAllAlertQueryHandler) - //bus.AddHandler("sql", HeartBeat) + bus.AddHandler("sql", SetAlertState) } -/* -func HeartBeat(query *m.HeartBeatCommand) error { - return inTransaction(func(sess *xorm.Session) error { - now := time.Now().Sub(0, 0, 0, 5) - activeTime := time.Now().Sub(0, 0, 0, 5) - ownHeartbeats := make([]m.HeartBeat, 0) - err := x.Where("server_id = ?", query.ServerId).Find(&ownHeartbeats) - - if err != nil { - return err - } - - if (len(ownHeartbeats)) > 0 && ownHeartbeats[0].Updated > activeTime { - //update - x.Insert(&m.HeartBeat{ServerId: query.ServerId, Created: now, Updated: now}) - } else { - thisServer := ownHeartbeats[0] - thisServer.Updated = now - x.Id(thisServer.Id).Update(&thisServer) - } - - activeServers := make([]m.HeartBeat, 0) - err = x.Where("server_id = ? and updated > ", query.ServerId, now.String()).OrderBy("id").Find(&activeServers) - - if err != nil { - return err - } - - for i, pos := range activeServers { - if pos.ServerId == query.ServerId { - query.Result = &m.AlertingClusterInfo{ - ClusterSize: len(activeServers), - UptimePosition: i, - } - return nil - } - } - - return nil - }) -} -*/ - func GetAlertById(query *m.GetAlertByIdQuery) error { alert := m.Alert{} has, err := x.Id(query.Id).Get(&alert) @@ -203,7 +160,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor } else { alert.Updated = time.Now() alert.Created = time.Now() - alert.State = "UNKNOWN" + alert.State = m.AlertStatePending alert.CreatedBy = cmd.UserId alert.UpdatedBy = cmd.UserId @@ -253,3 +210,20 @@ func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.Alert, return alerts, nil } + +func SetAlertState(cmd *m.SetAlertStateCommand) error { + return inTransaction(func(sess *xorm.Session) error { + alert := m.Alert{} + + if has, err := sess.Id(cmd.AlertId).Get(&alert); err != nil { + return err + } else if !has { + return fmt.Errorf("Could not find alert") + } + + alert.State = cmd.State + sess.Id(alert.Id).Update(&alert) + + return nil + }) +} diff --git a/pkg/services/sqlstore/alert_state.go b/pkg/services/sqlstore/alert_state.go index 2d591003292..38c14b077b5 100644 --- a/pkg/services/sqlstore/alert_state.go +++ b/pkg/services/sqlstore/alert_state.go @@ -1,77 +1,77 @@ package sqlstore -import ( - "fmt" - "time" - - "github.com/go-xorm/xorm" - "github.com/grafana/grafana/pkg/bus" - m "github.com/grafana/grafana/pkg/models" -) - -func init() { - bus.AddHandler("sql", SetNewAlertState) - bus.AddHandler("sql", GetAlertStateLogByAlertId) - bus.AddHandler("sql", GetLastAlertStateQuery) -} - -func GetLastAlertStateQuery(cmd *m.GetLastAlertStateQuery) error { - states := make([]m.AlertState, 0) - - if err := x.Where("alert_id = ? and org_id = ? ", cmd.AlertId, cmd.OrgId).Desc("created").Find(&states); err != nil { - return err - } - - if len(states) == 0 { - cmd.Result = nil - return nil - } - - cmd.Result = &states[0] - return nil -} - -func SetNewAlertState(cmd *m.UpdateAlertStateCommand) error { - return inTransaction(func(sess *xorm.Session) error { - if !cmd.IsValidState() { - return fmt.Errorf("new state is invalid") - } - - alert := m.Alert{} - has, err := sess.Id(cmd.AlertId).Get(&alert) - if err != nil { - return err - } - - if !has { - return fmt.Errorf("Could not find alert") - } - - alert.State = cmd.State - sess.Id(alert.Id).Update(&alert) - - alertState := m.AlertState{ - AlertId: cmd.AlertId, - OrgId: cmd.OrgId, - State: cmd.State, - Info: cmd.Info, - Created: time.Now(), - } - - sess.Insert(&alertState) - - cmd.Result = &alert - return nil - }) -} - -func GetAlertStateLogByAlertId(cmd *m.GetAlertsStateQuery) error { - states := make([]m.AlertState, 0) - - if err := x.Where("alert_id = ?", cmd.AlertId).Desc("created").Find(&states); err != nil { - return err - } - - cmd.Result = &states - return nil -} +// import ( +// "fmt" +// "time" +// +// "github.com/go-xorm/xorm" +// "github.com/grafana/grafana/pkg/bus" +// m "github.com/grafana/grafana/pkg/models" +// ) +// +// func init() { +// bus.AddHandler("sql", SetNewAlertState) +// bus.AddHandler("sql", GetAlertStateLogByAlertId) +// bus.AddHandler("sql", GetLastAlertStateQuery) +// } +// +// func GetLastAlertStateQuery(cmd *m.GetLastAlertStateQuery) error { +// states := make([]m.AlertState, 0) +// +// if err := x.Where("alert_id = ? and org_id = ? ", cmd.AlertId, cmd.OrgId).Desc("created").Find(&states); err != nil { +// return err +// } +// +// if len(states) == 0 { +// cmd.Result = nil +// return nil +// } +// +// cmd.Result = &states[0] +// return nil +// } +// +// func SetNewAlertState(cmd *m.UpdateAlertStateCommand) error { +// return inTransaction(func(sess *xorm.Session) error { +// if !cmd.IsValidState() { +// return fmt.Errorf("new state is invalid") +// } +// +// alert := m.Alert{} +// has, err := sess.Id(cmd.AlertId).Get(&alert) +// if err != nil { +// return err +// } +// +// if !has { +// return fmt.Errorf("Could not find alert") +// } +// +// alert.State = cmd.State +// sess.Id(alert.Id).Update(&alert) +// +// alertState := m.AlertState{ +// AlertId: cmd.AlertId, +// OrgId: cmd.OrgId, +// State: cmd.State, +// Info: cmd.Info, +// Created: time.Now(), +// } +// +// sess.Insert(&alertState) +// +// cmd.Result = &alert +// return nil +// }) +// } +// +// func GetAlertStateLogByAlertId(cmd *m.GetAlertsStateQuery) error { +// states := make([]m.AlertState, 0) +// +// if err := x.Where("alert_id = ?", cmd.AlertId).Desc("created").Find(&states); err != nil { +// return err +// } +// +// cmd.Result = &states +// return nil +// } diff --git a/pkg/services/sqlstore/alert_state_test.go b/pkg/services/sqlstore/alert_state_test.go index 2fbf652353c..1db1fca2b78 100644 --- a/pkg/services/sqlstore/alert_state_test.go +++ b/pkg/services/sqlstore/alert_state_test.go @@ -1,100 +1,100 @@ package sqlstore -import ( - "testing" - - m "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" -) - -func TestAlertingStateAccess(t *testing.T) { - Convey("Test alerting state changes", t, func() { - InitTestDB(t) - - testDash := insertTestDashboard("dashboard with alerts", 1, "alert") - - items := []*m.Alert{ - { - PanelId: 1, - DashboardId: testDash.Id, - OrgId: testDash.OrgId, - Name: "Alerting title", - Description: "Alerting description", - }, - } - - cmd := m.SaveAlertsCommand{ - Alerts: items, - DashboardId: testDash.Id, - OrgId: 1, - UserId: 1, - } - - err := SaveAlerts(&cmd) - So(err, ShouldBeNil) - - Convey("Cannot insert invalid states", func() { - err = SetNewAlertState(&m.UpdateAlertStateCommand{ - AlertId: 1, - NewState: "maybe ok", - Info: "Shit just hit the fan", - }) - - So(err, ShouldNotBeNil) - }) - - Convey("Changes state to alert", func() { - - err = SetNewAlertState(&m.UpdateAlertStateCommand{ - AlertId: 1, - NewState: "CRITICAL", - Info: "Shit just hit the fan", - }) - - Convey("can get new state for alert", func() { - query := &m.GetAlertByIdQuery{Id: 1} - err := GetAlertById(query) - So(err, ShouldBeNil) - So(query.Result.State, ShouldEqual, "CRITICAL") - }) - - Convey("Changes state to ok", func() { - err = SetNewAlertState(&m.UpdateAlertStateCommand{ - AlertId: 1, - NewState: "OK", - Info: "Shit just hit the fan", - }) - - Convey("get ok state for alert", func() { - query := &m.GetAlertByIdQuery{Id: 1} - err := GetAlertById(query) - So(err, ShouldBeNil) - So(query.Result.State, ShouldEqual, "OK") - }) - - Convey("should have two event state logs", func() { - query := &m.GetAlertsStateQuery{ - AlertId: 1, - OrgId: 1, - } - - err := GetAlertStateLogByAlertId(query) - So(err, ShouldBeNil) - - So(len(*query.Result), ShouldEqual, 2) - }) - - Convey("should not get any alerts with critical state", func() { - query := &m.GetAlertsQuery{ - OrgId: 1, - State: []string{"Critical", "Warn"}, - } - - err := HandleAlertsQuery(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 0) - }) - }) - }) - }) -} +// import ( +// "testing" +// +// m "github.com/grafana/grafana/pkg/models" +// . "github.com/smartystreets/goconvey/convey" +// ) +// +// func TestAlertingStateAccess(t *testing.T) { +// Convey("Test alerting state changes", t, func() { +// InitTestDB(t) +// +// testDash := insertTestDashboard("dashboard with alerts", 1, "alert") +// +// items := []*m.Alert{ +// { +// PanelId: 1, +// DashboardId: testDash.Id, +// OrgId: testDash.OrgId, +// Name: "Alerting title", +// Description: "Alerting description", +// }, +// } +// +// cmd := m.SaveAlertsCommand{ +// Alerts: items, +// DashboardId: testDash.Id, +// OrgId: 1, +// UserId: 1, +// } +// +// err := SaveAlerts(&cmd) +// So(err, ShouldBeNil) +// +// Convey("Cannot insert invalid states", func() { +// err = SetNewAlertState(&m.UpdateAlertStateCommand{ +// AlertId: 1, +// NewState: "maybe ok", +// Info: "Shit just hit the fan", +// }) +// +// So(err, ShouldNotBeNil) +// }) +// +// Convey("Changes state to alert", func() { +// +// err = SetNewAlertState(&m.UpdateAlertStateCommand{ +// AlertId: 1, +// NewState: "CRITICAL", +// Info: "Shit just hit the fan", +// }) +// +// Convey("can get new state for alert", func() { +// query := &m.GetAlertByIdQuery{Id: 1} +// err := GetAlertById(query) +// So(err, ShouldBeNil) +// So(query.Result.State, ShouldEqual, "CRITICAL") +// }) +// +// Convey("Changes state to ok", func() { +// err = SetNewAlertState(&m.UpdateAlertStateCommand{ +// AlertId: 1, +// NewState: "OK", +// Info: "Shit just hit the fan", +// }) +// +// Convey("get ok state for alert", func() { +// query := &m.GetAlertByIdQuery{Id: 1} +// err := GetAlertById(query) +// So(err, ShouldBeNil) +// So(query.Result.State, ShouldEqual, "OK") +// }) +// +// Convey("should have two event state logs", func() { +// query := &m.GetAlertsStateQuery{ +// AlertId: 1, +// OrgId: 1, +// } +// +// err := GetAlertStateLogByAlertId(query) +// So(err, ShouldBeNil) +// +// So(len(*query.Result), ShouldEqual, 2) +// }) +// +// Convey("should not get any alerts with critical state", func() { +// query := &m.GetAlertsQuery{ +// OrgId: 1, +// State: []string{"Critical", "Warn"}, +// } +// +// err := HandleAlertsQuery(query) +// So(err, ShouldBeNil) +// So(len(query.Result), ShouldEqual, 0) +// }) +// }) +// }) +// }) +// } diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts index 3bf6924bc93..e1d996f1013 100644 --- a/public/app/features/alerting/alert_def.ts +++ b/public/app/features/alerting/alert_def.ts @@ -1,16 +1,15 @@ /// -var alertStateToCssMap = { - "OK": "icon-gf-online alert-icon-online", - "WARN": "icon-gf-warn alert-icon-warn", - "CRITICAL": "icon-gf-critical alert-icon-critical", - "ACKNOWLEDGED": "icon-gf-alert-disabled" +var alertSeverityIconMap = { + "ok": "icon-gf-online alert-icon-online", + "warning": "icon-gf-warn alert-icon-warn", + "critical": "icon-gf-critical alert-icon-critical", }; -function getCssForState(alertState) { - return alertStateToCssMap[alertState]; +function getSeverityIconClass(alertState) { + return alertSeverityIconMap[alertState]; } export default { - getCssForState + getSeverityIconClass, }; diff --git a/public/app/features/alerting/alerts_ctrl.ts b/public/app/features/alerting/alerts_ctrl.ts index 7294ce69166..fd6134accfe 100644 --- a/public/app/features/alerting/alerts_ctrl.ts +++ b/public/app/features/alerting/alerts_ctrl.ts @@ -28,10 +28,9 @@ export class AlertListCtrl { updateFilter() { var stats = []; - this.filter.ok && stats.push('Ok'); + this.filter.ok && stats.push('OK'); this.filter.warn && stats.push('Warn'); this.filter.critical && stats.push('critical'); - this.filter.acknowleged && stats.push('acknowleged'); this.$route.current.params.state = stats; this.$route.updateParams(); @@ -40,10 +39,9 @@ export class AlertListCtrl { loadAlerts() { var stats = []; - this.filter.ok && stats.push('Ok'); + this.filter.ok && stats.push('OK'); this.filter.warn && stats.push('Warn'); this.filter.critical && stats.push('critical'); - this.filter.acknowleged && stats.push('acknowleged'); var params = { state: stats @@ -51,7 +49,8 @@ export class AlertListCtrl { this.backendSrv.get('/api/alerts', params).then(result => { this.alerts = _.map(result, alert => { - alert.iconCss = alertDef.getCssForState(alert.state); + alert.severityClass = alertDef.getSeverityClass(alert.severity); + alert.stateClass = alertDef.getStateClass(alert.state); return alert; }); }); diff --git a/public/app/features/alerting/partials/alert_list.html b/public/app/features/alerting/partials/alert_list.html index 29641aa6c21..420acd4b0bf 100644 --- a/public/app/features/alerting/partials/alert_list.html +++ b/public/app/features/alerting/partials/alert_list.html @@ -7,28 +7,29 @@
- + -
Status
+ +
Name StateSeverity
- + {{alert.name}} - - - + {{alert.state}} + + {{alert.severity}} diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 6ac835bda47..559e9c6b78e 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -46,8 +46,8 @@ export class AlertTabCtrl { {text: '<', value: '<'}, ]; severityLevels = [ - {text: 'Critical', value: 'CRITICAL'}, - {text: 'Warning', value: 'WARN'}, + {text: 'Critical', value: 'critical'}, + {text: 'Warning', value: 'warning'}, ]; /** @ngInject */ diff --git a/public/sass/components/_tags.scss b/public/sass/components/_tags.scss index 821e372e659..c00328c49c4 100644 --- a/public/sass/components/_tags.scss +++ b/public/sass/components/_tags.scss @@ -34,7 +34,7 @@ } .label-tag:hover { - opacity: 0.85; + opacity: 0.85; background-color: darken($purple, 10%); } From a6c609477577d7d95bcc43d684647bf01835f06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 22 Jul 2016 16:45:17 +0200 Subject: [PATCH 17/22] feat(alerting): started reworking notifications --- ' | 200 ------------------ pkg/api/alerting.go | 22 +- pkg/api/dtos/alerting.go | 4 +- pkg/models/alert_notifications.go | 46 ++-- pkg/models/annotations.go | 22 ++ pkg/services/sqlstore/alert_notification.go | 119 ++++------- .../alerting/notification_edit_ctrl.ts | 5 +- .../alerting/partials/notification_edit.html | 23 +- 8 files changed, 102 insertions(+), 339 deletions(-) delete mode 100644 ' create mode 100644 pkg/models/annotations.go diff --git a/' b/' deleted file mode 100644 index 2e342921a63..00000000000 --- a/' +++ /dev/null @@ -1,200 +0,0 @@ - -/** Created by: Alex Wendland (me@alexwendland.com), 2014-08-06 - * - * angular-json-tree - * - * Directive for creating a tree-view out of a JS Object. Only loads - * sub-nodes on demand in order to improve performance of rendering large - * objects. - * - * Attributes: - * - object (Object, 2-way): JS object to build the tree from - * - start-expanded (Boolean, 1-way, ?=true): should the tree default to expanded - * - * Usage: - * // In the controller - * scope.someObject = { - * test: 'hello', - * array: [1,1,2,3,5,8] - * }; - * // In the html - * - * - * Dependencies: - * - utils (json-tree.js) - * - ajsRecursiveDirectiveHelper (json-tree.js) - * - * Test: json-tree-test.js - */ - -import angular from 'angular'; -import coreModule from 'app/core/core_module'; - -var utils = { - /* See link for possible type values to check against. - * http://stackoverflow.com/questions/4622952/json-object-containing-array - * - * Value Class Type - * ------------------------------------- - * "foo" String string - * new String("foo") String object - * 1.2 Number number - * new Number(1.2) Number object - * true Boolean boolean - * new Boolean(true) Boolean object - * new Date() Date object - * new Error() Error object - * [1,2,3] Array object - * new Array(1, 2, 3) Array object - * new Function("") Function function - * /abc/g RegExp object (function in Nitro/V8) - * new RegExp("meow") RegExp object (function in Nitro/V8) - * {} Object object - * new Object() Object object - */ - is: function is(obj, clazz) { - return Object.prototype.toString.call(obj).slice(8, -1) === clazz; - }, - - // See above for possible values - whatClass: function whatClass(obj) { - return Object.prototype.toString.call(obj).slice(8, -1); - }, - - // Iterate over an objects keyset - forKeys: function forKeys(obj, f) { - for (var key in obj) { - if (obj.hasOwnProperty(key) && typeof obj[key] !== 'function') { - if (f(key, obj[key])) { - break; - } - } - } - } -}; - -coreModule.directive('jsonTree', [function jsonTreeDirective() { - return { - restrict: 'E', - scope: { - object: '=', - startExpanded: '=', - rootName: '@', - }, - template: '' - }; -}]); - -coreModule.directive('jsonNode', ['ajsRecursiveDirectiveHelper', function jsonNodeDirective(ajsRecursiveDirectiveHelper) { - return { - restrict: 'E', - scope: { - key: '=', - value: '=', - startExpanded: '=' - }, - compile: function jsonNodeDirectiveCompile(elem) { - return ajsRecursiveDirectiveHelper.compile(elem, this); - }, - template: ' {{key}}' + - ' {{value}}' + - ' ' + - ' {preview}}' + - '
    ' + - '
  • ' + - ' ' + - '
  • ' + - '
', - pre: function jsonNodeDirectiveLink(scope, elem, attrs) { - // Set value's type as Class for CSS styling - elem.addClass(utils.whatClass(scope.value).toLowerCase()); - // If the value is an Array or Object, use expandable view type - if (utils.is(scope.value, 'Object') || utils.is(scope.value, 'Array')) { - scope.isExpandable = true; - // Add expandable class for CSS usage - elem.addClass('expandable'); - // Setup preview text - var isArray = utils.is(scope.value, 'Array'); - scope.preview = isArray ? '[ ' : '{ '; - utils.forKeys(scope.value, function jsonNodeDirectiveLinkForKeys(key, value) { - if (isArray) { - scope.preview += value + ', '; - } else { - scope.preview += key + ': ' + value + ', '; - } - }); - scope.preview = scope.preview.substring(0, scope.preview.length - (scope.preview.length > 2 ? 2 : 0)) + (isArray ? ' ]' : ' }'); - // If directive initially has isExpanded set, also set shouldRender to true - if (scope.startExpanded) { - scope.shouldRender = true; - elem.addClass('expanded'); - } - // Setup isExpanded state handling - scope.isExpanded = scope.startExpanded ? scope.startExpanded() : false; - scope.toggleExpanded = function jsonNodeDirectiveToggleExpanded() { - scope.isExpanded = !scope.isExpanded; - if (scope.isExpanded) { - elem.addClass('expanded'); - } else { - elem.removeClass('expanded'); - } - // For delaying subnode render until requested - scope.shouldRender = true; - }; - } else { - scope.isExpandable = false; - // Add expandable class for CSS usage - elem.addClass('not-expandable'); - } - } - }; -}]); - -/** Added by: Alex Wendland (me@alexwendland.com), 2014-08-09 - * Source: http://stackoverflow.com/questions/14430655/recursion-in-angular-directives - * - * Used to allow for recursion within directives - */ -coreModule.factory('ajsRecursiveDirectiveHelper', ['$compile', function RecursiveDirectiveHelper($compile) { - return { - /** - * Manually compiles the element, fixing the recursion loop. - * @param element - * @param [link] A post-link function, or an object with function(s) registered via pre and post properties. - * @returns An object containing the linking functions. - */ - compile: function RecursiveDirectiveHelperCompile(element, link) { - // Normalize the link parameter - if (angular.isFunction(link)) { - link = { - post: link - }; - } - - // Break the recursion loop by removing the contents - var contents = element.contents().remove(); - var compiledContents; - return { - pre: (link && link.pre) ? link.pre : null, - /** - * Compiles and re-adds the contents - */ - post: function RecursiveDirectiveHelperCompilePost(scope, element) { - // Compile the contents - if (!compiledContents) { - compiledContents = $compile(contents); - } - // Re-add the compiled contents to the element - compiledContents(scope, function (clone) { - element.append(clone); - }); - - // Call the post-linking function, if any - if (link && link.post) { - link.post.apply(null, arguments); - } - } - }; - } - }; -}]); diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index ef845337d7b..634e9650f01 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -39,10 +39,10 @@ func GetAlerts(c *middleware.Context) Response { } dashboardIds := make([]int64, 0) - alertDTOs := make([]*dtos.AlertRuleDTO, 0) + alertDTOs := make([]*dtos.AlertRule, 0) for _, alert := range query.Result { dashboardIds = append(dashboardIds, alert.DashboardId) - alertDTOs = append(alertDTOs, &dtos.AlertRuleDTO{ + alertDTOs = append(alertDTOs, &dtos.AlertRule{ Id: alert.Id, DashboardId: alert.DashboardId, PanelId: alert.PanelId, @@ -176,18 +176,16 @@ func DelAlert(c *middleware.Context) Response { // } func GetAlertNotifications(c *middleware.Context) Response { - query := &models.GetAlertNotificationQuery{ - OrgID: c.OrgId, - } + query := &models.GetAlertNotificationsQuery{OrgId: c.OrgId} if err := bus.Dispatch(query); err != nil { return ApiError(500, "Failed to get alert notifications", err) } - var result []dtos.AlertNotificationDTO + var result []dtos.AlertNotification for _, notification := range query.Result { - result = append(result, dtos.AlertNotificationDTO{ + result = append(result, dtos.AlertNotification{ Id: notification.Id, Name: notification.Name, Type: notification.Type, @@ -200,8 +198,8 @@ func GetAlertNotifications(c *middleware.Context) Response { } func GetAlertNotificationById(c *middleware.Context) Response { - query := &models.GetAlertNotificationQuery{ - OrgID: c.OrgId, + query := &models.GetAlertNotificationsQuery{ + OrgId: c.OrgId, Id: c.ParamsInt64("notificationId"), } @@ -213,7 +211,7 @@ func GetAlertNotificationById(c *middleware.Context) Response { } func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response { - cmd.OrgID = c.OrgId + cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to create alert notification", err) @@ -223,7 +221,7 @@ func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotifi } func UpdateAlertNotification(c *middleware.Context, cmd models.UpdateAlertNotificationCommand) Response { - cmd.OrgID = c.OrgId + cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to update alert notification", err) @@ -242,5 +240,5 @@ func DeleteAlertNotification(c *middleware.Context) Response { return ApiError(500, "Failed to delete alert notification", err) } - return Json(200, map[string]interface{}{"notificationId": cmd.Id}) + return ApiSuccess("Notification deleted") } diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 35fc3f9e638..91678600a4e 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -7,7 +7,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -type AlertRuleDTO struct { +type AlertRule struct { Id int64 `json:"id"` DashboardId int64 `json:"dashboardId"` PanelId int64 `json:"panelId"` @@ -19,7 +19,7 @@ type AlertRuleDTO struct { DashbboardUri string `json:"dashboardUri"` } -type AlertNotificationDTO struct { +type AlertNotification struct { Id int64 `json:"id"` Name string `json:"name"` Type string `json:"type"` diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 3ac23438b8e..464d6dc88da 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -7,34 +7,31 @@ import ( ) type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - AlwaysExecute bool `json:"alwaysExecute"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - AlwaysExecute bool `json:"alwaysExecute"` - OrgID int64 `json:"-"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + Settings *simplejson.Json `json:"settings"` + OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - AlwaysExecute bool `json:"alwaysExecute"` - OrgID int64 `json:"-"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + Settings *simplejson.Json `json:"settings" binding:"Required"` + OrgId int64 `json:"-"` Result *AlertNotification } @@ -43,12 +40,11 @@ type DeleteAlertNotificationCommand struct { OrgId int64 } -type GetAlertNotificationQuery struct { - Name string - Id int64 - Ids []int64 - OrgID int64 - IncludeAlwaysExecute bool +type GetAlertNotificationsQuery struct { + Name string + Id int64 + Ids []int64 + OrgId int64 Result []*AlertNotification } diff --git a/pkg/models/annotations.go b/pkg/models/annotations.go new file mode 100644 index 00000000000..149181fe81a --- /dev/null +++ b/pkg/models/annotations.go @@ -0,0 +1,22 @@ +package models + +import ( + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" +) + +type AnnotationType string + +type AnnotationEvent struct { + Id int64 + OrgId int64 + Type AnnotationType + Title string + Text string + AlertId int64 + UserId int64 + Timestamp time.Time + + Data *simplejson.Json +} diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 3f4b9a03406..8a3c8543f91 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -3,7 +3,6 @@ package sqlstore import ( "bytes" "fmt" - "strconv" "time" "github.com/go-xorm/xorm" @@ -31,11 +30,11 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { }) } -func AlertNotificationQuery(query *m.GetAlertNotificationQuery) error { +func AlertNotificationQuery(query *m.GetAlertNotificationsQuery) error { return getAlertNotifications(query, x.NewSession()) } -func getAlertNotifications(query *m.GetAlertNotificationQuery, sess *xorm.Session) error { +func getAlertNotifications(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error { var sql bytes.Buffer params := make([]interface{}, 0) @@ -43,16 +42,15 @@ func getAlertNotifications(query *m.GetAlertNotificationQuery, sess *xorm.Sessio alert_notification.id, alert_notification.org_id, alert_notification.name, - alert_notification.type, + alert_notification.type, alert_notification.created, - alert_notification.updated, - alert_notification.settings, - alert_notification.always_execute + alert_notification.updated, + alert_notification.settings FROM alert_notification `) sql.WriteString(` WHERE alert_notification.org_id = ?`) - params = append(params, query.OrgID) + params = append(params, query.OrgId) if query.Name != "" { sql.WriteString(` AND alert_notification.name = ?`) @@ -61,60 +59,26 @@ func getAlertNotifications(query *m.GetAlertNotificationQuery, sess *xorm.Sessio if query.Id != 0 { sql.WriteString(` AND alert_notification.id = ?`) - params = append(params, strconv.Itoa(int(query.Id))) + params = append(params, query.Id) } if len(query.Ids) > 0 { - sql.WriteString(` AND (`) - - for i, id := range query.Ids { - if i != 0 { - sql.WriteString(` OR`) - } - sql.WriteString(` alert_notification.id = ?`) - params = append(params, id) - } - - sql.WriteString(`)`) + sql.WriteString(` AND alert_notification.id IN (?)`) + params = append(params, query.Ids) } - var searches []*m.AlertNotification - if err := sess.Sql(sql.String(), params...).Find(&searches); err != nil { + results := make([]*m.AlertNotification, 0) + if err := sess.Sql(sql.String(), params...).Find(&results); err != nil { return err } - var result []*m.AlertNotification - var def []*m.AlertNotification - if query.IncludeAlwaysExecute { - - if err := sess.Where("org_id = ? AND always_execute = 1", query.OrgID).Find(&def); err != nil { - return err - } - - result = append(result, def...) - } - - for _, s := range searches { - canAppend := true - for _, d := range result { - if d.Id == s.Id { - canAppend = false - break - } - } - - if canAppend { - result = append(result, s) - } - } - - query.Result = result + query.Result = results return nil } func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error { return inTransaction(func(sess *xorm.Session) error { - existingQuery := &m.GetAlertNotificationQuery{OrgID: cmd.OrgID, Name: cmd.Name, IncludeAlwaysExecute: false} + existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name} err := getAlertNotifications(existingQuery, sess) if err != nil { @@ -126,18 +90,15 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgID, - Name: cmd.Name, - Type: cmd.Type, - Created: time.Now(), - Settings: cmd.Settings, - Updated: time.Now(), - AlwaysExecute: cmd.AlwaysExecute, + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + Created: time.Now(), + Updated: time.Now(), } - _, err = sess.Insert(alertNotification) - - if err != nil { + if _, err = sess.Insert(alertNotification); err != nil { return err } @@ -148,38 +109,34 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { return inTransaction(func(sess *xorm.Session) (err error) { - current := &m.AlertNotification{} - _, err = sess.Id(cmd.Id).Get(current) + current := m.AlertNotification{} - if err != nil { + if _, err = sess.Id(cmd.Id).Get(¤t); err != nil { return err } - alertNotification := &m.AlertNotification{ - Id: cmd.Id, - OrgId: cmd.OrgID, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - Updated: time.Now(), - Created: current.Created, - AlwaysExecute: cmd.AlwaysExecute, - } - - sess.UseBool("always_execute") - - var affected int64 - affected, err = sess.Id(alertNotification.Id).Update(alertNotification) - - if err != nil { + // check if name exists + sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name} + if err := getAlertNotifications(sameNameQuery, sess); err != nil { return err } - if affected == 0 { + if len(sameNameQuery.Result) > 0 && sameNameQuery.Result[0].Id != current.Id { + return fmt.Errorf("Alert notification name %s already exists", cmd.Name) + } + + current.Updated = time.Now() + current.Settings = cmd.Settings + current.Name = cmd.Name + current.Type = cmd.Type + + if affected, err := sess.Id(cmd.Id).Update(current); err != nil { + return err + } else if affected == 0 { return fmt.Errorf("Could not find alert notification") } - cmd.Result = alertNotification + cmd.Result = ¤t return nil }) } diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 43dceea8dc2..69e2e276862 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -15,10 +15,7 @@ export class AlertNotificationEditCtrl { this.loadNotification($routeParams.notificationId); } else { this.notification = { - settings: { - sendCrit: true, - sendWarn: true, - } + type: 'email', }; } } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index fea91e5045a..8ed471887d5 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -6,14 +6,14 @@

Alert notification

-
+
Name - +
Type -
+
@@ -48,9 +41,9 @@
+

Email addresses

- To - +
From 14eba30f63926b41ca9a48a095ab7a173718430e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 23 Jul 2016 11:50:48 +0200 Subject: [PATCH 18/22] feat(alerting): more work on notifications --- pkg/services/alerting/notifier.go | 386 +++++++++--------- .../alerting/transformers/aggregation.go | 71 ---- .../alerting/transformers/transformer.go | 7 - 3 files changed, 194 insertions(+), 270 deletions(-) delete mode 100644 pkg/services/alerting/transformers/aggregation.go delete mode 100644 pkg/services/alerting/transformers/transformer.go diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 5f8f723e246..4d7c27f86b4 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -1,195 +1,197 @@ package alerting -// type NotifierImpl struct { -// log log.Logger -// getNotifications func(orgId int64, notificationGroups []int64) []*Notification -// } -// -// func NewNotifier() *NotifierImpl { -// log := log.New("alerting.notifier") -// return &NotifierImpl{ -// log: log, -// getNotifications: buildGetNotifiers(log), -// } -// } +import ( + "fmt" + "log" + "strconv" -// func (n NotifierImpl) ShouldDispath(alertResult *AlertResultContext, notifier *Notification) bool { -// warn := alertResult.State == alertstates.Warn && notifier.SendWarning -// crit := alertResult.State == alertstates.Critical && notifier.SendCritical -// return (warn || crit) || alertResult.State == alertstates.Ok -// } -// -// func (n *NotifierImpl) Notify(alertResult *AlertResultContext) { -// notifiers := n.getNotifications(alertResult.Rule.OrgId, alertResult.Rule.Notifications) -// -// for _, notifier := range notifiers { -// if n.ShouldDispath(alertResult, notifier) { -// n.log.Info("Sending notification", "state", alertResult.State, "type", notifier.Type) -// go notifier.Notifierr.Dispatch(alertResult) -// } -// } -// } -// -// type Notification struct { -// Name string -// Type string -// SendWarning bool -// SendCritical bool -// -// Notifierr NotificationDispatcher -// } -// -// type EmailNotifier struct { -// To string -// log log.Logger -// } -// -// func (this *EmailNotifier) Dispatch(alertResult *AlertResult) { -// this.log.Info("Sending email") -// grafanaUrl := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) -// if setting.AppSubUrl != "" { -// grafanaUrl += "/" + setting.AppSubUrl -// } -// -// query := &m.GetDashboardsQuery{ -// DashboardIds: []int64{alertResult.AlertJob.Rule.DashboardId}, -// } -// -// if err := bus.Dispatch(query); err != nil { -// this.log.Error("Failed to load dashboard", "error", err) -// return -// } -// -// if len(query.Result) != 1 { -// this.log.Error("Can only support one dashboard", "result", len(query.Result)) -// return -// } -// -// dashboard := query.Result[0] -// -// panelId := strconv.Itoa(int(alertResult.AlertJob.Rule.PanelId)) -// -// //TODO: get from alertrule and transforms to seconds -// from := "1466169458375" -// to := "1466171258375" -// -// renderUrl := fmt.Sprintf("%s/render/dashboard-solo/db/%s?from=%s&to=%s&panelId=%s&width=1000&height=500", grafanaUrl, dashboard.Slug, from, to, panelId) -// cmd := &m.SendEmailCommand{ -// Data: map[string]interface{}{ -// "Name": "Name", -// "State": alertResult.State, -// "Description": alertResult.Description, -// "TriggeredAlerts": alertResult.TriggeredAlerts, -// "DashboardLink": grafanaUrl + "/dashboard/db/" + dashboard.Slug, -// "AlertPageUrl": grafanaUrl + "/alerting", -// "DashboardImage": renderUrl, -// }, -// To: []string{this.To}, -// Template: "alert_notification.html", -// } -// -// err := bus.Dispatch(cmd) -// if err != nil { -// this.log.Error("Could not send alert notification as email", "error", err) -// } -// } -// -// type WebhookNotifier struct { -// Url string -// User string -// Password string -// log log.Logger -// } -// -// func (this *WebhookNotifier) Dispatch(alertResult *AlertResultContext) { -// this.log.Info("Sending webhook") -// -// bodyJSON := simplejson.New() -// bodyJSON.Set("name", alertResult.AlertJob.Rule.Name) -// bodyJSON.Set("state", alertResult.State) -// bodyJSON.Set("trigged", alertResult.TriggeredAlerts) -// -// body, _ := bodyJSON.MarshalJSON() -// -// cmd := &m.SendWebhook{ -// Url: this.Url, -// User: this.User, -// Password: this.Password, -// Body: string(body), -// } -// -// bus.Dispatch(cmd) -// } -// -// type NotificationDispatcher interface { -// Dispatch(alertResult *AlertResult) -// } -// -// func buildGetNotifiers(log log.Logger) func(orgId int64, notificationGroups []int64) []*Notification { -// return func(orgId int64, notificationGroups []int64) []*Notification { -// query := &m.GetAlertNotificationQuery{ -// OrgID: orgId, -// Ids: notificationGroups, -// IncludeAlwaysExecute: true, -// } -// err := bus.Dispatch(query) -// if err != nil { -// log.Error("Failed to read notifications", "error", err) -// } -// -// var result []*Notification -// for _, notification := range query.Result { -// not, err := NewNotificationFromDBModel(notification) -// if err == nil { -// result = append(result, not) -// } else { -// log.Error("Failed to read notification model", "error", err) -// } -// } -// -// return result -// } -// } -// -// func NewNotificationFromDBModel(model *m.AlertNotification) (*Notification, error) { -// notifier, err := createNotifier(model.Type, model.Settings) -// -// if err != nil { -// return nil, err -// } -// -// return &Notification{ -// Name: model.Name, -// Type: model.Type, -// Notifierr: notifier, -// SendCritical: model.Settings.Get("sendCrit").MustBool(), -// SendWarning: model.Settings.Get("sendWarn").MustBool(), -// }, nil -// } -// -// var createNotifier = func(notificationType string, settings *simplejson.Json) (NotificationDispatcher, error) { -// if notificationType == "email" { -// to := settings.Get("to").MustString() -// -// if to == "" { -// return nil, fmt.Errorf("Could not find to propertie in settings") -// } -// -// return &EmailNotifier{ -// To: to, -// log: log.New("alerting.notification.email"), -// }, nil -// } -// -// url := settings.Get("url").MustString() -// if url == "" { -// return nil, fmt.Errorf("Could not find url propertie in settings") -// } -// -// return &WebhookNotifier{ -// Url: url, -// User: settings.Get("user").MustString(), -// Password: settings.Get("password").MustString(), -// log: log.New("alerting.notification.webhook"), -// }, nil -// } + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/setting" +) + +type NotifierImpl struct { + log log.Logger + getNotifications func(orgId int64, notificationGroups []int64) []*Notification +} + +func NewNotifier() *NotifierImpl { + log := log.New("alerting.notifier") + return &NotifierImpl{ + log: log, + getNotifications: buildGetNotifiers(log), + } +} + +func (n *NotifierImpl) Notify(alertResult *AlertResultContext) { + notifiers := n.getNotifications(alertResult.Rule.OrgId, alertResult.Rule.Notifications) + + for _, notifier := range notifiers { + n.log.Info("Sending notification", "state", alertResult.State, "type", notifier.Type) + go notifier.Notifierr.Dispatch(alertResult) + } +} + +type Notification struct { + Name string + Type string + SendWarning bool + SendCritical bool + + Notifierr NotificationDispatcher +} + +type EmailNotifier struct { + To string + log log.Logger +} + +func (this *EmailNotifier) Dispatch(alertResult *AlertResult) { + this.log.Info("Sending email") + grafanaUrl := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) + if setting.AppSubUrl != "" { + grafanaUrl += "/" + setting.AppSubUrl + } + + query := &m.GetDashboardsQuery{ + DashboardIds: []int64{alertResult.AlertJob.Rule.DashboardId}, + } + + if err := bus.Dispatch(query); err != nil { + this.log.Error("Failed to load dashboard", "error", err) + return + } + + if len(query.Result) != 1 { + this.log.Error("Can only support one dashboard", "result", len(query.Result)) + return + } + + dashboard := query.Result[0] + + panelId := strconv.Itoa(int(alertResult.AlertJob.Rule.PanelId)) + + //TODO: get from alertrule and transforms to seconds + from := "1466169458375" + to := "1466171258375" + + renderUrl := fmt.Sprintf("%s/render/dashboard-solo/db/%s?from=%s&to=%s&panelId=%s&width=1000&height=500", grafanaUrl, dashboard.Slug, from, to, panelId) + cmd := &m.SendEmailCommand{ + Data: map[string]interface{}{ + "Name": "Name", + "State": alertResult.State, + "Description": alertResult.Description, + "TriggeredAlerts": alertResult.TriggeredAlerts, + "DashboardLink": grafanaUrl + "/dashboard/db/" + dashboard.Slug, + "AlertPageUrl": grafanaUrl + "/alerting", + "DashboardImage": renderUrl, + }, + To: []string{this.To}, + Template: "alert_notification.html", + } + + err := bus.Dispatch(cmd) + if err != nil { + this.log.Error("Could not send alert notification as email", "error", err) + } +} + +type WebhookNotifier struct { + Url string + User string + Password string + log log.Logger +} + +func (this *WebhookNotifier) Dispatch(alertResult *AlertResultContext) { + this.log.Info("Sending webhook") + + bodyJSON := simplejson.New() + bodyJSON.Set("name", alertResult.AlertJob.Rule.Name) + bodyJSON.Set("state", alertResult.State) + bodyJSON.Set("trigged", alertResult.TriggeredAlerts) + + body, _ := bodyJSON.MarshalJSON() + + cmd := &m.SendWebhook{ + Url: this.Url, + User: this.User, + Password: this.Password, + Body: string(body), + } + + bus.Dispatch(cmd) +} + +type NotificationDispatcher interface { + Dispatch(alertResult *AlertResult) +} + +func buildGetNotifiers(log log.Logger) func(orgId int64, notificationGroups []int64) []*Notification { + return func(orgId int64, notificationGroups []int64) []*Notification { + query := &m.GetAlertNotificationQuery{ + OrgID: orgId, + Ids: notificationGroups, + IncludeAlwaysExecute: true, + } + err := bus.Dispatch(query) + if err != nil { + log.Error("Failed to read notifications", "error", err) + } + + var result []*Notification + for _, notification := range query.Result { + not, err := NewNotificationFromDBModel(notification) + if err == nil { + result = append(result, not) + } else { + log.Error("Failed to read notification model", "error", err) + } + } + + return result + } +} + +func NewNotificationFromDBModel(model *m.AlertNotification) (*Notification, error) { + notifier, err := createNotifier(model.Type, model.Settings) + + if err != nil { + return nil, err + } + + return &Notification{ + Name: model.Name, + Type: model.Type, + Notifierr: notifier, + SendCritical: model.Settings.Get("sendCrit").MustBool(), + SendWarning: model.Settings.Get("sendWarn").MustBool(), + }, nil +} + +var createNotifier = func(notificationType string, settings *simplejson.Json) (NotificationDispatcher, error) { + if notificationType == "email" { + to := settings.Get("to").MustString() + + if to == "" { + return nil, fmt.Errorf("Could not find to propertie in settings") + } + + return &EmailNotifier{ + To: to, + log: log.New("alerting.notification.email"), + }, nil + } + + url := settings.Get("url").MustString() + if url == "" { + return nil, fmt.Errorf("Could not find url propertie in settings") + } + + return &WebhookNotifier{ + Url: url, + User: settings.Get("user").MustString(), + Password: settings.Get("password").MustString(), + log: log.New("alerting.notification.webhook"), + }, nil +} diff --git a/pkg/services/alerting/transformers/aggregation.go b/pkg/services/alerting/transformers/aggregation.go deleted file mode 100644 index b9f77a3ee96..00000000000 --- a/pkg/services/alerting/transformers/aggregation.go +++ /dev/null @@ -1,71 +0,0 @@ -package transformers - -import ( - "fmt" - "math" - - "github.com/grafana/grafana/pkg/tsdb" -) - -func NewAggregationTransformer(method string) *AggregationTransformer { - return &AggregationTransformer{ - Method: method, - } -} - -type AggregationTransformer struct { - Method string -} - -func (at *AggregationTransformer) Transform(timeserie *tsdb.TimeSeries) (float64, error) { - - if at.Method == "avg" { - sum := float64(0) - for _, point := range timeserie.Points { - sum += point[0] - } - - return sum / float64(len(timeserie.Points)), nil - } - - if at.Method == "sum" { - sum := float64(0) - - for _, v := range timeserie.Points { - sum += v[0] - } - - return sum, nil - } - - if at.Method == "min" { - min := timeserie.Points[0][0] - - for _, v := range timeserie.Points { - if v[0] < min { - min = v[0] - } - } - - return min, nil - } - - if at.Method == "max" { - max := timeserie.Points[0][0] - - for _, v := range timeserie.Points { - if v[0] > max { - max = v[0] - } - } - - return max, nil - } - - if at.Method == "mean" { - midPosition := int64(math.Floor(float64(len(timeserie.Points)) / float64(2))) - return timeserie.Points[midPosition][0], nil - } - - return float64(0), fmt.Errorf("Missing method") -} diff --git a/pkg/services/alerting/transformers/transformer.go b/pkg/services/alerting/transformers/transformer.go deleted file mode 100644 index bf2af42aeb8..00000000000 --- a/pkg/services/alerting/transformers/transformer.go +++ /dev/null @@ -1,7 +0,0 @@ -package transformers - -import "github.com/grafana/grafana/pkg/tsdb" - -type Transformer interface { - Transform(timeserie *tsdb.TimeSeries) (float64, error) -} From 8df558decea47a80c06c4a3f3e8041b2872d1e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 Jul 2016 13:52:17 +0200 Subject: [PATCH 19/22] feat(notifications): refactoring notification handling --- pkg/services/alerting/interfaces.go | 1 + pkg/services/alerting/notifier.go | 269 ++++++++---------- .../app/features/alerting/alert_log_ctrl.ts | 2 +- public/app/features/alerting/alerts_ctrl.ts | 3 +- 4 files changed, 117 insertions(+), 158 deletions(-) diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 7230bbfa036..773e02b7fbd 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -17,6 +17,7 @@ type Scheduler interface { type Notifier interface { Notify(alertResult *AlertResultContext) + GetType() string } type AlertCondition interface { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 4d7c27f86b4..de7c144a20e 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -1,197 +1,156 @@ package alerting import ( + "errors" "fmt" - "log" - "strconv" + "strings" "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" "github.com/grafana/grafana/pkg/setting" ) -type NotifierImpl struct { - log log.Logger - getNotifications func(orgId int64, notificationGroups []int64) []*Notification -} - -func NewNotifier() *NotifierImpl { - log := log.New("alerting.notifier") - return &NotifierImpl{ - log: log, - getNotifications: buildGetNotifiers(log), - } -} - -func (n *NotifierImpl) Notify(alertResult *AlertResultContext) { - notifiers := n.getNotifications(alertResult.Rule.OrgId, alertResult.Rule.Notifications) - - for _, notifier := range notifiers { - n.log.Info("Sending notification", "state", alertResult.State, "type", notifier.Type) - go notifier.Notifierr.Dispatch(alertResult) - } -} - -type Notification struct { - Name string - Type string - SendWarning bool - SendCritical bool - - Notifierr NotificationDispatcher -} - -type EmailNotifier struct { - To string +type RootNotifier struct { + NotifierBase log log.Logger } -func (this *EmailNotifier) Dispatch(alertResult *AlertResult) { - this.log.Info("Sending email") - grafanaUrl := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) - if setting.AppSubUrl != "" { - grafanaUrl += "/" + setting.AppSubUrl +func NewRootNotifier() *RootNotifier { + return &RootNotifier{ + log: log.New("alerting.notifier"), + } +} + +func (n *RootNotifier) Notify(context *AlertResultContext) { + notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications) + if err != nil { + n.log.Error("Failed to read notifications", "error", err) + return } - query := &m.GetDashboardsQuery{ - DashboardIds: []int64{alertResult.AlertJob.Rule.DashboardId}, + for _, notifier := range notifiers { + n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType()) + go notifier.Notify(context) } +} + +func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) { + query := &m.GetAlertNotificationsQuery{OrgId: orgId, Ids: notificationIds} if err := bus.Dispatch(query); err != nil { + return nil, err + } + + var result []Notifier + for _, notification := range query.Result { + if not, err := NewNotificationFromDBModel(notification); err != nil { + return nil, err + } else { + result = append(result, not) + } + } + + return result, nil +} + +type NotifierBase struct { + Name string + Type string +} + +func (n *NotifierBase) GetType() string { + return n.Type +} + +type EmailNotifier struct { + NotifierBase + Addresses []string + log log.Logger +} + +func (this *EmailNotifier) Notify(context *AlertResultContext) { + this.log.Info("Sending alert notification to %v", this.Addresses) + + slugQuery := &m.GetDashboardSlugByIdQuery{Id: context.Rule.DashboardId} + if err := bus.Dispatch(slugQuery); err != nil { this.log.Error("Failed to load dashboard", "error", err) return } + dashboardSlug := slugQuery.Result - if len(query.Result) != 1 { - this.log.Error("Can only support one dashboard", "result", len(query.Result)) - return - } - - dashboard := query.Result[0] - - panelId := strconv.Itoa(int(alertResult.AlertJob.Rule.PanelId)) - - //TODO: get from alertrule and transforms to seconds - from := "1466169458375" - to := "1466171258375" - - renderUrl := fmt.Sprintf("%s/render/dashboard-solo/db/%s?from=%s&to=%s&panelId=%s&width=1000&height=500", grafanaUrl, dashboard.Slug, from, to, panelId) cmd := &m.SendEmailCommand{ Data: map[string]interface{}{ - "Name": "Name", - "State": alertResult.State, - "Description": alertResult.Description, - "TriggeredAlerts": alertResult.TriggeredAlerts, - "DashboardLink": grafanaUrl + "/dashboard/db/" + dashboard.Slug, - "AlertPageUrl": grafanaUrl + "/alerting", - "DashboardImage": renderUrl, + "RuleName": context.Rule.Name, + "Severity": context.Rule.Severity, + "RuleLink": setting.ToAbsUrl("dashboard/db/" + dashboardSlug), }, - To: []string{this.To}, + To: this.Addresses, Template: "alert_notification.html", } err := bus.Dispatch(cmd) if err != nil { - this.log.Error("Could not send alert notification as email", "error", err) + this.log.Error("Failed tosend alert notification email", "error", err) } } -type WebhookNotifier struct { - Url string - User string - Password string - log log.Logger -} +// type WebhookNotifier struct { +// Url string +// User string +// Password string +// log log.Logger +// } +// +// func (this *WebhookNotifier) Dispatch(context *AlertResultContext) { +// this.log.Info("Sending webhook") +// +// bodyJSON := simplejson.New() +// bodyJSON.Set("name", context.AlertJob.Rule.Name) +// bodyJSON.Set("state", context.State) +// bodyJSON.Set("trigged", context.TriggeredAlerts) +// +// body, _ := bodyJSON.MarshalJSON() +// +// cmd := &m.SendWebhook{ +// Url: this.Url, +// User: this.User, +// Password: this.Password, +// Body: string(body), +// } +// +// bus.Dispatch(cmd) +// } -func (this *WebhookNotifier) Dispatch(alertResult *AlertResultContext) { - this.log.Info("Sending webhook") +func NewNotificationFromDBModel(model *m.AlertNotification) (Notifier, error) { + if model.Type == "email" { + addressesString := model.Settings.Get("addresses").MustString() - bodyJSON := simplejson.New() - bodyJSON.Set("name", alertResult.AlertJob.Rule.Name) - bodyJSON.Set("state", alertResult.State) - bodyJSON.Set("trigged", alertResult.TriggeredAlerts) - - body, _ := bodyJSON.MarshalJSON() - - cmd := &m.SendWebhook{ - Url: this.Url, - User: this.User, - Password: this.Password, - Body: string(body), - } - - bus.Dispatch(cmd) -} - -type NotificationDispatcher interface { - Dispatch(alertResult *AlertResult) -} - -func buildGetNotifiers(log log.Logger) func(orgId int64, notificationGroups []int64) []*Notification { - return func(orgId int64, notificationGroups []int64) []*Notification { - query := &m.GetAlertNotificationQuery{ - OrgID: orgId, - Ids: notificationGroups, - IncludeAlwaysExecute: true, - } - err := bus.Dispatch(query) - if err != nil { - log.Error("Failed to read notifications", "error", err) - } - - var result []*Notification - for _, notification := range query.Result { - not, err := NewNotificationFromDBModel(notification) - if err == nil { - result = append(result, not) - } else { - log.Error("Failed to read notification model", "error", err) - } - } - - return result - } -} - -func NewNotificationFromDBModel(model *m.AlertNotification) (*Notification, error) { - notifier, err := createNotifier(model.Type, model.Settings) - - if err != nil { - return nil, err - } - - return &Notification{ - Name: model.Name, - Type: model.Type, - Notifierr: notifier, - SendCritical: model.Settings.Get("sendCrit").MustBool(), - SendWarning: model.Settings.Get("sendWarn").MustBool(), - }, nil -} - -var createNotifier = func(notificationType string, settings *simplejson.Json) (NotificationDispatcher, error) { - if notificationType == "email" { - to := settings.Get("to").MustString() - - if to == "" { - return nil, fmt.Errorf("Could not find to propertie in settings") + if addressesString == "" { + return nil, fmt.Errorf("Could not find addresses in settings") } return &EmailNotifier{ - To: to, - log: log.New("alerting.notification.email"), + NotifierBase: NotifierBase{ + Name: model.Name, + Type: model.Type, + }, + Addresses: strings.Split(addressesString, "\n"), + log: log.New("alerting.notification.email"), }, nil } - url := settings.Get("url").MustString() - if url == "" { - return nil, fmt.Errorf("Could not find url propertie in settings") - } + return nil, errors.New("Unsupported notification type") - return &WebhookNotifier{ - Url: url, - User: settings.Get("user").MustString(), - Password: settings.Get("password").MustString(), - log: log.New("alerting.notification.webhook"), - }, nil + // url := settings.Get("url").MustString() + // if url == "" { + // return nil, fmt.Errorf("Could not find url propertie in settings") + // } + // + // return &WebhookNotifier{ + // Url: url, + // User: settings.Get("user").MustString(), + // Password: settings.Get("password").MustString(), + // log: log.New("alerting.notification.webhook"), + // }, nil } diff --git a/public/app/features/alerting/alert_log_ctrl.ts b/public/app/features/alerting/alert_log_ctrl.ts index a9a3788c686..5ef79b9e53f 100644 --- a/public/app/features/alerting/alert_log_ctrl.ts +++ b/public/app/features/alerting/alert_log_ctrl.ts @@ -22,7 +22,7 @@ export class AlertLogCtrl { loadAlertLogs(alertId: number) { this.backendSrv.get(`/api/alerts/${alertId}/states`).then(result => { this.alertLogs = _.map(result, log => { - log.iconCss = alertDef.getCssForState(log.state); + log.iconCss = alertDef.getSeverityIconClass(log.severity); log.humanTime = moment(log.created).format("YYYY-MM-DD HH:mm:ss"); return log; }); diff --git a/public/app/features/alerting/alerts_ctrl.ts b/public/app/features/alerting/alerts_ctrl.ts index fd6134accfe..8adc0e105cc 100644 --- a/public/app/features/alerting/alerts_ctrl.ts +++ b/public/app/features/alerting/alerts_ctrl.ts @@ -49,8 +49,7 @@ export class AlertListCtrl { this.backendSrv.get('/api/alerts', params).then(result => { this.alerts = _.map(result, alert => { - alert.severityClass = alertDef.getSeverityClass(alert.severity); - alert.stateClass = alertDef.getStateClass(alert.state); + alert.severityClass = alertDef.getSeverityIconClass(alert.severity); return alert; }); }); From 6cb1dafb1d814eff942477f6e8cbb7b9481dc0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 Jul 2016 16:26:28 +0200 Subject: [PATCH 20/22] feat(alerting): progress on notifications --- pkg/services/sqlstore/migrations/alert_mig.go | 1 - public/app/core/routes/routes.ts | 2 +- public/app/features/alerting/alerts_ctrl.ts | 1 - .../alerting/notification_edit_ctrl.ts | 44 +++--- .../alerting/notifications_list_ctrl.ts | 14 +- .../alerting/partials/alert_list.html | 2 +- .../alerting/partials/notification_edit.html | 23 +-- .../alerting/partials/notifications_list.html | 6 +- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 27 +++- .../panel/graph/partials/tab_alerting.html | 141 +++++++++--------- 10 files changed, 137 insertions(+), 124 deletions(-) diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index a043451cf1b..a228f186da9 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -65,7 +65,6 @@ func addAlertMigrations(mg *Migrator) { {Name: "org_id", Type: DB_BigInt, Nullable: false}, {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, - {Name: "always_execute", Type: DB_Bool, Nullable: false}, {Name: "settings", Type: DB_Text, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index ba79398cb72..85bf7334042 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -211,7 +211,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', resolve: loadAlertingBundle, }) - .when('/alerting/notification/:notificationId/edit', { + .when('/alerting/notification/:id/edit', { templateUrl: 'public/app/features/alerting/partials/notification_edit.html', controller: 'AlertNotificationEditCtrl', controllerAs: 'ctrl', diff --git a/public/app/features/alerting/alerts_ctrl.ts b/public/app/features/alerting/alerts_ctrl.ts index 8adc0e105cc..17d048cee85 100644 --- a/public/app/features/alerting/alerts_ctrl.ts +++ b/public/app/features/alerting/alerts_ctrl.ts @@ -27,7 +27,6 @@ export class AlertListCtrl { updateFilter() { var stats = []; - this.filter.ok && stats.push('OK'); this.filter.warn && stats.push('Warn'); this.filter.critical && stats.push('critical'); diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 69e2e276862..0b959945215 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -6,49 +6,39 @@ import coreModule from '../../core/core_module'; import config from 'app/core/config'; export class AlertNotificationEditCtrl { - - notification: any; + model: any; /** @ngInject */ - constructor(private $routeParams, private backendSrv, private $scope) { - if ($routeParams.notificationId) { - this.loadNotification($routeParams.notificationId); + constructor(private $routeParams, private backendSrv, private $scope, private $location) { + if ($routeParams.id) { + this.loadNotification($routeParams.id); } else { - this.notification = { + this.model = { type: 'email', + settings: {} }; } } - loadNotification(notificationId) { - this.backendSrv.get(`/api/alert-notifications/${notificationId}`).then(result => { - console.log(result); - this.notification = result; + loadNotification(id) { + this.backendSrv.get(`/api/alert-notifications/${id}`).then(result => { + this.model = result; }); } isNew() { - return this.notification === undefined || this.notification.id === undefined; + return this.model.id === undefined; } save() { - if (this.notification.id) { - console.log('this.notification: ', this.notification); - this.backendSrv.put(`/api/alert-notifications/${this.notification.id}`, this.notification) - .then(result => { - this.notification = result; - this.$scope.appEvent('alert-success', ['Notification created!', '']); - }, () => { - this.$scope.appEvent('alert-error', ['Unable to create notification.', '']); - }); + if (this.model.id) { + this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => { + this.model = res; + }); } else { - this.backendSrv.post(`/api/alert-notifications`, this.notification) - .then(result => { - this.notification = result; - this.$scope.appEvent('alert-success', ['Notification updated!', '']); - }, () => { - this.$scope.appEvent('alert-error', ['Unable to update notification.', '']); - }); + this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => { + this.$location.path('alerting/notification/' + res.id + '/edit'); + }); } } } diff --git a/public/app/features/alerting/notifications_list_ctrl.ts b/public/app/features/alerting/notifications_list_ctrl.ts index d5a05b3edca..184d829f69d 100644 --- a/public/app/features/alerting/notifications_list_ctrl.ts +++ b/public/app/features/alerting/notifications_list_ctrl.ts @@ -20,16 +20,12 @@ export class AlertNotificationsListCtrl { }); } - deleteNotification(notificationId) { - this.backendSrv.delete(`/api/alerts-notification/${notificationId}`) - .then(() => { - this.notifications = this.notifications.filter(notification => { - return notification.id !== notificationId; - }); - this.$scope.appEvent('alert-success', ['Notification deleted', '']); - }, () => { - this.$scope.appEvent('alert-error', ['Unable to delete notification', '']); + deleteNotification(id) { + this.backendSrv.delete(`/api/alert-notifications/${id}`).then(() => { + this.notifications = this.notifications.filter(notification => { + return notification.id !== notificationId; }); + }); } } diff --git a/public/app/features/alerting/partials/alert_list.html b/public/app/features/alerting/partials/alert_list.html index 420acd4b0bf..dbae224a0cc 100644 --- a/public/app/features/alerting/partials/alert_list.html +++ b/public/app/features/alerting/partials/alert_list.html @@ -1,4 +1,4 @@ - +
diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 8ed471887d5..a963bd348a3 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -1,4 +1,8 @@ - + + + + Notifications +
@@ -9,13 +13,13 @@
Name - +
Type
@@ -23,27 +27,28 @@
-
+

Webhook settings

Url - +
Username - +
Password - +
-
+ +

Email addresses

- +
diff --git a/public/app/features/alerting/partials/notifications_list.html b/public/app/features/alerting/partials/notifications_list.html index 10f45571c31..b46f23fe2a8 100644 --- a/public/app/features/alerting/partials/notifications_list.html +++ b/public/app/features/alerting/partials/notifications_list.html @@ -1,4 +1,8 @@ - + + + + Notifications +
diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 559e9c6b78e..fae5a4ca8ff 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -49,14 +49,17 @@ export class AlertTabCtrl { {text: 'Critical', value: 'critical'}, {text: 'Warning', value: 'warning'}, ]; + addNotificationSegment; /** @ngInject */ - constructor($scope, private $timeout, private backendSrv, private dashboardSrv) { + constructor($scope, private $timeout, private backendSrv, private dashboardSrv, private uiSegmentSrv) { this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; $scope.ctrl = this; this.metricTargets = this.panel.targets.map(val => val); + this.addNotificationSegment = uiSegmentSrv.newPlusButton(); + this.initModel(); // set panel alert edit mode @@ -66,6 +69,28 @@ export class AlertTabCtrl { }); } + getNotifications() { + return this.backendSrv.get('/api/alert-notifications').then(res => { + return res.map(item => { + return this.uiSegmentSrv.newSegment(item.name); + }); + }); + } + + notificationAdded() { + this.alert.notifications.push({ + name: this.addNotificationSegment.value + }); + + // reset plus button + this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value; + this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html; + } + + removeNotification(index) { + this.alert.notifications.splice(index, 1); + } + initModel() { var alert = this.alert = this.panel.alert = this.panel.alert || {}; diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index c5b85f5de59..41edfe6aaad 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -24,53 +24,33 @@
-
-
Alert Rule
-
-
- Name - -
- - - - - - - - - -
- Evaluate every - -
-
-
-
- Notifications - -
- -
- Severity -
- -
-
-
-
+
+
Alert Rule
+
+
+ Name + +
+
+ Evaluate every + +
+
+ Severity +
+ +
+
+
+
Conditions
- AND + AND + WHEN
- When Value + Value
@@ -98,48 +78,63 @@ -
+
-
-
-
- - -
+
+
Notifications
+
+
+ + {{nc.name}} + + + +
+
+
- +
+
+ + + +
+
- -
-
- Evaluating rule + Evaluating rule
- +
-
- -
+
+ +
From 0d9b98da6d4dd26ef6895ba711fde1c3428c18a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 26 Jul 2016 12:29:52 +0200 Subject: [PATCH 21/22] feat(alerting): progress on email notifications --- emails/templates/alert_notification.html | 33 +++---------- pkg/api/alerting.go | 2 +- pkg/api/dtos/alerting.go | 2 +- pkg/log/log.go | 4 +- pkg/services/alerting/alert_rule.go | 9 +++- pkg/services/alerting/alert_rule_test.go | 12 ++++- pkg/services/alerting/handler.go | 4 +- pkg/services/alerting/models.go | 4 +- pkg/services/alerting/notifier.go | 16 ++++--- pkg/services/alerting/reader.go | 21 ++++---- pkg/services/alerting/result_handler.go | 6 +-- pkg/services/sqlstore/alert_notification.go | 7 ++- public/app/features/dashboard/viewStateSrv.js | 5 ++ public/app/features/panel/panel_ctrl.ts | 8 ++-- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 48 +++++++++++++------ public/app/plugins/panel/graph/module.ts | 2 +- .../panel/graph/partials/tab_alerting.html | 2 +- public/emails/alert_notification.html | 33 +++---------- public/emails/invited_to_org.html | 2 +- public/emails/new_user_invite.html | 2 +- public/emails/signup_started.html | 2 +- 21 files changed, 118 insertions(+), 106 deletions(-) diff --git a/emails/templates/alert_notification.html b/emails/templates/alert_notification.html index 3955c3dbf1e..94d405bbb9d 100644 --- a/emails/templates/alert_notification.html +++ b/emails/templates/alert_notification.html @@ -1,31 +1,12 @@ - +[[Subject .Subject "Grafana Alert: [[.Severity]] [[.RuleName]]"]] -[[Subject .Subject "Grafana Alert: [ [[.State]] ] [[.Name]]" ]] +
+
-Alertstate: [[.State]]
-[[.AlertPageUrl]]
-[[.DashboardLink]]
-[[.Description]]
+Alert rule: [[.RuleName]]
+Alert state: [[.RuleState]]
-[[if eq .State "Ok"]] - Everything is Ok -[[end]] +Link to alert rule - +
-[[if ne .State "Ok" ]] - - - - - - - [[ range $ta := .TriggeredAlerts]] - - - - - - [[end]] -
SerieStateActual value
[[$ta.Name]][[$ta.State]][[$ta.ActualValue]]
-[[end]] diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 634e9650f01..7339485787f 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -104,7 +104,7 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response { dtoRes.Logs = append(dtoRes.Logs, &dtos.AlertTestResultLog{Message: log.Message, Data: log.Data}) } - dtoRes.Timing = fmt.Sprintf("%1.3fs", res.GetDurationSeconds()) + dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs()) return Json(200, dtoRes) } diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 91678600a4e..244d2bede66 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -34,7 +34,7 @@ type AlertTestCommand struct { type AlertTestResult struct { Firing bool `json:"firing"` - Timing string `json:"timing"` + TimeMs string `json:"timeMs"` Error string `json:"error,omitempty"` Logs []*AlertTestResultLog `json:"logs,omitempty"` } diff --git a/pkg/log/log.go b/pkg/log/log.go index 20fca9092d5..34a2aed4762 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -116,7 +116,9 @@ func getFilters(filterStrArray []string) map[string]log15.Lvl { for _, filterStr := range filterStrArray { parts := strings.Split(filterStr, ":") - filterMap[parts[0]] = getLogLevelFromString(parts[1]) + if len(parts) > 1 { + filterMap[parts[0]] = getLogLevelFromString(parts[1]) + } } return filterMap diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index 2402776e4be..a530517c5c4 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -60,6 +60,8 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { model := &AlertRule{} model.Id = ruleDef.Id model.OrgId = ruleDef.OrgId + model.DashboardId = ruleDef.DashboardId + model.PanelId = ruleDef.PanelId model.Name = ruleDef.Name model.Description = ruleDef.Description model.Frequency = ruleDef.Frequency @@ -67,8 +69,11 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { model.State = ruleDef.State for _, v := range ruleDef.Settings.Get("notifications").MustArray() { - if id, ok := v.(int64); ok { - model.Notifications = append(model.Notifications, int64(id)) + jsonModel := simplejson.NewFromAny(v) + if id, err := jsonModel.Get("id").Int64(); err != nil { + return nil, AlertValidationError{Reason: "Invalid notification schema"} + } else { + model.Notifications = append(model.Notifications, id) } } diff --git a/pkg/services/alerting/alert_rule_test.go b/pkg/services/alerting/alert_rule_test.go index e867410721c..7a007946207 100644 --- a/pkg/services/alerting/alert_rule_test.go +++ b/pkg/services/alerting/alert_rule_test.go @@ -49,8 +49,12 @@ func TestAlertRuleModel(t *testing.T) { }, "reducer": {"type": "avg", "params": []}, "evaluator": {"type": ">", "params": [100]} - } - ] + } + ], + "notifications": [ + {"id": 1134}, + {"id": 22} + ] } ` @@ -91,6 +95,10 @@ func TestAlertRuleModel(t *testing.T) { So(evaluator.Type, ShouldEqual, ">") }) }) + + Convey("Can read notifications", func() { + So(len(alertRule.Notifications), ShouldEqual, 2) + }) }) }) } diff --git a/pkg/services/alerting/handler.go b/pkg/services/alerting/handler.go index 628dc94d9cd..9ea971b0a84 100644 --- a/pkg/services/alerting/handler.go +++ b/pkg/services/alerting/handler.go @@ -18,7 +18,7 @@ type HandlerImpl struct { func NewHandler() *HandlerImpl { return &HandlerImpl{ - log: log.New("alerting.executor"), + log: log.New("alerting.handler"), alertJobTimeout: time.Second * 5, } } @@ -33,7 +33,7 @@ func (e *HandlerImpl) Execute(context *AlertResultContext) { context.EndTime = time.Now() e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id) case <-context.DoneChan: - e.log.Debug("Job Execution done", "timing", context.GetDurationSeconds(), "alertId", context.Rule.Id, "firing", context.Firing) + e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing) } } diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 486f0bc9f98..ecd7cec1079 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -42,8 +42,8 @@ type AlertResultContext struct { log log.Logger } -func (a *AlertResultContext) GetDurationSeconds() float64 { - return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000000) +func (a *AlertResultContext) GetDurationMs() float64 { + return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000) } func NewAlertResultContext(rule *AlertRule) *AlertResultContext { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index de7c144a20e..6576a25fd5f 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -23,6 +23,8 @@ func NewRootNotifier() *RootNotifier { } func (n *RootNotifier) Notify(context *AlertResultContext) { + n.log.Info("Sending notifications for", "ruleId", context.Rule.Id) + notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications) if err != nil { n.log.Error("Failed to read notifications", "error", err) @@ -70,20 +72,22 @@ type EmailNotifier struct { } func (this *EmailNotifier) Notify(context *AlertResultContext) { - this.log.Info("Sending alert notification to %v", this.Addresses) + this.log.Info("Sending alert notification to", "addresses", this.Addresses) slugQuery := &m.GetDashboardSlugByIdQuery{Id: context.Rule.DashboardId} if err := bus.Dispatch(slugQuery); err != nil { this.log.Error("Failed to load dashboard", "error", err) return } - dashboardSlug := slugQuery.Result + + ruleLink := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slugQuery.Result, context.Rule.PanelId) cmd := &m.SendEmailCommand{ Data: map[string]interface{}{ - "RuleName": context.Rule.Name, - "Severity": context.Rule.Severity, - "RuleLink": setting.ToAbsUrl("dashboard/db/" + dashboardSlug), + "RuleState": context.Rule.State, + "RuleName": context.Rule.Name, + "Severity": context.Rule.Severity, + "RuleLink": ruleLink, }, To: this.Addresses, Template: "alert_notification.html", @@ -91,7 +95,7 @@ func (this *EmailNotifier) Notify(context *AlertResultContext) { err := bus.Dispatch(cmd) if err != nil { - this.log.Error("Failed tosend alert notification email", "error", err) + this.log.Error("Failed to send alert notification email", "error", err) } } diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go index db7da930746..13d4c868f82 100644 --- a/pkg/services/alerting/reader.go +++ b/pkg/services/alerting/reader.go @@ -18,10 +18,13 @@ type AlertRuleReader struct { serverID string serverPosition int clusterSize int + log log.Logger } func NewRuleReader() *AlertRuleReader { - ruleReader := &AlertRuleReader{} + ruleReader := &AlertRuleReader{ + log: log.New("alerting.ruleReader"), + } go ruleReader.initReader() return ruleReader @@ -40,17 +43,19 @@ func (arr *AlertRuleReader) initReader() { func (arr *AlertRuleReader) Fetch() []*AlertRule { cmd := &m.GetAllAlertsQuery{} - err := bus.Dispatch(cmd) - if err != nil { - log.Error(1, "Alerting: ruleReader.fetch(): Could not load alerts", err) + if err := bus.Dispatch(cmd); err != nil { + arr.log.Error("Could not load alerts", "error", err) return []*AlertRule{} } - res := make([]*AlertRule, len(cmd.Result)) - for i, ruleDef := range cmd.Result { - model, _ := NewAlertRuleFromDBModel(ruleDef) - res[i] = model + res := make([]*AlertRule, 0) + for _, ruleDef := range cmd.Result { + if model, err := NewAlertRuleFromDBModel(ruleDef); err != nil { + arr.log.Error("Could not build alert model for rule", "ruleId", ruleDef.Id, "error", err) + } else { + res = append(res, model) + } } return res diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 7aae00dfe7e..25643f707db 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -17,7 +17,8 @@ type ResultHandlerImpl struct { func NewResultHandler() *ResultHandlerImpl { return &ResultHandlerImpl{ - log: log.New("alerting.resultHandler"), + log: log.New("alerting.resultHandler"), + notifier: NewRootNotifier(), } } @@ -47,7 +48,6 @@ func (handler *ResultHandlerImpl) Handle(result *AlertResultContext) { } result.Rule.State = newState - //handler.log.Debug("will notify about new state", "new state", result.State) - //handler.notifier.Notify(result) + handler.notifier.Notify(result) } } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 8a3c8543f91..e94e04e696e 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -3,6 +3,7 @@ package sqlstore import ( "bytes" "fmt" + "strings" "time" "github.com/go-xorm/xorm" @@ -63,8 +64,10 @@ func getAlertNotifications(query *m.GetAlertNotificationsQuery, sess *xorm.Sessi } if len(query.Ids) > 0 { - sql.WriteString(` AND alert_notification.id IN (?)`) - params = append(params, query.Ids) + sql.WriteString(` AND alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")") + for _, v := range query.Ids { + params = append(params, v) + } } results := make([]*m.AlertNotification, 0) diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index b74b3a4e8b3..b8a8af24ab9 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -115,6 +115,11 @@ function (angular, _, $) { } } + // if no edit state cleanup tab parm + if (!this.state.edit) { + delete this.state.tab; + } + $location.search(this.serializeToUrl()); this.syncState(); }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index bcb1980f854..e58994974cd 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -95,10 +95,10 @@ export class PanelCtrl { this.editModeInitiated = true; this.events.emit('init-edit-mode', null); - var routeParams = this.$injector.get('$routeParams'); - if (routeParams.editorTab) { + var urlTab = (this.$injector.get('$routeParams').tab || '').toLowerCase(); + if (urlTab) { this.editorTabs.forEach((tab, i) => { - if (tab.title === routeParams.editorTab) { + if (tab.title.toLowerCase() === urlTab) { this.editorTabIndex = i; } }); @@ -109,7 +109,7 @@ export class PanelCtrl { this.editorTabIndex = newIndex; var route = this.$injector.get('$route'); - route.current.params.editorTab = this.editorTabs[newIndex].title; + route.current.params.tab = this.editorTabs[newIndex].title.toLowerCase(); route.updateParams(); } diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index fae5a4ca8ff..7cd8246889e 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -1,8 +1,6 @@ /// import _ from 'lodash'; -import $ from 'jquery'; -import angular from 'angular'; import { QueryPartDef, @@ -28,7 +26,6 @@ var reducerAvgDef = new QueryPartDef({ export class AlertTabCtrl { panel: any; panelCtrl: any; - metricTargets; testing: boolean; testResult: any; @@ -50,37 +47,57 @@ export class AlertTabCtrl { {text: 'Warning', value: 'warning'}, ]; addNotificationSegment; + notifications; + alertNotifications; /** @ngInject */ - constructor($scope, private $timeout, private backendSrv, private dashboardSrv, private uiSegmentSrv) { + constructor(private $scope, private $timeout, private backendSrv, private dashboardSrv, private uiSegmentSrv) { this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; - $scope.ctrl = this; + this.$scope.ctrl = this; + } - this.metricTargets = this.panel.targets.map(val => val); - this.addNotificationSegment = uiSegmentSrv.newPlusButton(); + $onInit() { + this.addNotificationSegment = this.uiSegmentSrv.newPlusButton(); this.initModel(); // set panel alert edit mode - $scope.$on("$destroy", () => { + this.$scope.$on("$destroy", () => { this.panelCtrl.editingAlert = false; this.panelCtrl.render(); }); - } - getNotifications() { + // build notification model + this.notifications = []; + this.alertNotifications = []; + return this.backendSrv.get('/api/alert-notifications').then(res => { - return res.map(item => { - return this.uiSegmentSrv.newSegment(item.name); + this.notifications = res; + + _.each(this.alert.notifications, item => { + var model = _.findWhere(this.notifications, {id: item.id}); + if (model) { + this.alertNotifications.push(model); + } }); }); } + getNotifications() { + return Promise.resolve(this.notifications.map(item => { + return this.uiSegmentSrv.newSegment(item.name); + })); + } + notificationAdded() { - this.alert.notifications.push({ - name: this.addNotificationSegment.value - }); + var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value}); + if (!model) { + return; + } + + this.alertNotifications.push({name: model.name}); + this.alert.notifications.push({id: model.id}); // reset plus button this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value; @@ -89,6 +106,7 @@ export class AlertTabCtrl { removeNotification(index) { this.alert.notifications.splice(index, 1); + this.alertNotifications.splice(index, 1); } initModel() { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index a165589a8c5..ed0e0430592 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -132,7 +132,7 @@ class GraphCtrl extends MetricsPanelCtrl { this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); if (config.alertingEnabled) { - this.addEditorTab('Alerting', graphAlertEditor, 5); + this.addEditorTab('Alert', graphAlertEditor, 5); } this.logScales = { diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 41edfe6aaad..16c385af18d 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -99,7 +99,7 @@
Notifications
- + {{nc.name}} diff --git a/public/emails/alert_notification.html b/public/emails/alert_notification.html index 4491c71a5cc..21dded9c783 100644 --- a/public/emails/alert_notification.html +++ b/public/emails/alert_notification.html @@ -113,37 +113,18 @@ color: #FFFFFF !important;
- + {{Subject .Subject "Grafana Alert: {{.Severity}} {{.RuleName}}"}} -{{Subject .Subject "Grafana Alert: [ {{.State}} ] {{.Name}}" }} +
+
-Alertstate: {{.State}}
-{{.AlertPageUrl}}
-{{.DashboardLink}}
-{{.Description}}
+Alert rule: {{.RuleName}}
+Alert state: {{.RuleState}}
-{{if eq .State "Ok"}} - Everything is Ok -{{end}} +Link to alert rule -{{if ne .State "Ok" }} - +
- - - - - - - {{ range $ta := .TriggeredAlerts}} - - - - - - {{end}} -
-{{end}} diff --git a/public/emails/invited_to_org.html b/public/emails/invited_to_org.html index acfe6c354fc..8263fb65a15 100644 --- a/public/emails/invited_to_org.html +++ b/public/emails/invited_to_org.html @@ -149,7 +149,7 @@ color: #FFFFFF !important; diff --git a/public/emails/new_user_invite.html b/public/emails/new_user_invite.html index 1504ca1453c..15de19a7bf6 100644 --- a/public/emails/new_user_invite.html +++ b/public/emails/new_user_invite.html @@ -147,7 +147,7 @@ color: #FFFFFF !important; diff --git a/public/emails/signup_started.html b/public/emails/signup_started.html index 425f36f67e9..a8ee02174a8 100644 --- a/public/emails/signup_started.html +++ b/public/emails/signup_started.html @@ -148,7 +148,7 @@ color: #FFFFFF !important; From 0b0f6b08480931c0564aac404ed66ca85cfa654b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 26 Jul 2016 14:35:42 +0200 Subject: [PATCH 22/22] feat(alerting): made very basic threshold viz work again --- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 43 ++++++++++++- public/app/plugins/panel/graph/graph.js | 62 +++---------------- public/app/plugins/panel/graph/module.ts | 1 + .../panel/graph/partials/tab_alerting.html | 2 +- public/app/plugins/panel/graph/thresholds.ts | 9 +-- 5 files changed, 55 insertions(+), 62 deletions(-) diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 7cd8246889e..a8cac86a6ad 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -131,10 +131,45 @@ export class AlertTabCtrl { return memo; }, []); - this.panelCtrl.editingAlert = true; + ///this.panelCtrl.editingAlert = true; + this.syncThresholds(); this.panelCtrl.render(); } + syncThresholds() { + var threshold: any = {}; + if (this.panel.thresholds && this.panel.thresholds.length > 0) { + threshold = this.panel.thresholds[0]; + } else { + this.panel.thresholds = [threshold]; + } + + var updated = false; + for (var condition of this.conditionModels) { + if (condition.type === 'query') { + var value = condition.evaluator.params[0]; + if (!_.isNumber(value)) { + continue; + } + + if (value !== threshold.from) { + threshold.from = value; + updated = true; + } + + if (condition.evaluator.type === '<' && threshold.to !== -Infinity) { + threshold.to = -Infinity; + updated = true; + } else if (condition.evaluator.type === '>' && threshold.to !== Infinity) { + threshold.to = Infinity; + updated = true; + } + } + } + + return updated; + } + buildDefaultCondition() { return { type: 'query', @@ -180,8 +215,10 @@ export class AlertTabCtrl { this.initModel(); } - thresholdsUpdated() { - this.panelCtrl.render(); + thresholdUpdated() { + if (this.syncThresholds()) { + this.panelCtrl.render(); + } } test() { diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index eb657b55335..13172b06019 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -184,7 +184,7 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholds) { // give space to alert editing if (ctrl.editingAlert) { if (!thresholdControls) { - elem.css('margin-right', '220px'); + elem.css('margin-right', '110px'); thresholdControls = new ThresholdControls(ctrl); } } else if (thresholdControls) { @@ -327,74 +327,28 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholds) { } function addGridThresholds(options, panel) { - if (!panel.alert) { + if (!panel.thresholds || panel.thresholds.length === 0) { return; } - var crit = panel.alert.crit; - var warn = panel.alert.warn; - var critEdge = Infinity; - - if (_.isNumber(crit.value)) { - if (crit.op === '<') { - critEdge = -Infinity; + for (var i = 0; i < panel.thresholds.length; i++) { + var threshold = panel.thresholds[i]; + if (!_.isNumber(threshold.from)) { + continue; } // fill options.grid.markings.push({ - yaxis: {from: crit.value, to: critEdge}, + yaxis: {from: threshold.from, to: threshold.to}, color: 'rgba(234, 112, 112, 0.10)', }); // line options.grid.markings.push({ - yaxis: {from: crit.value, to: crit.value}, + yaxis: {from: threshold.from, to: threshold.from}, color: '#ed2e18' }); } - - if (_.isNumber(warn.value)) { - //var warnEdge = crit.value || Infinity; - var warnEdge; - if (crit.value) { - warnEdge = crit.value; - } else { - warnEdge = warn.op === '<' ? -Infinity : Infinity; - } - - // fill - options.grid.markings.push({ - yaxis: {from: warn.value, to: warnEdge}, - color: 'rgba(216, 200, 27, 0.10)', - }); - - // line - options.grid.markings.push({ - yaxis: {from: warn.value, to: warn.value}, - color: '#F79520' - }); - } - - // if (_.isNumber(panel.grid.threshold1)) { - // var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null); - // options.grid.markings.push({ - // yaxis: { from: panel.grid.threshold1, to: limit1 }, - // color: panel.grid.threshold1Color - // }); - // - // if (_.isNumber(panel.grid.threshold2)) { - // var limit2; - // if (panel.grid.thresholdLine) { - // limit2 = panel.grid.threshold2; - // } else { - // limit2 = panel.grid.threshold1 > panel.grid.threshold2 ? -Infinity : +Infinity; - // } - // options.grid.markings.push({ - // yaxis: { from: panel.grid.threshold2, to: limit2 }, - // color: panel.grid.threshold2Color - // }); - // } - // } } function addAnnotations(options) { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index ed0e0430592..d2fda8878df 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -105,6 +105,7 @@ class GraphCtrl extends MetricsPanelCtrl { // other style overrides seriesOverrides: [], alerting: {}, + thresholds: [], }; /** @ngInject */ diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 16c385af18d..463e2cb4154 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -70,7 +70,7 @@
Value - +