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 23e389f5990..7339485787f 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -1,10 +1,13 @@ package api import ( + "fmt" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" ) func ValidateOrgAlert(c *middleware.Context) { @@ -36,16 +39,17 @@ 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, Name: alert.Name, Description: alert.Description, State: alert.State, + Severity: alert.Severity, }) } @@ -71,6 +75,40 @@ func GetAlerts(c *middleware.Context) Response { return Json(200, alertDTOs) } +// POST /api/alerts/test +func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response { + backendCmd := alerting.AlertTestCommand{ + OrgId: c.OrgId, + Dashboard: dto.Dashboard, + PanelId: dto.PanelId, + } + + if err := bus.Dispatch(&backendCmd); err != nil { + if validationErr, ok := err.(alerting.AlertValidationError); ok { + return ApiError(422, validationErr.Error(), nil) + } + return ApiError(500, "Failed to test rule", err) + } + + res := backendCmd.Result + + dtoRes := &dtos.AlertTestResult{ + Firing: res.Firing, + } + + if res.Error != nil { + dtoRes.Error = res.Error.Error() + } + + for _, log := range res.Logs { + dtoRes.Logs = append(dtoRes.Logs, &dtos.AlertTestResultLog{Message: log.Message, Data: log.Data}) + } + + dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs()) + + return Json(200, dtoRes) +} + // GET /api/alerts/:id func GetAlert(c *middleware.Context) Response { id := c.ParamsInt64(":alertId") @@ -101,55 +139,53 @@ 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{ - 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, @@ -162,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"), } @@ -175,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) @@ -185,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) @@ -204,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/api.go b/pkg/api/api.go index 4d2430a7c35..b95e7dc5459 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -246,7 +246,8 @@ func Register(r *macaron.Macaron) { r.Get("/metrics", wrap(GetInternalMetrics)) r.Group("/alerts", func() { - r.Get("/:alertId/states", wrap(GetAlertStates)) + r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest)) + //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 b07ea9cd8b2..244d2bede66 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -1,31 +1,50 @@ package dtos -import "time" +import ( + "time" -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"` + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" +) + +type AlertRule struct { + 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"` } -type AlertNotificationDTO struct { +type AlertNotification struct { Id int64 `json:"id"` Name string `json:"name"` Type string `json:"type"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` } + +type AlertTestCommand struct { + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + PanelId int64 `json:"panelId" binding:"Required"` +} + +type AlertTestResult struct { + Firing bool `json:"firing"` + TimeMs string `json:"timeMs"` + Error string `json:"error,omitempty"` + Logs []*AlertTestResultLog `json:"logs,omitempty"` +} + +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/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/models/alert.go b/pkg/models/alert.go index a31d27096f6..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,7 +36,8 @@ type Alert struct { PanelId int64 Name string Description string - State string + Severity AlertSeverityType + State AlertStateType Handler int64 Enabled bool Frequency int64 @@ -31,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 } @@ -73,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 @@ -100,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 } @@ -123,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_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/alert_state.go b/pkg/models/alert_state.go index 679da91f22f..5071efc2171 100644 --- a/pkg/models/alert_state.go +++ b/pkg/models/alert_state.go @@ -1,55 +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"` - TriggeredAlerts *simplejson.Json `json:"triggeredAlerts"` - - 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/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/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index cc5968eeb70..a530517c5c4 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -4,31 +4,32 @@ import ( "fmt" "regexp" "strconv" - "strings" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/alerting/transformers" m "github.com/grafana/grafana/pkg/models" ) type AlertRule struct { - Id int64 - OrgId int64 - DashboardId int64 - PanelId int64 - Frequency int64 - Name string - Description string - State string - Warning Level - Critical Level - Query AlertQuery - Transform string - TransformParams simplejson.Json - Transformer transformers.Transformer + Id int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Frequency int64 + Name string + Description string + State m.AlertStateType + Severity m.AlertSeverityType + Conditions []AlertCondition + Notifications []int64 +} - NotificationGroups []int64 +type AlertValidationError struct { + Reason string +} + +func (e AlertValidationError) Error() string { + return e.Reason } var ( @@ -59,60 +60,37 @@ 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.State = ruleDef.State model.Frequency = ruleDef.Frequency + model.Severity = ruleDef.Severity + model.State = ruleDef.State - 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() { + 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) } } - model.NotificationGroups = ids - - critical := ruleDef.Settings.Get("crit") - model.Critical = Level{ - Operator: critical.Get("op").MustString(), - Value: critical.Get("value").MustFloat64(), + for index, condition := range ruleDef.Settings.Get("conditions").MustArray() { + conditionModel := simplejson.NewFromAny(condition) + switch conditionModel.Get("type").MustString() { + case "query": + queryCondition, err := NewQueryCondition(conditionModel, index) + 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..7a007946207 100644 --- a/pkg/services/alerting/alert_rule_test.go +++ b/pkg/services/alerting/alert_rule_test.go @@ -31,33 +31,30 @@ func TestAlertRuleModel(t *testing.T) { So(seconds, ShouldEqual, 1) }) - Convey("", func() { + Convey("can construct alert rule model", func() { json := ` { "name": "name2", "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, + "model": {"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} + }, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ], + "notifications": [ + {"id": 1134}, + {"id": 22} + ] } ` @@ -72,15 +69,36 @@ func TestAlertRuleModel(t *testing.T) { Settings: alertJSON, } - alertRule, err := NewAlertRuleFromDBModel(alert) + alertRule, err := NewAlertRuleFromDBModel(alert) So(err, ShouldBeNil) - So(alertRule.Warning.Operator, ShouldEqual, ">") - So(alertRule.Warning.Value, ShouldEqual, 10) + So(alertRule.Conditions, ShouldHaveLength, 1) - So(alertRule.Critical.Operator, ShouldEqual, ">") - So(alertRule.Critical.Value, ShouldEqual, 20) + Convey("Can read query condition from json model", func() { + queryCondition, ok := alertRule.Conditions[0].(*QueryCondition) + So(ok, ShouldBeTrue) + + So(queryCondition.Query.From, ShouldEqual, "5m") + So(queryCondition.Query.To, ShouldEqual, "now") + So(queryCondition.Query.DatasourceId, ShouldEqual, 1) + + Convey("Can read query reducer", func() { + reducer, ok := queryCondition.Reducer.(*SimpleReducer) + So(ok, ShouldBeTrue) + So(reducer.Type, ShouldEqual, "avg") + }) + + Convey("Can read evaluator", func() { + evaluator, ok := queryCondition.Evaluator.(*DefaultAlertEvaluator) + So(ok, ShouldBeTrue) + So(evaluator.Type, ShouldEqual, ">") + }) + }) + + Convey("Can read notifications", func() { + So(len(alertRule.Notifications), ShouldEqual, 2) + }) }) }) } 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 new file mode 100644 index 00000000000..42affee9d57 --- /dev/null +++ b/pkg/services/alerting/conditions.go @@ -0,0 +1,191 @@ +package alerting + +import ( + "encoding/json" + "fmt" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/tsdb" +) + +type QueryCondition struct { + Index int + Query AlertQuery + Reducer QueryReducer + Evaluator AlertEvaluator + HandleRequest tsdb.HandleRequestFunc +} + +func (c *QueryCondition) Eval(context *AlertResultContext) { + seriesList, err := c.executeQuery(context) + if err != nil { + context.Error = err + return + } + + 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.Firing = true + break + } + } +} + +func (c *QueryCondition) executeQuery(context *AlertResultContext) (tsdb.TimeSeriesSlice, error) { + getDsInfo := &m.GetDataSourceByIdQuery{ + Id: c.Query.DatasourceId, + OrgId: context.Rule.OrgId, + } + + if err := bus.Dispatch(getDsInfo); err != nil { + return nil, fmt.Errorf("Could not find datasource") + } + + req := c.getRequestForAlertRule(getDsInfo.Result) + result := make(tsdb.TimeSeriesSlice, 0) + + resp, err := c.HandleRequest(req) + if err != nil { + return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err) + } + + for _, v := range resp.Results { + if v.Error != nil { + return nil, fmt.Errorf("tsdb.HandleRequest() response error %v", v) + } + + result = append(result, v.Series...) + + if context.IsTestRun { + context.Logs = append(context.Logs, &AlertResultLogEntry{ + Message: fmt.Sprintf("Condition[%d]: Query Result", c.Index), + Data: v.Series, + }) + } + } + + return result, nil +} + +func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb.Request { + req := &tsdb.Request{ + TimeRange: tsdb.TimeRange{ + From: c.Query.From, + To: c.Query.To, + }, + Queries: []*tsdb.Query{ + { + RefId: "A", + Query: c.Query.Model.Get("target").MustString(), + DataSource: &tsdb.DataSourceInfo{ + Id: datasource.Id, + Name: datasource.Name, + PluginId: datasource.Type, + Url: datasource.Url, + }, + }, + }, + } + + return req +} + +func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, error) { + condition := QueryCondition{} + condition.Index = index + condition.HandleRequest = tsdb.HandleRequest + + queryJson := model.Get("query") + + condition.Query.Model = queryJson.Get("model") + condition.Query.From = queryJson.Get("params").MustArray()[1].(string) + condition.Query.To = queryJson.Get("params").MustArray()[2].(string) + condition.Query.DatasourceId = queryJson.Get("datasourceId").MustInt64() + + reducerJson := model.Get("reducer") + condition.Reducer = NewSimpleReducer(reducerJson.Get("type").MustString()) + + evaluatorJson := model.Get("evaluator") + evaluator, err := NewDefaultAlertEvaluator(evaluatorJson) + if err != nil { + return nil, err + } + + condition.Evaluator = evaluator + return &condition, nil +} + +type SimpleReducer struct { + Type string +} + +func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 { + var value float64 = 0 + + switch s.Type { + case "avg": + for _, point := range series.Points { + value += point[0] + } + value = value / float64(len(series.Points)) + } + + return value +} + +func NewSimpleReducer(typ string) *SimpleReducer { + return &SimpleReducer{Type: typ} +} + +type DefaultAlertEvaluator struct { + Type string + Threshold float64 +} + +func (e *DefaultAlertEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { + switch e.Type { + case ">": + return reducedValue > e.Threshold + case "<": + return reducedValue < e.Threshold + } + + return false +} + +func NewDefaultAlertEvaluator(model *simplejson.Json) (*DefaultAlertEvaluator, error) { + evaluator := &DefaultAlertEvaluator{} + + evaluator.Type = model.Get("type").MustString() + if evaluator.Type == "" { + return nil, AlertValidationError{Reason: "Evaluator missing type property"} + } + + params := model.Get("params").MustArray() + if len(params) == 0 { + return nil, AlertValidationError{Reason: "Evaluator missing threshold parameter"} + } + + threshold, ok := params[0].(json.Number) + if !ok { + return nil, AlertValidationError{Reason: "Evaluator has invalid threshold parameter"} + } + + evaluator.Threshold, _ = threshold.Float64() + return evaluator, nil +} diff --git a/pkg/services/alerting/conditions_test.go b/pkg/services/alerting/conditions_test.go new file mode 100644 index 00000000000..6fbe2ebe93b --- /dev/null +++ b/pkg/services/alerting/conditions_test.go @@ -0,0 +1,92 @@ +package alerting + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" +) + +func TestQueryCondition(t *testing.T) { + + Convey("when evaluating query condition", t, func() { + + queryConditionScenario("Given avg() and > 100", func(ctx *queryConditionTestContext) { + + ctx.reducer = `{"type": "avg"}` + ctx.evaluator = `{"type": ">", "params": [100]}` + + 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.Firing, ShouldBeTrue) + }) + + 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.Firing, ShouldBeFalse) + }) + }) + }) +} + +type queryConditionTestContext struct { + reducer string + evaluator string + series tsdb.TimeSeriesSlice + result *AlertResultContext +} + +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":` + ctx.reducer + `, + "evaluator":` + ctx.evaluator + ` + }`)) + So(err, ShouldBeNil) + + condition, err := NewQueryCondition(jsonModel, 0) + So(err, ShouldBeNil) + + condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) { + return &tsdb.Response{ + Results: map[string]*tsdb.QueryResult{ + "A": {Series: ctx.series}, + }, + }, nil + } + + condition.Eval(ctx.result) +} + +func queryConditionScenario(desc string, fn queryConditionScenarioFunc) { + Convey(desc, func() { + + 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/engine.go b/pkg/services/alerting/engine.go index c16473f793e..e9fa1d529d8 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -1,38 +1,34 @@ package alerting import ( - "fmt" "time" "github.com/benbjohnson/clock" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/services/alerting/alertstates" ) type Engine struct { execQueue chan *AlertJob - resultQueue chan *AlertResult + resultQueue chan *AlertResultContext clock clock.Clock ticker *Ticker scheduler Scheduler - handler AlertingHandler + handler AlertHandler ruleReader RuleReader log log.Logger responseHandler ResultHandler - alertJobTimeout time.Duration } func NewEngine() *Engine { e := &Engine{ ticker: NewTicker(time.Now(), time.Second*0, clock.New()), execQueue: make(chan *AlertJob, 1000), - resultQueue: make(chan *AlertResult, 1000), + resultQueue: make(chan *AlertResultContext, 1000), scheduler: NewScheduler(), handler: NewHandler(), ruleReader: NewRuleReader(), log: log.New("alerting.engine"), responseHandler: NewResultHandler(), - alertJobTimeout: time.Second * 5, } return e @@ -75,41 +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) - job.Running = true - e.executeJob(job) + e.log.Debug("Starting executing alert rule %s", job.Rule.Name) + go e.executeJob(job) } } func (e *Engine) executeJob(job *AlertJob) { - startTime := time.Now() - - resultChan := make(chan *AlertResult, 1) - go e.handler.Execute(job, resultChan) - - select { - case <-time.After(e.alertJobTimeout): - e.resultQueue <- &AlertResult{ - State: alertstates.Pending, - Error: fmt.Errorf("Timeout"), - AlertJob: job, - StartTime: startTime, - EndTime: time.Now(), + defer func() { + if err := recover(); err != nil { + e.log.Error("Execute Alert Panic", "error", err, "stack", log.Stack(1)) } - close(resultChan) - e.log.Debug("Job Execution timeout", "alertRuleId", job.Rule.Id) - case result := <-resultChan: - duration := float64(result.EndTime.Nanosecond()-result.StartTime.Nanosecond()) / float64(1000000) - e.log.Debug("Job Execution done", "timeTakenMs", duration, "ruleId", job.Rule.Id) - e.resultQueue <- result - } + }() + + job.Running = true + context := NewAlertResultContext(job.Rule) + e.handler.Execute(context) + job.Running = false + + e.resultQueue <- context } func (e *Engine) resultHandler() { @@ -120,25 +100,11 @@ func (e *Engine) resultHandler() { }() for result := range e.resultQueue { - e.log.Debug("Alert Rule Result", "ruleId", result.AlertJob.Rule.Id, "state", result.State, "retry", result.AlertJob.RetryCount) - - result.AlertJob.Running = false + e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing) if result.Error != nil { - result.AlertJob.IncRetry() - - if result.AlertJob.Retryable() { - e.log.Error("Alert Rule Result Error", "ruleId", result.AlertJob.Rule.Id, "error", result.Error, "retry", result.AlertJob.RetryCount) - e.execQueue <- result.AlertJob - } else { - e.log.Error("Alert Rule Result Error After Max Retries", "ruleId", result.AlertJob.Rule.Id, "error", result.Error, "retry", result.AlertJob.RetryCount) - - result.State = alertstates.Critical - result.Description = fmt.Sprintf("Failed to run check after %d retires, Error: %v", maxAlertExecutionRetries, result.Error) - e.responseHandler.Handle(result) - } + e.log.Error("Alert Rule Result Error", "ruleId", result.Rule.Id, "error", result.Error, "retry") } else { - result.AlertJob.ResetRetry() e.responseHandler.Handle(result) } } diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 196af8ae171..88f4998b9cf 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -47,6 +47,17 @@ func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (*m.DataSource, e return nil, errors.New("Could not find datasource id for " + dsName) } +func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Json { + for _, targetsObj := range panel.Get("targets").MustArray() { + target := simplejson.NewFromAny(targetsObj) + + if target.Get("refId").MustString() == refId { + return target + } + } + return nil +} + func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { e.log.Debug("GetAlerts") @@ -78,34 +89,39 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { Handler: jsonAlert.Get("handler").MustInt64(), Enabled: jsonAlert.Get("enabled").MustBool(), Description: jsonAlert.Get("description").MustString(), + Severity: m.AlertSeverityType(jsonAlert.Get("severity").MustString()), Frequency: getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()), } - valueQuery := jsonAlert.Get("query") - valueQueryRef := valueQuery.Get("refId").MustString() - for _, targetsObj := range panel.Get("targets").MustArray() { - target := simplejson.NewFromAny(targetsObj) + if !alert.Severity.IsValid() { + return nil, AlertValidationError{Reason: "Invalid alert Severity"} + } - if target.Get("refId").MustString() == valueQueryRef { - dsName := "" - if target.Get("datasource").MustString() != "" { - dsName = target.Get("datasource").MustString() - } else if panel.Get("datasource").MustString() != "" { - dsName = panel.Get("datasource").MustString() - } + for _, condition := range jsonAlert.Get("conditions").MustArray() { + jsonCondition := simplejson.NewFromAny(condition) - if datasource, err := e.lookupDatasourceId(dsName); err != nil { - return nil, err - } else { - valueQuery.SetPath([]string{"datasourceId"}, datasource.Id) - valueQuery.SetPath([]string{"datasourceType"}, datasource.Type) - } + jsonQuery := jsonCondition.Get("query") + queryRefId := jsonQuery.Get("params").MustArray()[0].(string) + panelQuery := findPanelQueryByRefId(panel, queryRefId) - targetQuery := target.Get("target").MustString() - if targetQuery != "" { - jsonAlert.SetPath([]string{"query", "query"}, targetQuery) - } + if panelQuery == nil { + return nil, AlertValidationError{Reason: "Alert refes to query that cannot be found"} } + + dsName := "" + if panelQuery.Get("datasource").MustString() != "" { + dsName = panelQuery.Get("datasource").MustString() + } else if panel.Get("datasource").MustString() != "" { + dsName = panel.Get("datasource").MustString() + } + + if datasource, err := e.lookupDatasourceId(dsName); err != nil { + return nil, err + } else { + jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) + } + + jsonQuery.Set("model", panelQuery.Interface()) } alert.Settings = jsonAlert @@ -116,9 +132,8 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { alerts = append(alerts, alert) } else { e.log.Error("Failed to extract alerts from dashboard", "error", err) - return nil, errors.New("Failed to extract alerts from dashboard") + return nil, err } - } } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 88fab4bf70d..cd88c8697a5 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -14,162 +14,71 @@ func TestAlertRuleExtraction(t *testing.T) { Convey("Parsing alert rules from dashboard json", t, func() { Convey("Parsing and validating alerts from dashboards", func() { json := `{ - "id": 57, - "title": "Graphite 4", - "originalTitle": "Graphite 4", - "tags": [ - "graphite" - ], - "rows": [ - { - - "panels": [ + "id": 57, + "title": "Graphite 4", + "originalTitle": "Graphite 4", + "tags": ["graphite"], + "rows": [ { - "title": "Active desktop users", - "editable": true, - "type": "graph", - "id": 3, - "targets": [ + "panels": [ + { + "title": "Active desktop users", + "editable": true, + "type": "graph", + "id": 3, + "targets": [ { "refId": "A", "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" } - ], - "datasource": null, - "alert": { - "name": "name1", - "description": "desc1", - "handler": 1, - "enabled": true, - "critical": { - "value": 20, - "op": ">" - }, - "frequency": "60s", - "query": { - "from": "5m", - "refId": "A", - "to": "now" - }, - "transform": { - "type": "avg", - "name": "aggregation" - }, - "warn": { - "value": 10, - "op": ">" - } - } - }, - { - "title": "Active mobile users", - "id": 4, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)" - } - ], - "datasource": "graphite2", - "alert": { - "name": "name2", - "description": "desc2", - "handler": 0, - "enabled": true, - "critical": { - "value": 20, - "op": ">" - }, - "frequency": "60s", - "query": { - "from": "5m", - "refId": "A", - "to": "now" - }, - "transform": { - "type": "avg", - "name": "aggregation" - }, - "warn": { - "value": 10, - "op": ">" - } - } - } - ], - "title": "Row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "datasource": "InfluxDB", - "id": 2, - "alert": { - "name": "name2", - "description": "desc2", - "enabled": false, - "critical": { - "level": 20, - "op": ">" - }, - "warn": { - "level": 10, - "op": ">" + ], + "datasource": null, + "alert": { + "name": "name1", + "description": "desc1", + "handler": 1, + "enabled": true, + "frequency": "60s", + "severity": "critical", + "conditions": [ + { + "type": "query", + "query": {"params": ["A", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] } }, - "targets": [ - { - "dsType": "influxdb", - "groupBy": [ - { - "params": [ - "$interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "measurement": "cpu", - "policy": "default", - "query": "SELECT mean(\"value\") FROM \"cpu\" WHERE $timeFilter GROUP BY time($interval) fill(null)", - "refId": "A", - "resultFormat": "table", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [], - "target": "" + { + "title": "Active mobile users", + "id": 4, + "targets": [ + {"refId": "A", "target": ""}, + {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} + ], + "datasource": "graphite2", + "alert": { + "name": "name2", + "description": "desc2", + "handler": 0, + "enabled": true, + "frequency": "60s", + "severity": "warning", + "conditions": [ + { + "type": "query", + "query": {"params": ["B", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] } - ], - "title": "Broken influxdb panel", - "transform": "table", - "type": "table" + } + ] } - ], - "title": "New row" - } - ] - -}` + ] + }` dashJson, err := simplejson.NewJson([]byte(json)) So(err, ShouldBeNil) @@ -215,6 +124,11 @@ func TestAlertRuleExtraction(t *testing.T) { So(alerts[1].Handler, ShouldEqual, 0) }) + Convey("should extract Severity property", func() { + So(alerts[0].Severity, ShouldEqual, "critical") + So(alerts[1].Severity, ShouldEqual, "warning") + }) + Convey("should extract frequency in seconds", func() { So(alerts[0].Frequency, ShouldEqual, 60) So(alerts[1].Frequency, ShouldEqual, 60) @@ -231,6 +145,18 @@ func TestAlertRuleExtraction(t *testing.T) { So(alerts[1].Name, ShouldEqual, "name2") So(alerts[1].Description, ShouldEqual, "desc2") }) + + Convey("should set datasourceId", func() { + condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0]) + query := condition.Get("query") + So(query.Get("datasourceId").MustInt64(), ShouldEqual, 12) + }) + + Convey("should copy query model to condition", func() { + condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0]) + model := condition.Get("query").Get("model") + So(model.Get("target").MustString(), ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)") + }) }) }) }) diff --git a/pkg/services/alerting/handler.go b/pkg/services/alerting/handler.go index fc7ff71e8dd..9ea971b0a84 100644 --- a/pkg/services/alerting/handler.go +++ b/pkg/services/alerting/handler.go @@ -4,11 +4,7 @@ import ( "fmt" "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" - "github.com/grafana/grafana/pkg/tsdb" ) var ( @@ -16,133 +12,148 @@ var ( ) type HandlerImpl struct { - log log.Logger + log log.Logger + alertJobTimeout time.Duration } func NewHandler() *HandlerImpl { return &HandlerImpl{ - log: log.New("alerting.executor"), + log: log.New("alerting.handler"), + alertJobTimeout: time.Second * 5, } } -func (e *HandlerImpl) Execute(job *AlertJob, resultQueue chan *AlertResult) { - startTime := time.Now() +func (e *HandlerImpl) Execute(context *AlertResultContext) { - timeSeries, err := e.executeQuery(job) - if err != nil { - resultQueue <- &AlertResult{ - Error: err, - State: alertstates.Pending, - AlertJob: job, - StartTime: time.Now(), - EndTime: time.Now(), - } + go e.eval(context) + + select { + case <-time.After(e.alertJobTimeout): + context.Error = fmt.Errorf("Timeout") + context.EndTime = time.Now() + e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id) + case <-context.DoneChan: + e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing) } - result := e.evaluateRule(job.Rule, timeSeries) - result.AlertJob = job - result.StartTime = startTime - result.EndTime = time.Now() - - resultQueue <- result } -func (e *HandlerImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) { - getDsInfo := &m.GetDataSourceByIdQuery{ - Id: job.Rule.Query.DatasourceId, - OrgId: job.Rule.OrgId, - } +func (e *HandlerImpl) eval(context *AlertResultContext) { - if err := bus.Dispatch(getDsInfo); err != nil { - return nil, fmt.Errorf("Could not find datasource") - } + for _, condition := range context.Rule.Conditions { + condition.Eval(context) - req := e.GetRequestForAlertRule(job.Rule, getDsInfo.Result) - result := make(tsdb.TimeSeriesSlice, 0) - - resp, err := tsdb.HandleRequest(req) - if err != nil { - return nil, fmt.Errorf("Alerting: GetSeries() tsdb.HandleRequest() error %v", err) - } - - for _, v := range resp.Results { - if v.Error != nil { - return nil, fmt.Errorf("Alerting: GetSeries() tsdb.HandleRequest() response error %v", v) + // break if condition could not be evaluated + if context.Error != nil { + break } - result = append(result, v.Series...) + // break if result has not triggered yet + if context.Firing == false { + break + } } - return result, nil + context.EndTime = time.Now() + context.DoneChan <- true } -func (e *HandlerImpl) GetRequestForAlertRule(rule *AlertRule, datasource *m.DataSource) *tsdb.Request { - e.log.Debug("GetRequest", "query", rule.Query.Query, "from", rule.Query.From, "datasourceId", datasource.Id) - req := &tsdb.Request{ - TimeRange: tsdb.TimeRange{ - From: "-" + rule.Query.From, - To: rule.Query.To, - }, - Queries: []*tsdb.Query{ - { - RefId: "A", - Query: rule.Query.Query, - DataSource: &tsdb.DataSourceInfo{ - Id: datasource.Id, - Name: datasource.Name, - PluginId: datasource.Type, - Url: datasource.Url, - }, - }, - }, - } - - return req -} - -func (e *HandlerImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice) *AlertResult { - e.log.Debug("Evaluating Alerting Rule", "seriesCount", len(series), "ruleName", rule.Name) - - triggeredAlert := make([]*TriggeredAlert, 0) - - for _, serie := range series { - e.log.Debug("Evaluating series", "series", serie.Name) - transformedValue, _ := rule.Transformer.Transform(serie) - - critResult := evalCondition(rule.Critical, transformedValue) - condition2 := fmt.Sprintf("%v %s %v ", transformedValue, rule.Critical.Operator, rule.Critical.Value) - e.log.Debug("Alert execution Crit", "name", serie.Name, "condition", condition2, "result", critResult) - if critResult { - triggeredAlert = append(triggeredAlert, &TriggeredAlert{ - State: alertstates.Critical, - Value: transformedValue, - Metric: serie.Name, - }) - continue - } - - warnResult := evalCondition(rule.Warning, transformedValue) - condition := fmt.Sprintf("%v %s %v ", transformedValue, rule.Warning.Operator, rule.Warning.Value) - e.log.Debug("Alert execution Warn", "name", serie.Name, "condition", condition, "result", warnResult) - if warnResult { - triggeredAlert = append(triggeredAlert, &TriggeredAlert{ - State: alertstates.Warn, - Value: transformedValue, - Metric: serie.Name, - }) - } - } - - executionState := alertstates.Ok - for _, raised := range triggeredAlert { - if raised.State == alertstates.Critical { - executionState = alertstates.Critical - } - - if executionState != alertstates.Critical && raised.State == alertstates.Warn { - executionState = alertstates.Warn - } - } - - return &AlertResult{State: executionState, TriggeredAlerts: triggeredAlert} -} +// func (e *HandlerImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) { +// getDsInfo := &m.GetDataSourceByIdQuery{ +// Id: job.Rule.Query.DatasourceId, +// OrgId: job.Rule.OrgId, +// } +// +// if err := bus.Dispatch(getDsInfo); err != nil { +// return nil, fmt.Errorf("Could not find datasource") +// } +// +// req := e.GetRequestForAlertRule(job.Rule, getDsInfo.Result) +// result := make(tsdb.TimeSeriesSlice, 0) +// +// resp, err := tsdb.HandleRequest(req) +// if err != nil { +// return nil, fmt.Errorf("Alerting: GetSeries() tsdb.HandleRequest() error %v", err) +// } +// +// for _, v := range resp.Results { +// if v.Error != nil { +// return nil, fmt.Errorf("Alerting: GetSeries() tsdb.HandleRequest() response error %v", v) +// } +// +// result = append(result, v.Series...) +// } +// +// return result, nil +// } +// +// func (e *HandlerImpl) GetRequestForAlertRule(rule *AlertRule, datasource *m.DataSource) *tsdb.Request { +// e.log.Debug("GetRequest", "query", rule.Query.Query, "from", rule.Query.From, "datasourceId", datasource.Id) +// req := &tsdb.Request{ +// TimeRange: tsdb.TimeRange{ +// From: "-" + rule.Query.From, +// To: rule.Query.To, +// }, +// Queries: []*tsdb.Query{ +// { +// RefId: "A", +// Query: rule.Query.Query, +// DataSource: &tsdb.DataSourceInfo{ +// Id: datasource.Id, +// Name: datasource.Name, +// PluginId: datasource.Type, +// Url: datasource.Url, +// }, +// }, +// }, +// } +// +// return req +// } +// +// func (e *HandlerImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice) *AlertResult { +// e.log.Debug("Evaluating Alerting Rule", "seriesCount", len(series), "ruleName", rule.Name) +// +// triggeredAlert := make([]*TriggeredAlert, 0) +// +// for _, serie := range series { +// e.log.Debug("Evaluating series", "series", serie.Name) +// transformedValue, _ := rule.Transformer.Transform(serie) +// +// critResult := evalCondition(rule.Critical, transformedValue) +// condition2 := fmt.Sprintf("%v %s %v ", transformedValue, rule.Critical.Operator, rule.Critical.Value) +// e.log.Debug("Alert execution Crit", "name", serie.Name, "condition", condition2, "result", critResult) +// if critResult { +// triggeredAlert = append(triggeredAlert, &TriggeredAlert{ +// State: alertstates.Critical, +// Value: transformedValue, +// Metric: serie.Name, +// }) +// continue +// } +// +// warnResult := evalCondition(rule.Warning, transformedValue) +// condition := fmt.Sprintf("%v %s %v ", transformedValue, rule.Warning.Operator, rule.Warning.Value) +// e.log.Debug("Alert execution Warn", "name", serie.Name, "condition", condition, "result", warnResult) +// if warnResult { +// triggeredAlert = append(triggeredAlert, &TriggeredAlert{ +// State: alertstates.Warn, +// Value: transformedValue, +// Metric: serie.Name, +// }) +// } +// } +// +// executionState := alertstates.Ok +// for _, raised := range triggeredAlert { +// if raised.State == alertstates.Critical { +// executionState = alertstates.Critical +// } +// +// if executionState != alertstates.Critical && raised.State == alertstates.Warn { +// executionState = alertstates.Warn +// } +// } +// +// return &AlertResult{State: executionState, TriggeredAlerts: triggeredAlert} +// } diff --git a/pkg/services/alerting/handler_test.go b/pkg/services/alerting/handler_test.go index 32171bcb557..10869226dd7 100644 --- a/pkg/services/alerting/handler_test.go +++ b/pkg/services/alerting/handler_test.go @@ -3,149 +3,162 @@ package alerting import ( "testing" - "github.com/grafana/grafana/pkg/services/alerting/alertstates" - "github.com/grafana/grafana/pkg/services/alerting/transformers" - "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" ) +type conditionStub struct { + firing bool +} + +func (c *conditionStub) Eval(context *AlertResultContext) { + context.Firing = c.firing +} + func TestAlertingExecutor(t *testing.T) { Convey("Test alert execution", t, func() { - executor := NewHandler() + handler := NewHandler() - Convey("single time serie", func() { - Convey("Show return ok since avg is above 2", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: ">"}, - Transformer: transformers.NewAggregationTransformer("avg"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Ok) - }) - - Convey("Show return critical since below 2", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: "<"}, - Transformer: transformers.NewAggregationTransformer("avg"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) - }) - - Convey("Show return critical since sum is above 10", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: ">"}, - Transformer: transformers.NewAggregationTransformer("sum"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) - }) - - Convey("Show return ok since avg is below 10", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: ">"}, - Transformer: transformers.NewAggregationTransformer("avg"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Ok) - }) - - Convey("Show return ok since min is below 10", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: ">"}, - Transformer: transformers.NewAggregationTransformer("avg"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}, {9, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Ok) - }) - - Convey("Show return ok since max is above 10", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: ">"}, - Transformer: transformers.NewAggregationTransformer("max"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{6, 0}, {11, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) + Convey("Show return triggered with single passing condition", func() { + context := NewAlertResultContext(&AlertRule{ + Conditions: []AlertCondition{&conditionStub{ + firing: true, + }}, }) + handler.eval(context) + So(context.Firing, ShouldEqual, true) }) - Convey("muliple time series", func() { - Convey("both are ok", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: ">"}, - Transformer: transformers.NewAggregationTransformer("avg"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), - tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Ok) + Convey("Show return false with not passing condition", func() { + context := NewAlertResultContext(&AlertRule{ + Conditions: []AlertCondition{ + &conditionStub{firing: true}, + &conditionStub{firing: false}, + }, }) - Convey("first serie is good, second is critical", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: ">"}, - Transformer: transformers.NewAggregationTransformer("avg"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), - tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) - }) - - Convey("first serie is warn, second is critical", func() { - rule := &AlertRule{ - Critical: Level{Value: 10, Operator: ">"}, - Warning: Level{Value: 5, Operator: ">"}, - Transformer: transformers.NewAggregationTransformer("avg"), - } - - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{6, 0}}), - tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) - }) + handler.eval(context) + So(context.Firing, ShouldEqual, false) }) + + // Convey("Show return critical since below 2", func() { + // rule := &AlertRule{ + // Critical: Level{Value: 10, Operator: "<"}, + // Transformer: transformers.NewAggregationTransformer("avg"), + // } + // + // timeSeries := []*tsdb.TimeSeries{ + // tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), + // } + // + // result := executor.evaluateRule(rule, timeSeries) + // So(result.State, ShouldEqual, alertstates.Critical) + // }) + // + // Convey("Show return critical since sum is above 10", func() { + // rule := &AlertRule{ + // Critical: Level{Value: 10, Operator: ">"}, + // Transformer: transformers.NewAggregationTransformer("sum"), + // } + // + // timeSeries := []*tsdb.TimeSeries{ + // tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), + // } + // + // result := executor.evaluateRule(rule, timeSeries) + // So(result.State, ShouldEqual, alertstates.Critical) + // }) + // + // Convey("Show return ok since avg is below 10", func() { + // rule := &AlertRule{ + // Critical: Level{Value: 10, Operator: ">"}, + // Transformer: transformers.NewAggregationTransformer("avg"), + // } + // + // timeSeries := []*tsdb.TimeSeries{ + // tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), + // } + // + // result := executor.evaluateRule(rule, timeSeries) + // So(result.State, ShouldEqual, alertstates.Ok) + // }) + // + // Convey("Show return ok since min is below 10", func() { + // rule := &AlertRule{ + // Critical: Level{Value: 10, Operator: ">"}, + // Transformer: transformers.NewAggregationTransformer("avg"), + // } + // + // timeSeries := []*tsdb.TimeSeries{ + // tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}, {9, 0}}), + // } + // + // result := executor.evaluateRule(rule, timeSeries) + // So(result.State, ShouldEqual, alertstates.Ok) + // }) + // + // Convey("Show return ok since max is above 10", func() { + // rule := &AlertRule{ + // Critical: Level{Value: 10, Operator: ">"}, + // Transformer: transformers.NewAggregationTransformer("max"), + // } + // + // timeSeries := []*tsdb.TimeSeries{ + // tsdb.NewTimeSeries("test1", [][2]float64{{6, 0}, {11, 0}}), + // } + // + // result := executor.evaluateRule(rule, timeSeries) + // So(result.State, ShouldEqual, alertstates.Critical) + // }) + // + // }) + // + // Convey("muliple time series", func() { + // Convey("both are ok", func() { + // rule := &AlertRule{ + // Critical: Level{Value: 10, Operator: ">"}, + // Transformer: transformers.NewAggregationTransformer("avg"), + // } + // + // timeSeries := []*tsdb.TimeSeries{ + // tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), + // tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), + // } + // + // result := executor.evaluateRule(rule, timeSeries) + // So(result.State, ShouldEqual, alertstates.Ok) + // }) + // + // Convey("first serie is good, second is critical", func() { + // rule := &AlertRule{ + // Critical: Level{Value: 10, Operator: ">"}, + // Transformer: transformers.NewAggregationTransformer("avg"), + // } + // + // timeSeries := []*tsdb.TimeSeries{ + // tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), + // tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}}), + // } + // + // result := executor.evaluateRule(rule, timeSeries) + // So(result.State, ShouldEqual, alertstates.Critical) + // }) + // + // Convey("first serie is warn, second is critical", func() { + // rule := &AlertRule{ + // Critical: Level{Value: 10, Operator: ">"}, + // Warning: Level{Value: 5, Operator: ">"}, + // Transformer: transformers.NewAggregationTransformer("avg"), + // } + // + // timeSeries := []*tsdb.TimeSeries{ + // tsdb.NewTimeSeries("test1", [][2]float64{{6, 0}}), + // tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}}), + // } + // + // result := executor.evaluateRule(rule, timeSeries) + // So(result.State, ShouldEqual, alertstates.Critical) + // }) + // }) }) } diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 87ded1a2631..773e02b7fbd 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -1,9 +1,13 @@ package alerting -import "time" +import ( + "time" -type AlertingHandler interface { - Execute(rule *AlertJob, resultChan chan *AlertResult) + "github.com/grafana/grafana/pkg/tsdb" +) + +type AlertHandler interface { + Execute(context *AlertResultContext) } type Scheduler interface { @@ -12,5 +16,18 @@ type Scheduler interface { } type Notifier interface { - Notify(alertResult *AlertResult) + Notify(alertResult *AlertResultContext) + GetType() string +} + +type AlertCondition interface { + Eval(result *AlertResultContext) +} + +type QueryReducer interface { + Reduce(timeSeries *tsdb.TimeSeries) float64 +} + +type AlertEvaluator interface { + Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool } diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index eb7e60784c2..ecd7cec1079 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -1,6 +1,11 @@ package alerting -import "time" +import ( + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" +) type AlertJob struct { Offset int64 @@ -22,18 +27,43 @@ func (aj *AlertJob) IncRetry() { aj.RetryCount++ } -type AlertResult struct { - State string - TriggeredAlerts []*TriggeredAlert - Error error - Description string - StartTime time.Time - EndTime time.Time - - AlertJob *AlertJob +type AlertResultContext struct { + Firing bool + IsTestRun bool + Events []*AlertEvent + Logs []*AlertResultLogEntry + Error error + Description string + StartTime time.Time + EndTime time.Time + Rule *AlertRule + DoneChan chan bool + CancelChan chan bool + log log.Logger } -type TriggeredAlert struct { +func (a *AlertResultContext) GetDurationMs() float64 { + return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000) +} + +func NewAlertResultContext(rule *AlertRule) *AlertResultContext { + return &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"), + } +} + +type AlertResultLogEntry struct { + Message string + Data interface{} +} + +type AlertEvent struct { Value float64 Metric string State string @@ -46,7 +76,7 @@ type Level struct { } type AlertQuery struct { - Query string + Model *simplejson.Json DatasourceId int64 From string To string diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 61ce6460b11..6576a25fd5f 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -1,207 +1,160 @@ package alerting import ( + "errors" "fmt" - "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/services/alerting/alertstates" "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) ShouldDispath(alertResult *AlertResult, 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 *AlertResult) { - notifiers := n.getNotifications(alertResult.AlertJob.Rule.OrgId, alertResult.AlertJob.Rule.NotificationGroups) - - 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 +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) { + 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) + 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", "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 } - if len(query.Result) != 1 { - this.log.Error("Can only support one dashboard", "result", len(query.Result)) - return - } + ruleLink := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slugQuery.Result, context.Rule.PanelId) - 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, + "RuleState": context.Rule.State, + "RuleName": context.Rule.Name, + "Severity": context.Rule.Severity, + "RuleLink": ruleLink, }, - 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 to send 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 *AlertResult) { - 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/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index 4bb24661549..ebe305c6174 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -1,125 +1,114 @@ package alerting -import ( - "testing" - - "reflect" - - "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting/alertstates" - . "github.com/smartystreets/goconvey/convey" -) - -func TestAlertNotificationExtraction(t *testing.T) { - Convey("Notifier tests", t, func() { - Convey("rules for sending notifications", func() { - dummieNotifier := NotifierImpl{} - - result := &AlertResult{ - State: alertstates.Critical, - } - - notifier := &Notification{ - Name: "Test Notifier", - Type: "TestType", - SendCritical: true, - SendWarning: true, - } - - Convey("Should send notification", func() { - So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeTrue) - }) - - Convey("warn:false and state:warn should not send", func() { - result.State = alertstates.Warn - notifier.SendWarning = false - So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeFalse) - }) - }) - - Convey("Parsing alert notification from settings", func() { - Convey("Parsing email", func() { - Convey("empty settings should return error", func() { - json := `{ }` - - settingsJSON, _ := simplejson.NewJson([]byte(json)) - model := &m.AlertNotification{ - Name: "ops", - Type: "email", - Settings: settingsJSON, - } - - _, err := NewNotificationFromDBModel(model) - So(err, ShouldNotBeNil) - }) - - Convey("from settings", func() { - json := ` - { - "to": "ops@grafana.org" - }` - - settingsJSON, _ := simplejson.NewJson([]byte(json)) - model := &m.AlertNotification{ - Name: "ops", - Type: "email", - Settings: settingsJSON, - } - - not, err := NewNotificationFromDBModel(model) - - So(err, ShouldBeNil) - So(not.Name, ShouldEqual, "ops") - So(not.Type, ShouldEqual, "email") - So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.EmailNotifier") - - email := not.Notifierr.(*EmailNotifier) - So(email.To, ShouldEqual, "ops@grafana.org") - }) - }) - - Convey("Parsing webhook", func() { - Convey("empty settings should return error", func() { - json := `{ }` - - settingsJSON, _ := simplejson.NewJson([]byte(json)) - model := &m.AlertNotification{ - Name: "ops", - Type: "webhook", - Settings: settingsJSON, - } - - _, err := NewNotificationFromDBModel(model) - So(err, ShouldNotBeNil) - }) - - Convey("from settings", func() { - json := ` - { - "url": "http://localhost:3000", - "username": "username", - "password": "password" - }` - - settingsJSON, _ := simplejson.NewJson([]byte(json)) - model := &m.AlertNotification{ - Name: "slack", - Type: "webhook", - Settings: settingsJSON, - } - - not, err := NewNotificationFromDBModel(model) - - So(err, ShouldBeNil) - So(not.Name, ShouldEqual, "slack") - So(not.Type, ShouldEqual, "webhook") - So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.WebhookNotifier") - - webhook := not.Notifierr.(*WebhookNotifier) - So(webhook.Url, ShouldEqual, "http://localhost:3000") - }) - }) - }) - }) -} +// func TestAlertNotificationExtraction(t *testing.T) { +// Convey("Notifier tests", t, func() { +// Convey("rules for sending notifications", func() { +// dummieNotifier := NotifierImpl{} +// +// result := &AlertResult{ +// State: alertstates.Critical, +// } +// +// notifier := &Notification{ +// Name: "Test Notifier", +// Type: "TestType", +// SendCritical: true, +// SendWarning: true, +// } +// +// Convey("Should send notification", func() { +// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeTrue) +// }) +// +// Convey("warn:false and state:warn should not send", func() { +// result.State = alertstates.Warn +// notifier.SendWarning = false +// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeFalse) +// }) +// }) +// +// Convey("Parsing alert notification from settings", func() { +// Convey("Parsing email", func() { +// Convey("empty settings should return error", func() { +// json := `{ }` +// +// settingsJSON, _ := simplejson.NewJson([]byte(json)) +// model := &m.AlertNotification{ +// Name: "ops", +// Type: "email", +// Settings: settingsJSON, +// } +// +// _, err := NewNotificationFromDBModel(model) +// So(err, ShouldNotBeNil) +// }) +// +// Convey("from settings", func() { +// json := ` +// { +// "to": "ops@grafana.org" +// }` +// +// settingsJSON, _ := simplejson.NewJson([]byte(json)) +// model := &m.AlertNotification{ +// Name: "ops", +// Type: "email", +// Settings: settingsJSON, +// } +// +// not, err := NewNotificationFromDBModel(model) +// +// So(err, ShouldBeNil) +// So(not.Name, ShouldEqual, "ops") +// So(not.Type, ShouldEqual, "email") +// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.EmailNotifier") +// +// email := not.Notifierr.(*EmailNotifier) +// So(email.To, ShouldEqual, "ops@grafana.org") +// }) +// }) +// +// Convey("Parsing webhook", func() { +// Convey("empty settings should return error", func() { +// json := `{ }` +// +// settingsJSON, _ := simplejson.NewJson([]byte(json)) +// model := &m.AlertNotification{ +// Name: "ops", +// Type: "webhook", +// Settings: settingsJSON, +// } +// +// _, err := NewNotificationFromDBModel(model) +// So(err, ShouldNotBeNil) +// }) +// +// Convey("from settings", func() { +// json := ` +// { +// "url": "http://localhost:3000", +// "username": "username", +// "password": "password" +// }` +// +// settingsJSON, _ := simplejson.NewJson([]byte(json)) +// model := &m.AlertNotification{ +// Name: "slack", +// Type: "webhook", +// Settings: settingsJSON, +// } +// +// not, err := NewNotificationFromDBModel(model) +// +// So(err, ShouldBeNil) +// So(not.Name, ShouldEqual, "slack") +// So(not.Type, ShouldEqual, "webhook") +// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.WebhookNotifier") +// +// webhook := not.Notifierr.(*WebhookNotifier) +// So(webhook.Url, ShouldEqual, "http://localhost:3000") +// }) +// }) +// }) +// }) +// } 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 45cb1bde1a6..25643f707db 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -1,16 +1,13 @@ package alerting 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" ) type ResultHandler interface { - Handle(result *AlertResult) + Handle(result *AlertResultContext) } type ResultHandlerImpl struct { @@ -20,49 +17,37 @@ type ResultHandlerImpl struct { func NewResultHandler() *ResultHandlerImpl { return &ResultHandlerImpl{ - log: log.New("alerting.responseHandler"), - notifier: NewNotifier(), + log: log.New("alerting.resultHandler"), + notifier: NewRootNotifier(), } } -func (handler *ResultHandlerImpl) Handle(result *AlertResult) { - if handler.shouldUpdateState(result) { - cmd := &m.UpdateAlertStateCommand{ - AlertId: result.AlertJob.Rule.Id, - State: result.State, - Info: result.Description, - OrgId: result.AlertJob.Rule.OrgId, - TriggeredAlerts: simplejson.NewFromAny(result.TriggeredAlerts), +func (handler *ResultHandlerImpl) Handle(result *AlertResultContext) { + 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 + } + + if result.Rule.State != newState { + handler.log.Info("New state change", "alertId", result.Rule.Id, "newState", newState, "oldState", result.Rule.State) + + cmd := &m.SetAlertStateCommand{ + AlertId: result.Rule.Id, + OrgId: result.Rule.OrgId, + State: newState, } 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) + result.Rule.State = newState handler.notifier.Notify(result) } } - -func (handler *ResultHandlerImpl) shouldUpdateState(result *AlertResult) 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 -} diff --git a/pkg/services/alerting/result_handler_test.go b/pkg/services/alerting/result_handler_test.go index f44049ecb6d..32589bef172 100644 --- a/pkg/services/alerting/result_handler_test.go +++ b/pkg/services/alerting/result_handler_test.go @@ -1,59 +1,58 @@ 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, - }, - }, - } - mockAlertState := &m.AlertState{} - bus.ClearBusHandlers() - bus.AddHandler("test", func(query *m.GetLastAlertStateQuery) error { - query.Result = mockAlertState - return nil - }) - - Convey("Should update", func() { - - Convey("when no earlier alert state", func() { - mockAlertState = nil - So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) - }) - - Convey("alert state have changed", func() { - mockAlertState = &m.AlertState{ - NewState: alertstates.Critical, - } - mockResult.State = alertstates.Ok - So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) - }) - - Convey("last alert state was 15min ago", func() { - now := time.Now() - mockAlertState = &m.AlertState{ - NewState: alertstates.Critical, - Created: now.Add(time.Minute * -30), - } - mockResult.State = alertstates.Critical - mockResult.ExeuctionTime = time.Now() - So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) - }) - }) - }) -} +// 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 := &AlertResultContext{ +// Triggered: false, +// Rule: &AlertRule{ +// Id: 1, +// OrgId 1, +// }, +// } +// mockAlertState := &m.AlertState{} +// bus.ClearBusHandlers() +// bus.AddHandler("test", func(query *m.GetLastAlertStateQuery) error { +// query.Result = mockAlertState +// return nil +// }) +// +// Convey("Should update", func() { +// +// Convey("when no earlier alert state", func() { +// mockAlertState = nil +// So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) +// }) +// +// Convey("alert state have changed", func() { +// mockAlertState = &m.AlertState{ +// State: alertstates.Critical, +// } +// mockResult.Triggered = false +// So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) +// }) +// +// Convey("last alert state was 15min ago", func() { +// now := time.Now() +// mockAlertState = &m.AlertState{ +// State: alertstates.Critical, +// Created: now.Add(time.Minute * -30), +// } +// mockResult.Triggered = true +// mockResult.StartTime = time.Now() +// So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) +// }) +// }) +// }) +// } diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go new file mode 100644 index 00000000000..4583202c9db --- /dev/null +++ b/pkg/services/alerting/test_rule.go @@ -0,0 +1,57 @@ +package alerting + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" +) + +type AlertTestCommand struct { + Dashboard *simplejson.Json + PanelId int64 + OrgId int64 + + Result *AlertResultContext +} + +func init() { + bus.AddHandler("alerting", handleAlertTestCommand) +} + +func handleAlertTestCommand(cmd *AlertTestCommand) error { + + dash := m.NewDashboardFromJson(cmd.Dashboard) + + extractor := NewDashAlertExtractor(dash, cmd.OrgId) + alerts, err := extractor.GetAlerts() + if err != nil { + return err + } + + for _, alert := range alerts { + if alert.PanelId == cmd.PanelId { + rule, err := NewAlertRuleFromDBModel(alert) + if err != nil { + return err + } + + cmd.Result = testAlertRule(rule) + return nil + } + } + + return fmt.Errorf("Could not find alert with panel id %d", cmd.PanelId) +} + +func testAlertRule(rule *AlertRule) *AlertResultContext { + handler := NewHandler() + + context := NewAlertResultContext(rule) + context.IsTestRun = true + + handler.Execute(context) + + return context +} 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) -} 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_notification.go b/pkg/services/sqlstore/alert_notification.go index 3f4b9a03406..e94e04e696e 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -3,7 +3,7 @@ package sqlstore import ( "bytes" "fmt" - "strconv" + "strings" "time" "github.com/go-xorm/xorm" @@ -31,11 +31,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 +43,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 +60,28 @@ 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(` AND alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")") + for _, v := range query.Ids { + params = append(params, v) } - - sql.WriteString(`)`) } - 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 +93,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 +112,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/pkg/services/sqlstore/alert_state.go b/pkg/services/sqlstore/alert_state.go index d335f7402a7..38c14b077b5 100644 --- a/pkg/services/sqlstore/alert_state.go +++ b/pkg/services/sqlstore/alert_state.go @@ -1,78 +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(), - TriggeredAlerts: cmd.TriggeredAlerts, - } - - 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/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 3707740ae9a..a228f186da9 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -19,6 +19,7 @@ func addAlertMigrations(mg *Migrator) { {Name: "settings", Type: DB_Text, Nullable: false}, {Name: "frequency", Type: DB_BigInt, Nullable: false}, {Name: "handler", Type: DB_BigInt, Nullable: false}, + {Name: "severity", Type: DB_Text, Nullable: false}, {Name: "enabled", Type: DB_Bool, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, @@ -64,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/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 98f4c648048..2a68b23df71 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -31,16 +31,15 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC result := &tsdb.BatchResult{} params := url.Values{ - "from": []string{formatTimeRange(context.TimeRange.From)}, - "until": []string{context.TimeRange.To}, + "from": []string{"-" + formatTimeRange(context.TimeRange.From)}, + "until": []string{formatTimeRange(context.TimeRange.To)}, "format": []string{"json"}, "maxDataPoints": []string{"500"}, } for _, query := range queries { - params["target"] = []string{ - query.Query, - } + params["target"] = []string{query.Query} + glog.Debug("Graphite request", "query", query.Query) } client := http.Client{Timeout: time.Duration(10 * time.Second)} @@ -79,5 +78,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC } func formatTimeRange(input string) string { + if input == "now" { + return input + } return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1) } diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 29e0ff2cd32..2954c630b68 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -46,8 +46,8 @@ type QueryResult struct { } type TimeSeries struct { - Name string - Points [][2]float64 + Name string `json:"name"` + Points [][2]float64 `json:"points"` } type TimeSeriesSlice []*TimeSeries diff --git a/pkg/tsdb/request.go b/pkg/tsdb/request.go index 2c96a3ff3ce..2e5e5eec25a 100644 --- a/pkg/tsdb/request.go +++ b/pkg/tsdb/request.go @@ -1,5 +1,7 @@ package tsdb +type HandleRequestFunc func(req *Request) (*Response, error) + func HandleRequest(req *Request) (*Response, error) { context := NewQueryContext(req.Queries, req.TimeRange) diff --git a/public/app/core/components/jsontree/jsontree.ts b/public/app/core/components/jsontree/jsontree.ts new file mode 100644 index 00000000000..14f6012ce12 --- /dev/null +++ b/public/app/core/components/jsontree/jsontree.ts @@ -0,0 +1,200 @@ + +/** 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.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/public/app/core/core.ts b/public/app/core/core.ts index 4dae139b66d..1174e267f4e 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -19,6 +19,7 @@ import "./directives/rebuild_on_change"; import "./directives/give_focus"; import './jquery_extended'; import './partials'; +import './components/jsontree/jsontree'; import {grafanaAppDirective} from './components/grafana_app'; import {sideMenuDirective} from './components/sidemenu/sidemenu'; diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 96fb3394aea..547a22dfd7a 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -212,7 +212,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/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 9682afdb39e..135f6aa1e21 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -16,7 +16,7 @@ export class AlertSrv { init() { this.$rootScope.onAppEvent('alert-error', (e, alert) => { - this.set(alert[0], alert[1], 'error', 0); + this.set(alert[0], alert[1], 'error', 7000); }, this.$rootScope); this.$rootScope.onAppEvent('alert-warning', (e, alert) => { 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/alert_log_ctrl.ts b/public/app/features/alerting/alert_log_ctrl.ts index 2727f486604..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.newState); + 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 7294ce69166..17d048cee85 100644 --- a/public/app/features/alerting/alerts_ctrl.ts +++ b/public/app/features/alerting/alerts_ctrl.ts @@ -27,11 +27,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 +38,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 +48,7 @@ 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.getSeverityIconClass(alert.severity); return alert; }); }); diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 43dceea8dc2..0b959945215 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -6,52 +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 = { - settings: { - sendCrit: true, - sendWarn: true, - } + 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 29641aa6c21..dbae224a0cc 100644 --- a/public/app/features/alerting/partials/alert_list.html +++ b/public/app/features/alerting/partials/alert_list.html @@ -1,4 +1,4 @@ - +
@@ -7,28 +7,29 @@
- + -
+ +
Name StateSeverity
- + {{alert.name}} - - - + {{alert.state}} + + {{alert.severity}} 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}} -
-
- diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index fea91e5045a..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 +
@@ -6,51 +10,45 @@

Alert notification

-
+
Name - +
Type -
+
-
- -
-
- -
-
- -
-
+ +
+

Webhook settings

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

Email addresses

- To - +
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/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 2578f17ffca..a8cac86a6ad 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, @@ -19,72 +17,107 @@ 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; + testing: boolean; + testResult: any; + 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: 'Other alert', value: 'other_alert'}, + {text: 'Time of day', value: 'time_of_day'}, + {text: 'Day of week', value: 'day_of_week'}, ]; - aggregators = ['avg', 'sum', 'min', 'max', 'last']; alert: any; - thresholds: any; - query: any; - queryParams: any; - transformDef: any; - levelOpList = [ + conditionModels: any; + evalFunctions = [ {text: '>', value: '>'}, {text: '<', value: '<'}, - {text: '=', value: '='}, ]; + severityLevels = [ + {text: 'Critical', value: 'critical'}, + {text: 'Warning', value: 'warning'}, + ]; + addNotificationSegment; + notifications; + alertNotifications; /** @ngInject */ - constructor($scope, private $timeout) { + 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; + } + + $onInit() { + this.addNotificationSegment = this.uiSegmentSrv.newPlusButton(); - this.metricTargets = this.panel.targets.map(val => val); this.initModel(); // set panel alert edit mode - $scope.$on("$destroy", () => { + this.$scope.$on("$destroy", () => { this.panelCtrl.editingAlert = false; this.panelCtrl.render(); }); + + // build notification model + this.notifications = []; + this.alertNotifications = []; + + return this.backendSrv.get('/api/alert-notifications').then(res => { + this.notifications = res; + + _.each(this.alert.notifications, item => { + var model = _.findWhere(this.notifications, {id: item.id}); + if (model) { + this.alertNotifications.push(model); + } + }); + }); } - getThresholdWithDefaults(threshold) { - threshold = threshold || {}; - threshold.op = threshold.op || '>'; - threshold.value = threshold.value || undefined; - return threshold; + getNotifications() { + return Promise.resolve(this.notifications.map(item => { + return this.uiSegmentSrv.newSegment(item.name); + })); + } + + notificationAdded() { + 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; + this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html; + } + + removeNotification(index) { + this.alert.notifications.splice(index, 1); + this.alertNotifications.splice(index, 1); } 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.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.severity = alert.severity || 'critical'; alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; @@ -93,50 +126,87 @@ 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] - }; + this.conditionModels = _.reduce(alert.conditions, (memo, value) => { + memo.push(this.buildConditionModel(value)); + return memo; + }, []); - // init the query part components model - this.query = new QueryPart(this.queryParams, alertQueryDef); - this.transformDef = _.findWhere(this.transforms, {type: alert.transform.type}); - - this.panelCtrl.editingAlert = true; + ///this.panelCtrl.editingAlert = true; + this.syncThresholds(); this.panelCtrl.render(); } - queryUpdated() { - this.alert.query = { - refId: this.query.params[0], - from: this.query.params[1], - to: this.query.params[2], + 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', + query: {params: ['A', '5m', 'now']}, + reducer: {type: 'avg', params: []}, + evaluator: {type: '>', params: [null]}, }; } - 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; - } - } + cm.queryPart = new QueryPart(source.query, alertQueryDef); + cm.reducerPart = new QueryPart({params: []}, reducerAvgDef); + cm.evaluator = source.evaluator; + + return cm; + } + + queryPartUpdated(conditionModel) { + } + + 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() { this.alert.enabled = false; - this.alert.warn.value = undefined; - this.alert.crit.value = undefined; - - // reset model but keep thresholds instance this.initModel(); } @@ -145,8 +215,24 @@ export class AlertTabCtrl { this.initModel(); } - thresholdsUpdated() { - this.panelCtrl.render(); + thresholdUpdated() { + if (this.syncThresholds()) { + this.panelCtrl.render(); + } + } + + test() { + this.testing = true; + + var payload = { + dashboard: this.dashboardSrv.getCurrent().getSaveModelClone(), + panelId: this.panelCtrl.panel.id, + }; + + return this.backendSrv.post('/api/alerts/test', payload).then(res => { + this.testResult = res; + this.testing = false; + }); } } 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 75aa05ee57c..6e00bef084a 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 */ @@ -132,7 +133,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 4b76648a845..463e2cb4154 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,147 +1,140 @@ -
-
-
Visual Thresholds
-
-
- - - Critcal if - - - -
-
- - - Warn if - - - -
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + +
-
-
-
Alert Query
-
-
- - -
-
- Transform using -
- -
-
-
- Method -
- -
-
-
- Timespan - -
-
-
+
+
Alert Rule
+
+
+ Name + +
+
+ Evaluate every + +
+
+ Severity +
+ +
+
+
+
-
-
Thresholds
-
-
- - - Critcal if - - - -
-
- - - Warn if - - - -
-
-
-
- -
-
-
Execution
-
-
- Handler -
- -
-
-
- Evaluate every - -
-
-
-
-
Notifications
-
-
- Groups - - -
-
-
-
- -
-
Information
-
- Alert name - -
-
+
+
Conditions
+
- Alert description + AND + WHEN
- + +
+
+ Reducer + + +
+
+ Value + + +
+
+ +
-
+ +
+ +
+
+ +
+
Notifications
+
+
+ + {{nc.name}} + + + +
+
+
+ +
+
+ + + +
+
+
-
-
- - -
+
+ Evaluating rule +
+ +
+ +
+ +
+
+ +
diff --git a/public/app/plugins/panel/graph/thresholds.ts b/public/app/plugins/panel/graph/thresholds.ts index ee413fd990f..9ec7a5efb53 100644 --- a/public/app/plugins/panel/graph/thresholds.ts +++ b/public/app/plugins/panel/graph/thresholds.ts @@ -8,10 +8,10 @@ export class ThresholdControls { plot: any; placeholder: any; height: any; - alert: any; + thresholds: any; constructor(private panelCtrl) { - this.alert = this.panelCtrl.panel.alert; + this.thresholds = this.panelCtrl.panel.thresholds; } getHandleInnerHtml(type, op, value) { @@ -120,8 +120,9 @@ export class ThresholdControls { this.placeholder = plot.getPlaceholder(); this.height = plot.height(); - this.renderHandle('crit', this.alert.crit, 10); - this.renderHandle('warn', this.alert.warn, this.height-30); + if (this.thresholds.length > 0) { + this.renderHandle('crit', this.thresholds[0], 10); + } } } 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;
Status
- + {{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; 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..011566f8731 --- /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, & > .json-tree-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: 8px; + 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; + } +} 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%); }