Merge branch 'alert_conditions' into alerting

This commit is contained in:
Torkel Ödegaard 2016-07-27 09:52:42 +02:00
commit cde1bbff78
64 changed files with 2354 additions and 1996 deletions

View File

@ -1,31 +1,12 @@
<!-- This email is sent when an existing user is added to an organization -->
[[Subject .Subject "Grafana Alert: [[.Severity]] [[.RuleName]]"]]
[[Subject .Subject "Grafana Alert: [ [[.State]] ] [[.Name]]" ]]
<br>
<br>
Alertstate: [[.State]]<br />
[[.AlertPageUrl]]<br />
[[.DashboardLink]]<br />
[[.Description]]<br />
Alert rule: [[.RuleName]]<br>
Alert state: [[.RuleState]]<br>
[[if eq .State "Ok"]]
Everything is Ok
[[end]]
<a href="[[.RuleLink]]">Link to alert rule</a>
<img src="[[.DashboardImage]]" />
<br>
[[if ne .State "Ok" ]]
<table class="row">
<tr>
<td class="expander">Serie</td>
<td class="expander">State</td>
<td class="expander">Actual value</td>
</tr>
[[ range $ta := .TriggeredAlerts]]
<tr>
<td class="expander">[[$ta.Name]]</td>
<td class="expander">[[$ta.State]]</td>
<td class="expander">[[$ta.ActualValue]]</td>
</tr>
[[end]]
</table>
[[end]]

View File

@ -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")
}

View File

@ -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

View File

@ -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"`
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
// }

22
pkg/models/annotations.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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)
})
})
})
}

View File

@ -1,16 +0,0 @@
package alertstates
var (
ValidStates = []string{
Ok,
Warn,
Critical,
Unknown,
}
Ok = "OK"
Warn = "WARN"
Critical = "CRITICAL"
Pending = "PENDING"
Unknown = "UNKNOWN"
)

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)")
})
})
})
})

View File

@ -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}
// }

View File

@ -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)
// })
// })
})
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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")
// })
// })
// })
// })
// }

View File

@ -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

View File

@ -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
}

View File

@ -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)
// })
// })
// })
// }

View File

@ -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
}

View File

@ -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")
}

View File

@ -1,7 +0,0 @@
package transformers
import "github.com/grafana/grafana/pkg/tsdb"
type Transformer interface {
Transform(timeserie *tsdb.TimeSeries) (float64, error)
}

View File

@ -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
})
}

View File

@ -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(&current); 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 = &current
return nil
})
}

View File

@ -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
// }

View File

@ -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)
// })
// })
// })
// })
// }

View File

@ -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},

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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
* <json-tree object="someObject"></json-tree>
*
* 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: '<json-node key="rootName" value="object" start-expanded="startExpanded"></json-node>'
};
}]);
coreModule.directive('jsonNode', ['ajsRecursiveDirectiveHelper', function jsonNodeDirective(ajsRecursiveDirectiveHelper) {
return {
restrict: 'E',
scope: {
key: '=',
value: '=',
startExpanded: '@'
},
compile: function jsonNodeDirectiveCompile(elem) {
return ajsRecursiveDirectiveHelper.compile(elem, this);
},
template: ' <span class="json-tree-key" ng-click="toggleExpanded()">{{key}}</span>' +
' <span class="json-tree-leaf-value" ng-if="!isExpandable">{{value}}</span>' +
' <span class="json-tree-branch-preview" ng-if="isExpandable" ng-show="!isExpanded" ng-click="toggleExpanded()">' +
' {{preview}}</span>' +
' <ul class="json-tree-branch-value" ng-if="isExpandable && shouldRender" ng-show="isExpanded">' +
' <li ng-repeat="(subkey,subval) in value">' +
' <json-node key="subkey" value="subval"></json-node>' +
' </li>' +
' </ul>',
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);
}
}
};
}
};
}]);

View File

@ -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';

View File

@ -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',

View File

@ -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) => {

View File

@ -1,16 +1,15 @@
///<reference path="../../headers/common.d.ts" />
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,
};

View File

@ -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;
});

View File

@ -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;
});
});

View File

@ -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');
});
}
}
}

View File

@ -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;
});
});
}
}

View File

@ -1,4 +1,4 @@
<navbar icon="fa fa-fw fa-list" title="Alerting" title-url="alerting">
<navbar icon="icon-gf icon-gf-monitoring" title="Alerting" title-url="alerting">
</navbar>
<div class="page-container" >
@ -7,28 +7,29 @@
</div>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" label="Ok" label-class="width-5" checked="ctrl.filter.ok" on-change="ctrl.updateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="OK" label-class="width-5" checked="ctrl.filter.ok" on-change="ctrl.updateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Warn" label-class="width-5" checked="ctrl.filter.warn" on-change="ctrl.updateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Critical" label-class="width-5" checked="ctrl.filter.critical" on-change="ctrl.updateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Acknowleged" label-class="width-7" checked="ctrl.filter.acknowleged" on-change="ctrl.updateFilter()"></gf-form-switch>
</div>
<table class="grafana-options-table">
<thead>
<th style="min-width: 200px"><strong>Name</strong></th>
<th style="width: 1%">State</th>
<th style="width: 1%">Severity</th>
<th style="width: 1%"></th>
</thead>
<tr ng-repeat="alert in ctrl.alerts">
<td>
<a href="alerting/{{alert.id}}/states">
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&editorTab=Alerting">
{{alert.name}}
</a>
</td>
<td class="text-center">
<a href="alerting/{{alert.id}}/states">
<i class="icon-gf {{alert.iconCss}}"></i>
</a>
{{alert.state}}
</td>
<td class="text-center">
{{alert.severity}}
</td>
<td class="text-center">
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&editorTab=Alerting" class="btn btn-inverse btn-small">

View File

@ -6,55 +6,6 @@
<h1>Alert history for {{ctrl.alert.title}}</h1>
</div>
<div class="gf-form-group section" >
<h5 class="section-heading">Thresholds</h5>
<div class="gf-form">
<span class="gf-form-label width-9">
<i class="icon-gf icon-gf-warn alert-icon-warn"></i>
Warn level
</span>
<div class="gf-form-label max-width-10">
{{ctrl.alert.warnOperator}}
</div>
<div class="gf-form-label max-width-10">
{{ctrl.alert.warnLevel}}
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-9">
<i class="icon-gf icon-gf-critical alert-icon-critical"></i>
Critical level
</span>
<div class="gf-form-label max-width-10">
{{ctrl.alert.critOperator}}
</div>
<div class="gf-form-label max-width-10">
{{ctrl.alert.critLevel}}
</div>
</div>
</div>
<div class="gf-form-group section" >
<h5 class="section-heading">Aggregators</h5>
<div class="gf-form">
<span class="gf-form-label width-12">
Aggregator
</span>
<div class="gf-form-label max-width-10">
{{ctrl.alert.aggregator}}
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-12">Query range (seconds)</span>
<span class="gf-form-label width-10">{{ctrl.alert.queryRange}}</span>
</div>
<div class="gf-form">
<span class="gf-form-label width-12">Frequency (seconds)</span>
<span class="gf-form-label width-10">{{ctrl.alert.frequency}}</span>
</div>
</div>
<table class="filter-table">
<thead>
<th style="width: 68px">Status</th>

View File

@ -1,4 +1,8 @@
<navbar icon="fa fa-fw fa-list" title="Alerting" title-url="alerting">
<navbar icon="icon-gf icon-gf-monitoring" title="Alerting" title-url="alerting">
<a href="alerting/notifications" class="navbar-page-btn">
<i class="fa fa-fw fa-envelope-o"></i>
Notifications
</a>
</navbar>
<div class="page-container" >
@ -6,51 +10,45 @@
<h1>Alert notification</h1>
</div>
<div class="gf-form-group section">
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-8">Name</span>
<input type="text" class="gf-form-input max-width-12" ng-model="ctrl.notification.name"></input>
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Type</span>
<div class="gf-form-select-wrapper width-12">
<div class="gf-form-select-wrapper width-15">
<select class="gf-form-input"
ng-model="ctrl.notification.type"
ng-model="ctrl.model.type"
ng-options="t for t in ['webhook', 'email']"
ng-change="ctrl.typeChanged(notification, $index)">
</select>
</div>
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label-class="width-8" label="Always execute" checked="ctrl.notification.alwaysExecute" on-change=""></gf-form-switch>
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label-class="width-8" label="Send Warning" checked="ctrl.notification.settings.sendWarn" on-change=""></gf-form-switch>
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label-class="width-8" label="Send Critical" checked="ctrl.notification.settings.sendCrit" on-change=""></gf-form-switch>
</div>
</div>
<div class="gf-form-group section" ng-show="ctrl.notification.type === 'webhook'">
<div class="gf-form-group" ng-show="ctrl.model.type === 'webhook'">
<h3 class="page-heading">Webhook settings</h3>
<div class="gf-form">
<span class="gf-form-label width-6">Url</span>
<input type="text" class="gf-form-input max-width-26" ng-model="ctrl.notification.settings.url"></input>
<input type="text" class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-6">Username</span>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.notification.settings.username"></input>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.username"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-6">Password</span>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.notification.settings.password"></input>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.password"></input>
</div>
</div>
</div>
<div class="gf-form-group section" ng-show="ctrl.notification.type === 'email'">
<div class="gf-form-group section" ng-show="ctrl.model.type === 'email'">
<h3 class="page-heading">Email addresses</h3>
<div class="gf-form">
<span class="gf-form-label width-8">To</span>
<input type="text" class="gf-form-input max-width-26" ng-model="ctrl.notification.settings.to">
<textarea rows="7" class="gf-form-input width-25" ng-model="ctrl.model.settings.addresses"></textarea>
</div>
</div>

View File

@ -1,4 +1,8 @@
<navbar icon="fa fa-fw fa-list" title="Alerting" title-url="alerting">
<navbar icon="icon-gf icon-gf-monitoring" title="Alerting" title-url="alerting">
<a href="alerting/notifications" class="navbar-page-btn">
<i class="fa fa-fw fa-envelope-o"></i>
Notifications
</a>
</navbar>
<div class="page-container" >

View File

@ -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();
};

View File

@ -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();
}

View File

@ -1,8 +1,6 @@
///<reference path="../../../headers/common.d.ts" />
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;
});
}
}

View File

@ -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) {

View File

@ -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 = {

View File

@ -1,147 +1,140 @@
<div ng-if="!ctrl.alert.enabled">
<div class="gf-form-group">
<h5 class="section-heading">Visual Thresholds</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">
<i class="icon-gf icon-gf-warn alert-icon-critical"></i>
Critcal if
</span>
<metric-segment-model property="ctrl.alert.crit.op" options="ctrl.operatorList" custom="false" css-class="query-segment-operator" on-change="ctrl.thresholdsUpdated()"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.crit.value" ng-change="ctrl.thresholdsUpdated()"></input>
</div>
<div class="gf-form">
<span class="gf-form-label">
<i class="icon-gf icon-gf-warn alert-icon-warn"></i>
Warn if
</span>
<metric-segment-model property="ctrl.alert.warn.op" options="ctrl.operatorList" custom="false" css-class="query-segment-operator" on-change="ctrl.thresholdsUpdated()"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.warn.value" ng-change="ctrl.thresholdsUpdated()"></input>
</div>
</div>
</div>
</div>
<!-- <div ng&#45;if="!ctrl.alert.enabled"> -->
<!-- <div class="gf&#45;form&#45;group"> -->
<!-- <h5 class="section&#45;heading">Visual Thresholds</h5> -->
<!-- <div class="gf&#45;form&#45;inline"> -->
<!-- <div class="gf&#45;form"> -->
<!-- <span class="gf&#45;form&#45;label"> -->
<!-- <i class="icon&#45;gf icon&#45;gf&#45;warn alert&#45;icon&#45;critical"></i> -->
<!-- Critcal if -->
<!-- </span> -->
<!-- <metric&#45;segment&#45;model property="ctrl.alert.crit.op" options="ctrl.operatorList" custom="false" css&#45;class="query&#45;segment&#45;operator" on&#45;change="ctrl.thresholdsUpdated()"></metric&#45;segment&#45;model> -->
<!-- <input class="gf&#45;form&#45;input max&#45;width&#45;7" type="number" ng&#45;model="ctrl.alert.crit.value" ng&#45;change="ctrl.thresholdsUpdated()"></input> -->
<!-- </div> -->
<!-- <div class="gf&#45;form"> -->
<!-- <span class="gf&#45;form&#45;label"> -->
<!-- <i class="icon&#45;gf icon&#45;gf&#45;warn alert&#45;icon&#45;warn"></i> -->
<!-- Warn if -->
<!-- </span> -->
<!-- <metric&#45;segment&#45;model property="ctrl.alert.warn.op" options="ctrl.operatorList" custom="false" css&#45;class="query&#45;segment&#45;operator" on&#45;change="ctrl.thresholdsUpdated()"></metric&#45;segment&#45;model> -->
<!-- <input class="gf&#45;form&#45;input max&#45;width&#45;7" type="number" ng&#45;model="ctrl.alert.warn.value" ng&#45;change="ctrl.thresholdsUpdated()"></input> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
<div ng-if="ctrl.alert.enabled">
<div class="editor-row">
<div class="gf-form-group section" >
<h5 class="section-heading">Alert Query</h5>
<div class="gf-form-inline">
<div class="gf-form">
<query-part-editor
class="gf-form-label query-part"
part="ctrl.query"
part-updated="ctrl.queryUpdated()">
</query-part-editor>
</div>
<div class="gf-form">
<span class="gf-form-label">Transform using</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-model="ctrl.alert.transform.type"
ng-options="f.type as f.text for f in ctrl.transforms"
ng-change="ctrl.transformChanged()"
>
</select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.transformDef.type === 'aggregation'">
<span class="gf-form-label">Method</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-model="ctrl.alert.transform.method"
ng-options="f for f in ctrl.aggregators">
</select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.transformDef.type === 'forecast'">
<span class="gf-form-label">Timespan</span>
<input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
</div>
</div>
</div>
<div class="gf-form-group">
<h5 class="section-heading">Alert Rule</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">Name</span>
<input type="text" class="gf-form-input width-22" ng-model="ctrl.alert.name">
</div>
<div class="gf-form">
<span class="gf-form-label">Evaluate every</span>
<input class="gf-form-input max-width-7" type="text" ng-model="ctrl.alert.frequency"></input>
</div>
<div class="gf-form">
<span class="gf-form-label">Severity</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.alert.severity" ng-options="f.value as f.text for f in ctrl.severityLevels">
</select>
</div>
</div>
</div>
</div>
<div class="gf-form-group section">
<h5 class="section-heading">Thresholds</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">
<i class="icon-gf icon-gf-warn alert-icon-critical"></i>
Critcal if
</span>
<metric-segment-model property="ctrl.alert.crit.op" options="ctrl.operatorList" custom="false" css-class="query-segment-operator" on-change="ctrl.thresholdsUpdated()"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.crit.value" ng-change="ctrl.thresholdsUpdated()"></input>
</div>
<div class="gf-form">
<span class="gf-form-label">
<i class="icon-gf icon-gf-warn alert-icon-warn"></i>
Warn if
</span>
<metric-segment-model property="ctrl.alert.warn.op" options="ctrl.operatorList" custom="false" css-class="query-segment-operator" on-change="ctrl.thresholdsUpdated()"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.warn.value" ng-change="ctrl.thresholdsUpdated()"></input>
</div>
</div>
</div>
</div>
<div class="editor-row">
<div class="gf-form-group section">
<h5 class="section-heading">Execution</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">Handler</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-model="ctrl.alert.handler"
ng-options="f.value as f.text for f in ctrl.handlers">
</select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label">Evaluate every</span>
<input class="gf-form-input max-width-7" type="text" ng-model="ctrl.alert.frequency"></input>
</div>
</div>
</div>
<div class="gf-form-group section">
<h5 class="section-heading">Notifications</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">Groups</span>
<input class="gf-form-input max-width-7" type="text" ng-model="ctrl.alert.notify"></input>
<!--
<bootstrap-tagsinput ng-model="ctrl.alert.notify" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
-->
</div>
</div>
</div>
</div>
<div class="gf-form-group section">
<h5 class="section-heading">Information</h5>
<div class="gf-form">
<span class="gf-form-label width-10">Alert name</span>
<input type="text" class="gf-form-input width-22" ng-model="ctrl.alert.name">
</div>
<div class="gf-form-inline">
<div class="gf-form-group">
<h5 class="section-heading">Conditions</h5>
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
<div class="gf-form">
<span class="gf-form-label width-10" style="margin-top: -73px;">Alert description</span>
<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div>
<div class="gf-form">
<textarea rows="5" ng-model="ctrl.alert.description" class="gf-form-input width-22"></textarea>
<query-part-editor
class="gf-form-label query-part"
part="conditionModel.queryPart"
part-updated="ctrl.queryPartUpdated(conditionModel)">
</query-part-editor>
</div>
<div class="gf-form">
<span class="gf-form-label">Reducer</span>
<query-part-editor
class="gf-form-label query-part"
part="conditionModel.reducerPart"
part-updated="ctrl.reducerPartUpdated(conditionModel)">
</query-part-editor>
</div>
<div class="gf-form">
<span class="gf-form-label">Value</span>
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-segment-operator" on-change="ctrl.thresholdUpdated()"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.thresholdUpdated()"></input>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
<i class="fa fa-trash"></i>
</a>
</label>
</div>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-plus"></i>
</a>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
</li>
</ul>
</label>
</div>
</div>
<div class="gf-form-group">
<h5 class="section-heading">Notifications</h5>
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications">
{{nc.name}}
<i class="fa fa-remove pointer" ng-click="ctrl.removeNotification($index)"></i>
</span>
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
</div>
</div>
</div>
<div class="gf-form-group">
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.test()">
Test Rule
</button>
<button class="btn btn-inverse" ng-click="ctrl.delete()">
Delete Alert
</button>
</div>
</div>
</div>
<div class="editor-row">
<div class="gf-form-button-row">
<button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.alert.enabled">Delete</button>
<button class="btn btn-inverse" ng-click="ctrl.enable()" ng-hide="ctrl.alert.enabled">
<i class="icon-gf icon-gf-alert"></i>
Create Alert
</button>
</div>
<div class="gf-form-group" ng-if="ctrl.testing">
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
</div>
<div class="gf-form-group" ng-if="ctrl.testResult">
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
</div>
<div class="gf-form-group" ng-if="!ctrl.alert.enabled">
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.enable()">
<i class="icon-gf icon-gf-alert"></i>
Create Alert
</button>
</div>
</div>

View File

@ -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);
}
}
}

View File

@ -113,37 +113,18 @@ color: #FFFFFF !important;
<table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
{{Subject .Subject "Grafana Alert: {{.Severity}} {{.RuleName}}"}}
{{Subject .Subject "Grafana Alert: [ {{.State}} ] {{.Name}}" }}
<br />
<br />
Alertstate: {{.State}}<br />
{{.AlertPageUrl}}<br />
{{.DashboardLink}}<br />
{{.Description}}<br />
Alert rule: {{.RuleName}}<br />
Alert state: {{.RuleState}}<br />
{{if eq .State "Ok"}}
Everything is Ok
{{end}}
<a href="{{.RuleLink}}" style="color: #E67612; text-decoration: none">Link to alert rule</a>
{{if ne .State "Ok" }}
<img src="{{.DashboardImage}}" style="-ms-interpolation-mode: bicubic; clear: both; display: block; float: left; max-width: 100%; outline: none; text-decoration: none; width: auto" align="left" />
<br />
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">Serie</td>
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">State</td>
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">Actual value</td>
</tr>
{{ range $ta := .TriggeredAlerts}}
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">{{$ta.Name}}</td>
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">{{$ta.State}}</td>
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">{{$ta.ActualValue}}</td>
</tr>
{{end}}
</table>
{{end}}
<table class="row footer" style="border-collapse: collapse; border-spacing: 0; display: block; margin-top: 20px; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">

View File

@ -149,7 +149,7 @@ color: #FFFFFF !important;
<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.AppUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Log in now</a></td>
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.AppUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border: 1px solid #ff8f2b; border-radius: 2px; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Log in now</a></td>
</tr>
</table>
</td>

View File

@ -147,7 +147,7 @@ color: #FFFFFF !important;
<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.LinkUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Accept Invitation</a></td>
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.LinkUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border: 1px solid #ff8f2b; border-radius: 2px; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Accept Invitation</a></td>
</tr>
</table>
</td>

View File

@ -148,7 +148,7 @@ color: #FFFFFF !important;
<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.SignUpUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Complete Sign Up</a></td>
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.SignUpUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border: 1px solid #ff8f2b; border-radius: 2px; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Complete Sign Up</a></td>
</tr>
</table>
</td>

View File

@ -71,6 +71,7 @@
@import "components/query_editor";
@import "components/tabbed_view";
@import "components/query_part";
@import "components/jsontree";
// PAGES
@import "pages/login";

View File

@ -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;
}
}

View File

@ -34,7 +34,7 @@
}
.label-tag:hover {
opacity: 0.85;
opacity: 0.85;
background-color: darken($purple, 10%);
}