mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'alert_conditions' into alerting
This commit is contained in:
commit
cde1bbff78
@ -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]]
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
22
pkg/models/annotations.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
package alertstates
|
||||
|
||||
var (
|
||||
ValidStates = []string{
|
||||
Ok,
|
||||
Warn,
|
||||
Critical,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
Ok = "OK"
|
||||
Warn = "WARN"
|
||||
Critical = "CRITICAL"
|
||||
Pending = "PENDING"
|
||||
Unknown = "UNKNOWN"
|
||||
)
|
191
pkg/services/alerting/conditions.go
Normal file
191
pkg/services/alerting/conditions.go
Normal 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
|
||||
}
|
92
pkg/services/alerting/conditions_test.go
Normal file
92
pkg/services/alerting/conditions_test.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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}
|
||||
// }
|
||||
|
@ -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)
|
||||
// })
|
||||
// })
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
57
pkg/services/alerting/test_rule.go
Normal file
57
pkg/services/alerting/test_rule.go
Normal 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
|
||||
}
|
@ -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")
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package transformers
|
||||
|
||||
import "github.com/grafana/grafana/pkg/tsdb"
|
||||
|
||||
type Transformer interface {
|
||||
Transform(timeserie *tsdb.TimeSeries) (float64, error)
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package sqlstore
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
@ -31,11 +31,11 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func AlertNotificationQuery(query *m.GetAlertNotificationQuery) error {
|
||||
func AlertNotificationQuery(query *m.GetAlertNotificationsQuery) error {
|
||||
return getAlertNotifications(query, x.NewSession())
|
||||
}
|
||||
|
||||
func getAlertNotifications(query *m.GetAlertNotificationQuery, sess *xorm.Session) error {
|
||||
func getAlertNotifications(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
@ -43,16 +43,15 @@ func getAlertNotifications(query *m.GetAlertNotificationQuery, sess *xorm.Sessio
|
||||
alert_notification.id,
|
||||
alert_notification.org_id,
|
||||
alert_notification.name,
|
||||
alert_notification.type,
|
||||
alert_notification.type,
|
||||
alert_notification.created,
|
||||
alert_notification.updated,
|
||||
alert_notification.settings,
|
||||
alert_notification.always_execute
|
||||
alert_notification.updated,
|
||||
alert_notification.settings
|
||||
FROM alert_notification
|
||||
`)
|
||||
|
||||
sql.WriteString(` WHERE alert_notification.org_id = ?`)
|
||||
params = append(params, query.OrgID)
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if query.Name != "" {
|
||||
sql.WriteString(` AND alert_notification.name = ?`)
|
||||
@ -61,60 +60,28 @@ func getAlertNotifications(query *m.GetAlertNotificationQuery, sess *xorm.Sessio
|
||||
|
||||
if query.Id != 0 {
|
||||
sql.WriteString(` AND alert_notification.id = ?`)
|
||||
params = append(params, strconv.Itoa(int(query.Id)))
|
||||
params = append(params, query.Id)
|
||||
}
|
||||
|
||||
if len(query.Ids) > 0 {
|
||||
sql.WriteString(` AND (`)
|
||||
|
||||
for i, id := range query.Ids {
|
||||
if i != 0 {
|
||||
sql.WriteString(` OR`)
|
||||
}
|
||||
sql.WriteString(` alert_notification.id = ?`)
|
||||
params = append(params, id)
|
||||
sql.WriteString(` AND alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
|
||||
for _, v := range query.Ids {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
sql.WriteString(`)`)
|
||||
}
|
||||
|
||||
var searches []*m.AlertNotification
|
||||
if err := sess.Sql(sql.String(), params...).Find(&searches); err != nil {
|
||||
results := make([]*m.AlertNotification, 0)
|
||||
if err := sess.Sql(sql.String(), params...).Find(&results); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result []*m.AlertNotification
|
||||
var def []*m.AlertNotification
|
||||
if query.IncludeAlwaysExecute {
|
||||
|
||||
if err := sess.Where("org_id = ? AND always_execute = 1", query.OrgID).Find(&def); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, def...)
|
||||
}
|
||||
|
||||
for _, s := range searches {
|
||||
canAppend := true
|
||||
for _, d := range result {
|
||||
if d.Id == s.Id {
|
||||
canAppend = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if canAppend {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
|
||||
query.Result = result
|
||||
query.Result = results
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
existingQuery := &m.GetAlertNotificationQuery{OrgID: cmd.OrgID, Name: cmd.Name, IncludeAlwaysExecute: false}
|
||||
existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
||||
err := getAlertNotifications(existingQuery, sess)
|
||||
|
||||
if err != nil {
|
||||
@ -126,18 +93,15 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
|
||||
}
|
||||
|
||||
alertNotification := &m.AlertNotification{
|
||||
OrgId: cmd.OrgID,
|
||||
Name: cmd.Name,
|
||||
Type: cmd.Type,
|
||||
Created: time.Now(),
|
||||
Settings: cmd.Settings,
|
||||
Updated: time.Now(),
|
||||
AlwaysExecute: cmd.AlwaysExecute,
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Type: cmd.Type,
|
||||
Settings: cmd.Settings,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
_, err = sess.Insert(alertNotification)
|
||||
|
||||
if err != nil {
|
||||
if _, err = sess.Insert(alertNotification); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -148,38 +112,34 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
|
||||
|
||||
func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
||||
return inTransaction(func(sess *xorm.Session) (err error) {
|
||||
current := &m.AlertNotification{}
|
||||
_, err = sess.Id(cmd.Id).Get(current)
|
||||
current := m.AlertNotification{}
|
||||
|
||||
if err != nil {
|
||||
if _, err = sess.Id(cmd.Id).Get(¤t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
alertNotification := &m.AlertNotification{
|
||||
Id: cmd.Id,
|
||||
OrgId: cmd.OrgID,
|
||||
Name: cmd.Name,
|
||||
Type: cmd.Type,
|
||||
Settings: cmd.Settings,
|
||||
Updated: time.Now(),
|
||||
Created: current.Created,
|
||||
AlwaysExecute: cmd.AlwaysExecute,
|
||||
}
|
||||
|
||||
sess.UseBool("always_execute")
|
||||
|
||||
var affected int64
|
||||
affected, err = sess.Id(alertNotification.Id).Update(alertNotification)
|
||||
|
||||
if err != nil {
|
||||
// check if name exists
|
||||
sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
||||
if err := getAlertNotifications(sameNameQuery, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if affected == 0 {
|
||||
if len(sameNameQuery.Result) > 0 && sameNameQuery.Result[0].Id != current.Id {
|
||||
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
|
||||
}
|
||||
|
||||
current.Updated = time.Now()
|
||||
current.Settings = cmd.Settings
|
||||
current.Name = cmd.Name
|
||||
current.Type = cmd.Type
|
||||
|
||||
if affected, err := sess.Id(cmd.Id).Update(current); err != nil {
|
||||
return err
|
||||
} else if affected == 0 {
|
||||
return fmt.Errorf("Could not find alert notification")
|
||||
}
|
||||
|
||||
cmd.Result = alertNotification
|
||||
cmd.Result = ¤t
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
// }
|
||||
|
@ -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)
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
@ -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},
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
200
public/app/core/components/jsontree/jsontree.ts
Normal file
200
public/app/core/components/jsontree/jsontree.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
@ -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';
|
||||
|
@ -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',
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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" >
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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 = {
|
||||
|
@ -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-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-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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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%">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -71,6 +71,7 @@
|
||||
@import "components/query_editor";
|
||||
@import "components/tabbed_view";
|
||||
@import "components/query_part";
|
||||
@import "components/jsontree";
|
||||
|
||||
// PAGES
|
||||
@import "pages/login";
|
||||
|
61
public/sass/components/_jsontree.scss
Normal file
61
public/sass/components/_jsontree.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@
|
||||
}
|
||||
|
||||
.label-tag:hover {
|
||||
opacity: 0.85;
|
||||
opacity: 0.85;
|
||||
background-color: darken($purple, 10%);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user