Merge branch 'alert_ui_take2' into alerting
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"url": "https://floobits.com/raintank/grafana"
|
||||
}
|
||||
|
||||
"url": "https://floobits.com/raintank/grafana"
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
* **InfluxDB**: Add spread function, closes [#5211](https://github.com/grafana/grafana/issues/5211)
|
||||
* **Scripts**: Use restart instead of start for deb package script, closes [#5282](https://github.com/grafana/grafana/pull/5282)
|
||||
* **Logging**: Moved to structured logging lib, and moved to component specific level filters via config file, closes [#4590](https://github.com/grafana/grafana/issues/4590)
|
||||
* **Search**: Add search limit query parameter, closes [#5292](https://github.com/grafana/grafana/pull/5292)
|
||||
|
||||
## Breaking changes
|
||||
* **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log ouput.
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"alert": {
|
||||
"name": "Majority servers down",
|
||||
"frequency": 60,
|
||||
"notify": ["group1", "group2"],
|
||||
"expressions": [
|
||||
{
|
||||
"left": [
|
||||
{
|
||||
"type": "query",
|
||||
"refId": "A",
|
||||
"timeRange": {"from": "5m", "to": "now-1m"},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "max"
|
||||
}
|
||||
],
|
||||
"operator": ">",
|
||||
"right": [
|
||||
{
|
||||
"type": "constant",
|
||||
"value": 100
|
||||
}
|
||||
],
|
||||
"level": 2,
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"alert": {
|
||||
"name": "Majority servers down take2",
|
||||
"frequency": 60,
|
||||
"notify": ["group1", "group2"],
|
||||
"expressions": [
|
||||
{
|
||||
"left": [
|
||||
{
|
||||
"type": "query",
|
||||
"refId": "A",
|
||||
"timeRange": {"from": "5m", "to": "now-1m"},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "max"
|
||||
}
|
||||
],
|
||||
"operator": ">",
|
||||
"right": [
|
||||
{
|
||||
"type": "query",
|
||||
"refId": "A",
|
||||
"timeRange": {"from": "now-1d-5m", "to": "now-1d"},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "max"
|
||||
}
|
||||
],
|
||||
"level": 2,
|
||||
}
|
||||
]
|
||||
},
|
||||
"alert": {
|
||||
"name": "CPU usage last 5min above 90%",
|
||||
"frequency": 60,
|
||||
"expressions": [
|
||||
{
|
||||
"expr": "query(#A, 5m, now, avg)",
|
||||
"operator": ">",
|
||||
"critLevel": 90,
|
||||
}
|
||||
]
|
||||
},
|
||||
"alert": {
|
||||
"name": "Series count above 10",
|
||||
"frequency": "1m",
|
||||
"expressions": [
|
||||
{
|
||||
"expr": "query(#A, 5m, now, avg) | countSeries()",
|
||||
"operator": ">",
|
||||
"critLevel": 10,
|
||||
}
|
||||
]
|
||||
},
|
||||
"alert": {
|
||||
"name": "Disk Free Zero in 3 days",
|
||||
"frequency": "1d",
|
||||
"expressions": [
|
||||
{
|
||||
"expr": "query(#A, 1d, now, trend(3d))",
|
||||
"operator": ">",
|
||||
"critLevel": 0,
|
||||
}
|
||||
]
|
||||
},
|
||||
"alert": {
|
||||
"name": "Server requests is zero for more than 10min",
|
||||
"frequency": "1d",
|
||||
"expressions": [
|
||||
{
|
||||
"expr": "query(#A, 10m, now, sum)",
|
||||
"operator": "=",
|
||||
"critLevel": 0,
|
||||
}
|
||||
]
|
||||
},
|
||||
"alert": {
|
||||
"name": "Timeouts should not be more than 0.1% of requests",
|
||||
"frequency": "1d",
|
||||
"expressions": [
|
||||
{
|
||||
"expr": "query(#A, 10m, now, sum) | subtract | query(#B, 10m, now, sum)",
|
||||
"operator": ">",
|
||||
"critLevel": 0,
|
||||
}
|
||||
]
|
||||
},
|
||||
"alert": {
|
||||
"name": "CPU usage last 5min changed by more than 20% compared to last 24hours",
|
||||
"frequency": "1m",
|
||||
"value": "query(#A, 5m, now, avg)",
|
||||
"operator": "percent change",
|
||||
"threshold": "query(#A, 1d, now, avg)",
|
||||
},
|
||||
|
||||
"alert": {
|
||||
"name": "CPU higher than 90%",
|
||||
"frequency": "1m",
|
||||
"valueExpr": "query(#A, 5m, now, avg)",
|
||||
"evalType": "greater than",
|
||||
"critLevel": 20,
|
||||
"warnLevel": 10,
|
||||
},
|
||||
|
||||
"alert": {
|
||||
"name": "CPU usage last 5min changed by more than 20% compared to last 24hours",
|
||||
"frequency": "1m",
|
||||
"expr": "query(#A, 5m, now, avg) percentGreaterThan()",
|
||||
"evalType": "percentscre change",
|
||||
"evalExpr": "query(#A, 1d, now, avg)",
|
||||
"critLevel": 20,
|
||||
"warnLevel": 10,
|
||||
},
|
||||
"alert": {
|
||||
"name": "CPU usage last 5min changed by more than 20% compared to last 24hours",
|
||||
"frequency": "1m",
|
||||
"valueQuery": "query(#A, 5m, now, avg) ",
|
||||
"evalType": "simple", "// other options are: percent change, trend"
|
||||
"evalQuery": "query(#A, 1d, now, avg)",
|
||||
"comparison": "greater than",
|
||||
"critLevel": 20,
|
||||
"warnLevel": 10,
|
||||
},
|
||||
"alert": {
|
||||
"name": "CPU usage last 5min changed by more than 20% compared to last 24hours",
|
||||
"frequency": "1m",
|
||||
"valueQuery": "query(#A, 5m, now, avg) | Evaluate Against: Static Threshold | >200 Warn | >300 Critical",
|
||||
"valueQuery": "query(#A, 5m, now, avg) | Evaluate Against: Percent Change Compared To | query(#B, 5m, now, avg) | >200 Warn | >300 Critical",
|
||||
"valueQuery": "query(#A, 5m, now, trend) | Evaluate Against: Forcast | 7days | >200 Warn | >300 Critical",
|
||||
"evalType": "simple", "// other options are: percent change, trend"
|
||||
"evalQuery": "query(#A, 1d, now, avg)",
|
||||
"comparison": "greater than",
|
||||
"critLevel": 20,
|
||||
"warnLevel": 10,
|
||||
},
|
||||
}
|
||||
+1
-8
@@ -43,7 +43,7 @@ func GetAlertChanges(c *middleware.Context) Response {
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/alerts
|
||||
// GET /api/alerts/rules/
|
||||
func GetAlerts(c *middleware.Context) Response {
|
||||
query := models.GetAlertsQuery{
|
||||
OrgId: c.OrgId,
|
||||
@@ -64,15 +64,8 @@ func GetAlerts(c *middleware.Context) Response {
|
||||
Id: alert.Id,
|
||||
DashboardId: alert.DashboardId,
|
||||
PanelId: alert.PanelId,
|
||||
Query: alert.Query,
|
||||
QueryRefId: alert.QueryRefId,
|
||||
WarnLevel: alert.WarnLevel,
|
||||
CritLevel: alert.CritLevel,
|
||||
Frequency: alert.Frequency,
|
||||
Name: alert.Name,
|
||||
Description: alert.Description,
|
||||
QueryRange: alert.QueryRange,
|
||||
Aggregator: alert.Aggregator,
|
||||
State: alert.State,
|
||||
})
|
||||
}
|
||||
|
||||
+33
-23
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -21,13 +20,15 @@ var (
|
||||
)
|
||||
|
||||
type Response interface {
|
||||
WriteTo(out http.ResponseWriter)
|
||||
WriteTo(ctx *middleware.Context)
|
||||
}
|
||||
|
||||
type NormalResponse struct {
|
||||
status int
|
||||
body []byte
|
||||
header http.Header
|
||||
status int
|
||||
body []byte
|
||||
header http.Header
|
||||
errMessage string
|
||||
err error
|
||||
}
|
||||
|
||||
func wrap(action interface{}) macaron.Handler {
|
||||
@@ -41,17 +42,21 @@ func wrap(action interface{}) macaron.Handler {
|
||||
res = ServerError(err)
|
||||
}
|
||||
|
||||
res.WriteTo(c.Resp)
|
||||
res.WriteTo(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NormalResponse) WriteTo(out http.ResponseWriter) {
|
||||
header := out.Header()
|
||||
func (r *NormalResponse) WriteTo(ctx *middleware.Context) {
|
||||
if r.err != nil {
|
||||
ctx.Logger.Error(r.errMessage, "error", r.err)
|
||||
}
|
||||
|
||||
header := ctx.Resp.Header()
|
||||
for k, v := range r.header {
|
||||
header[k] = v
|
||||
}
|
||||
out.WriteHeader(r.status)
|
||||
out.Write(r.body)
|
||||
ctx.Resp.WriteHeader(r.status)
|
||||
ctx.Resp.Write(r.body)
|
||||
}
|
||||
|
||||
func (r *NormalResponse) Cache(ttl string) *NormalResponse {
|
||||
@@ -64,7 +69,6 @@ func (r *NormalResponse) Header(key, value string) *NormalResponse {
|
||||
}
|
||||
|
||||
// functions to create responses
|
||||
|
||||
func Empty(status int) *NormalResponse {
|
||||
return Respond(status, nil)
|
||||
}
|
||||
@@ -80,29 +84,35 @@ func ApiSuccess(message string) *NormalResponse {
|
||||
}
|
||||
|
||||
func ApiError(status int, message string, err error) *NormalResponse {
|
||||
resp := make(map[string]interface{})
|
||||
|
||||
if err != nil {
|
||||
log.Error(4, "%s: %v", message, err)
|
||||
if setting.Env != setting.PROD {
|
||||
resp["error"] = err.Error()
|
||||
}
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
|
||||
switch status {
|
||||
case 404:
|
||||
metrics.M_Api_Status_404.Inc(1)
|
||||
resp["message"] = "Not Found"
|
||||
data["message"] = "Not Found"
|
||||
case 500:
|
||||
metrics.M_Api_Status_500.Inc(1)
|
||||
resp["message"] = "Internal Server Error"
|
||||
data["message"] = "Internal Server Error"
|
||||
}
|
||||
|
||||
if message != "" {
|
||||
resp["message"] = message
|
||||
data["message"] = message
|
||||
}
|
||||
|
||||
return Json(status, resp)
|
||||
if err != nil {
|
||||
if setting.Env != setting.PROD {
|
||||
data["error"] = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
resp := Json(status, data)
|
||||
|
||||
if err != nil {
|
||||
resp.errMessage = message
|
||||
resp.err = err
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func Respond(status int, body interface{}) *NormalResponse {
|
||||
|
||||
@@ -151,15 +151,13 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
|
||||
}
|
||||
|
||||
if setting.AlertingEnabled {
|
||||
saveAlertCommand := m.SaveAlertsCommand{
|
||||
DashboardId: cmd.Result.Id,
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Alerts: alerting.ParseAlertsFromDashboard(&cmd),
|
||||
alertCmd := alerting.UpdateDashboardAlertsCommand{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Dashboard: cmd.Result,
|
||||
}
|
||||
|
||||
err = bus.Dispatch(&saveAlertCommand)
|
||||
if err != nil {
|
||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||
c.JsonApiErr(500, "Failed to save alerts", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/go-stack/stack"
|
||||
"github.com/inconshreveable/log15"
|
||||
"github.com/inconshreveable/log15/term"
|
||||
)
|
||||
@@ -22,6 +23,7 @@ var loggersToClose []DisposableHandler
|
||||
func init() {
|
||||
loggersToClose = make([]DisposableHandler, 0)
|
||||
Root = log15.Root()
|
||||
Root.SetHandler(log15.DiscardHandler())
|
||||
}
|
||||
|
||||
func New(logger string, ctx ...interface{}) Logger {
|
||||
@@ -227,3 +229,9 @@ func LogFilterHandler(maxLevel log15.Lvl, filters map[string]log15.Lvl, h log15.
|
||||
return r.Lvl <= maxLevel
|
||||
}, h)
|
||||
}
|
||||
|
||||
func Stack(skip int) string {
|
||||
call := stack.Caller(skip)
|
||||
s := stack.Trace().TrimBelow(call).TrimRuntime()
|
||||
return s.String()
|
||||
}
|
||||
|
||||
@@ -48,7 +48,11 @@ func Logger() macaron.Handler {
|
||||
|
||||
if ctx, ok := c.Data["ctx"]; ok {
|
||||
ctxTyped := ctx.(*Context)
|
||||
ctxTyped.Logger.Info("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ns", timeTakenMs, "size", rw.Size())
|
||||
if status == 500 {
|
||||
ctxTyped.Logger.Error("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ns", timeTakenMs, "size", rw.Size())
|
||||
} else {
|
||||
ctxTyped.Logger.Info("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ns", timeTakenMs, "size", rw.Size())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func initContextWithAnonymousUser(ctx *Context) bool {
|
||||
func initContextWithUserSessionCookie(ctx *Context) bool {
|
||||
// initialize session
|
||||
if err := ctx.Session.Start(ctx); err != nil {
|
||||
log.Error(3, "Failed to start session", err)
|
||||
ctx.Logger.Error("Failed to start session", "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func initContextWithUserSessionCookie(ctx *Context) bool {
|
||||
|
||||
query := m.GetSignedInUserQuery{UserId: userId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
log.Error(3, "Failed to get user with id %v", userId)
|
||||
ctx.Logger.Error("Failed to get user with id", "userId", userId)
|
||||
return false
|
||||
} else {
|
||||
ctx.SignedInUser = query.Result
|
||||
@@ -185,7 +185,7 @@ func initContextWithApiKeyFromSession(ctx *Context) bool {
|
||||
|
||||
keyQuery := m.GetApiKeyByIdQuery{ApiKeyId: keyId.(int64)}
|
||||
if err := bus.Dispatch(&keyQuery); err != nil {
|
||||
log.Error(3, "Failed to get api key by id", err)
|
||||
ctx.Logger.Error("Failed to get api key by id", "id", keyId, "error", err)
|
||||
return false
|
||||
} else {
|
||||
apikey := keyQuery.Result
|
||||
@@ -202,7 +202,7 @@ func initContextWithApiKeyFromSession(ctx *Context) bool {
|
||||
// Handle handles and logs error by given status.
|
||||
func (ctx *Context) Handle(status int, title string, err error) {
|
||||
if err != nil {
|
||||
log.Error(4, "%s: %v", title, err)
|
||||
ctx.Logger.Error(title, "error", err)
|
||||
if setting.Env != setting.PROD {
|
||||
ctx.Data["ErrorMsg"] = err
|
||||
}
|
||||
@@ -223,9 +223,7 @@ func (ctx *Context) Handle(status int, title string, err error) {
|
||||
|
||||
func (ctx *Context) JsonOK(message string) {
|
||||
resp := make(map[string]interface{})
|
||||
|
||||
resp["message"] = message
|
||||
|
||||
ctx.JSON(200, resp)
|
||||
}
|
||||
|
||||
@@ -237,7 +235,7 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
|
||||
resp := make(map[string]interface{})
|
||||
|
||||
if err != nil {
|
||||
log.Error(4, "%s: %v", message, err)
|
||||
ctx.Logger.Error(message, "error", err)
|
||||
if setting.Env != setting.PROD {
|
||||
resp["error"] = err.Error()
|
||||
}
|
||||
|
||||
@@ -191,9 +191,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
var createUserCmd *m.CreateUserCommand
|
||||
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
|
||||
createUserCmd = cmd
|
||||
cmd.Result = m.User{Id: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"gopkg.in/macaron.v1"
|
||||
@@ -35,10 +34,8 @@ func QuotaReached(c *Context, target string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("checking quota for %s in scopes %v", target, scopes))
|
||||
|
||||
for _, scope := range scopes {
|
||||
log.Debug(fmt.Sprintf("checking scope %s", scope.Name))
|
||||
c.Logger.Debug("Checking quota", "target", target, "scope", scope)
|
||||
|
||||
switch scope.Name {
|
||||
case "global":
|
||||
@@ -51,7 +48,7 @@ func QuotaReached(c *Context, target string) (bool, error) {
|
||||
if target == "session" {
|
||||
usedSessions := getSessionCount()
|
||||
if int64(usedSessions) > scope.DefaultLimit {
|
||||
log.Debug(fmt.Sprintf("%d sessions active, limit is %d", usedSessions, scope.DefaultLimit))
|
||||
c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
|
||||
return true, nil
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
DashboardId int64
|
||||
PanelId int64
|
||||
Name string
|
||||
Description string
|
||||
State string
|
||||
Scheduler int64
|
||||
Enabled bool
|
||||
Frequency int
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
|
||||
Expression *simplejson.Json
|
||||
}
|
||||
|
||||
func (alert *Alert) ValidToSave() bool {
|
||||
return alert.DashboardId != 0
|
||||
}
|
||||
|
||||
func (this *Alert) ContainsUpdates(other *Alert) bool {
|
||||
result := false
|
||||
result = result || this.Name != other.Name
|
||||
result = result || this.Description != other.Description
|
||||
|
||||
if this.Expression != nil && other.Expression != nil {
|
||||
json1, err1 := this.Expression.Encode()
|
||||
json2, err2 := other.Expression.Encode()
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
result = result || string(json1) != string(json2)
|
||||
}
|
||||
|
||||
//don't compare .State! That would be insane.
|
||||
return result
|
||||
}
|
||||
|
||||
type AlertingClusterInfo struct {
|
||||
ServerId string
|
||||
ClusterSize int
|
||||
UptimePosition int
|
||||
}
|
||||
|
||||
type HeartBeat struct {
|
||||
Id int64
|
||||
ServerId string
|
||||
Updated time.Time
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type HeartBeatCommand struct {
|
||||
ServerId string
|
||||
Result AlertingClusterInfo
|
||||
}
|
||||
|
||||
type AlertChange struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"-"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
Type string `json:"type"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
// Commands
|
||||
type SaveAlertsCommand struct {
|
||||
DashboardId int64
|
||||
UserId int64
|
||||
OrgId int64
|
||||
|
||||
Alerts []*Alert
|
||||
}
|
||||
|
||||
type DeleteAlertCommand struct {
|
||||
AlertId int64
|
||||
}
|
||||
|
||||
//Queries
|
||||
type GetAlertsQuery struct {
|
||||
OrgId int64
|
||||
State []string
|
||||
DashboardId int64
|
||||
PanelId int64
|
||||
|
||||
Result []*Alert
|
||||
}
|
||||
|
||||
type GetAllAlertsQuery struct {
|
||||
Result []*Alert
|
||||
}
|
||||
|
||||
type GetAlertByIdQuery struct {
|
||||
Id int64
|
||||
|
||||
Result *Alert
|
||||
}
|
||||
|
||||
type GetAlertChangesQuery struct {
|
||||
OrgId int64
|
||||
Limit int64
|
||||
SinceId int64
|
||||
|
||||
Result []*AlertChange
|
||||
}
|
||||
@@ -31,7 +31,7 @@ type UpdateAlertStateCommand struct {
|
||||
NewState string `json:"newState" binding:"Required"`
|
||||
Info string `json:"info"`
|
||||
|
||||
Result *AlertRule
|
||||
Result *Alert
|
||||
}
|
||||
|
||||
// Queries
|
||||
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAlertingModelTest(t *testing.T) {
|
||||
Convey("Testing Alerting model", t, func() {
|
||||
|
||||
json1, _ := simplejson.NewJson([]byte(`{ "field": "value" }`))
|
||||
json2, _ := simplejson.NewJson([]byte(`{ "field": "value" }`))
|
||||
|
||||
rule1 := &Alert{
|
||||
Expression: json1,
|
||||
Name: "Namn",
|
||||
Description: "Description",
|
||||
}
|
||||
|
||||
rule2 := &Alert{
|
||||
Expression: json2,
|
||||
Name: "Namn",
|
||||
Description: "Description",
|
||||
}
|
||||
|
||||
Convey("Testing AlertRule equals", func() {
|
||||
|
||||
So(rule1.ContainsUpdates(rule2), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Changing the expression should contain update", func() {
|
||||
json2, _ := simplejson.NewJson([]byte(`{ "field": "newValue" }`))
|
||||
rule1.Expression = json2
|
||||
So(rule1.ContainsUpdates(rule2), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type AlertRule struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"-"`
|
||||
DatasourceId int64 `json:"datasourceId"`
|
||||
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"`
|
||||
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
func (alertRule *AlertRule) ValidToSave() bool {
|
||||
return alertRule.Query != "" && alertRule.Frequency != 0 && alertRule.QueryRange != 0 && alertRule.Name != ""
|
||||
}
|
||||
|
||||
func (this *AlertRule) Equals(other *AlertRule) bool {
|
||||
result := false
|
||||
|
||||
result = result || this.Aggregator != other.Aggregator
|
||||
result = result || this.CritLevel != other.CritLevel
|
||||
result = result || this.WarnLevel != other.WarnLevel
|
||||
result = result || this.WarnOperator != other.WarnOperator
|
||||
result = result || this.CritOperator != other.CritOperator
|
||||
result = result || this.Query != other.Query
|
||||
result = result || this.QueryRefId != other.QueryRefId
|
||||
result = result || this.Frequency != other.Frequency
|
||||
result = result || this.Name != other.Name
|
||||
result = result || this.Description != other.Description
|
||||
result = result || this.QueryRange != other.QueryRange
|
||||
//don't compare .State! That would be insane.
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type AlertingClusterInfo struct {
|
||||
ServerId string
|
||||
ClusterSize int
|
||||
UptimePosition int
|
||||
}
|
||||
|
||||
type HeartBeat struct {
|
||||
Id int64
|
||||
ServerId string
|
||||
Updated time.Time
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type HeartBeatCommand struct {
|
||||
ServerId string
|
||||
|
||||
Result AlertingClusterInfo
|
||||
}
|
||||
|
||||
type AlertRuleChange struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"-"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
Type string `json:"type"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
// Commands
|
||||
type SaveAlertsCommand struct {
|
||||
DashboardId int64
|
||||
UserId int64
|
||||
OrgId int64
|
||||
|
||||
Alerts []*AlertRule
|
||||
}
|
||||
|
||||
type DeleteAlertCommand struct {
|
||||
AlertId int64
|
||||
}
|
||||
|
||||
//Queries
|
||||
type GetAlertsQuery struct {
|
||||
OrgId int64
|
||||
State []string
|
||||
DashboardId int64
|
||||
PanelId int64
|
||||
|
||||
Result []*AlertRule
|
||||
}
|
||||
|
||||
type GetAllAlertsQuery struct {
|
||||
Result []*AlertRule
|
||||
}
|
||||
|
||||
type GetAlertsForExecutionQuery struct {
|
||||
Timestamp int64
|
||||
|
||||
Result []*AlertRule
|
||||
}
|
||||
|
||||
type GetAlertByIdQuery struct {
|
||||
Id int64
|
||||
|
||||
Result *AlertRule
|
||||
}
|
||||
|
||||
type GetAlertChangesQuery struct {
|
||||
OrgId int64
|
||||
Limit int64
|
||||
SinceId int64
|
||||
|
||||
Result []*AlertRuleChange
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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 transformer.Transformer
|
||||
}
|
||||
|
||||
func getTimeDurationStringToSeconds(str string) int64 {
|
||||
return 60
|
||||
}
|
||||
|
||||
func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) {
|
||||
model := &AlertRule{}
|
||||
model.Id = ruleDef.Id
|
||||
model.OrgId = ruleDef.OrgId
|
||||
model.Name = ruleDef.Name
|
||||
model.Description = ruleDef.Description
|
||||
model.State = ruleDef.State
|
||||
|
||||
critical := ruleDef.Expression.Get("critical")
|
||||
model.Critical = Level{
|
||||
Operator: critical.Get("op").MustString(),
|
||||
Level: critical.Get("level").MustFloat64(),
|
||||
}
|
||||
|
||||
warning := ruleDef.Expression.Get("warn")
|
||||
model.Warning = Level{
|
||||
Operator: warning.Get("op").MustString(),
|
||||
Level: warning.Get("level").MustFloat64(),
|
||||
}
|
||||
|
||||
model.Frequency = getTimeDurationStringToSeconds(ruleDef.Expression.Get("frequency").MustString())
|
||||
model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
|
||||
model.TransformParams = *ruleDef.Expression.Get("transform")
|
||||
|
||||
if model.Transform == "aggregation" {
|
||||
method := ruleDef.Expression.Get("transform").Get("method").MustString()
|
||||
model.Transformer = transformer.NewAggregationTransformer(method)
|
||||
}
|
||||
|
||||
query := ruleDef.Expression.Get("query")
|
||||
model.Query = AlertQuery{
|
||||
Query: query.Get("query").MustString(),
|
||||
DatasourceId: query.Get("datasourceId").MustInt64(),
|
||||
From: query.Get("from").MustString(),
|
||||
To: query.Get("to").MustString(),
|
||||
Aggregator: query.Get("agg").MustString(),
|
||||
}
|
||||
|
||||
if model.Query.Query == "" {
|
||||
return nil, fmt.Errorf("missing query.query")
|
||||
}
|
||||
|
||||
if model.Query.DatasourceId == 0 {
|
||||
return nil, fmt.Errorf("missing query.datasourceId")
|
||||
}
|
||||
|
||||
return model, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type UpdateDashboardAlertsCommand struct {
|
||||
UserId int64
|
||||
OrgId int64
|
||||
Dashboard *m.Dashboard
|
||||
}
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("alerting", updateDashboardAlerts)
|
||||
}
|
||||
|
||||
func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
|
||||
saveAlerts := m.SaveAlertsCommand{
|
||||
OrgId: cmd.OrgId,
|
||||
UserId: cmd.UserId,
|
||||
DashboardId: cmd.Dashboard.Id,
|
||||
}
|
||||
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
|
||||
if alerts, err := extractor.GetAlerts(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
saveAlerts.Alerts = alerts
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&saveAlerts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRule {
|
||||
alerts := make([]*m.AlertRule, 0)
|
||||
|
||||
for _, rowObj := range cmd.Dashboard.Get("rows").MustArray() {
|
||||
row := simplejson.NewFromAny(rowObj)
|
||||
|
||||
for _, panelObj := range row.Get("panels").MustArray() {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
|
||||
alerting := panel.Get("alerting")
|
||||
alert := &m.AlertRule{
|
||||
DashboardId: cmd.Result.Id,
|
||||
OrgId: cmd.Result.OrgId,
|
||||
PanelId: panel.Get("id").MustInt64(),
|
||||
Id: alerting.Get("id").MustInt64(),
|
||||
QueryRefId: alerting.Get("queryRef").MustString(),
|
||||
WarnLevel: alerting.Get("warnLevel").MustFloat64(),
|
||||
CritLevel: alerting.Get("critLevel").MustFloat64(),
|
||||
WarnOperator: alerting.Get("warnOperator").MustString(),
|
||||
CritOperator: alerting.Get("critOperator").MustString(),
|
||||
Frequency: alerting.Get("frequency").MustInt64(),
|
||||
Name: alerting.Get("name").MustString(),
|
||||
Description: alerting.Get("description").MustString(),
|
||||
QueryRange: alerting.Get("queryRange").MustInt(),
|
||||
Aggregator: alerting.Get("aggregator").MustString(),
|
||||
}
|
||||
|
||||
log.Info("Alertrule: %v", alert.Name)
|
||||
for _, targetsObj := range panel.Get("targets").MustArray() {
|
||||
target := simplejson.NewFromAny(targetsObj)
|
||||
|
||||
if target.Get("refId").MustString() == alert.QueryRefId {
|
||||
targetJson, err := target.MarshalJSON()
|
||||
if err == nil {
|
||||
alert.Query = string(targetJson)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if panel.Get("datasource").MustString() == "" {
|
||||
query := &m.GetDataSourcesQuery{OrgId: cmd.OrgId}
|
||||
if err := bus.Dispatch(query); err == nil {
|
||||
for _, ds := range query.Result {
|
||||
if ds.IsDefault {
|
||||
alert.DatasourceId = ds.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := &m.GetDataSourceByNameQuery{
|
||||
Name: panel.Get("datasource").MustString(),
|
||||
OrgId: cmd.OrgId,
|
||||
}
|
||||
bus.Dispatch(query)
|
||||
alert.DatasourceId = query.Result.Id
|
||||
}
|
||||
|
||||
if alert.ValidToSave() {
|
||||
alerts = append(alerts, alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alerts
|
||||
}
|
||||
@@ -50,6 +50,12 @@ func (e *Engine) Stop() {
|
||||
}
|
||||
|
||||
func (e *Engine) alertingTicker() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Scheduler Panic, stopping...", "error", err, "stack", log.Stack(1))
|
||||
}
|
||||
}()
|
||||
|
||||
tickIndex := 0
|
||||
|
||||
for {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package alerting
|
||||
|
||||
type compareFn func(float64, float64) bool
|
||||
|
||||
func evalCondition(level Level, result float64) bool {
|
||||
return operators[level.Operator](result, level.Level)
|
||||
}
|
||||
|
||||
var operators = map[string]compareFn{
|
||||
">": func(num1, num2 float64) bool { return num1 > num2 },
|
||||
">=": func(num1, num2 float64) bool { return num1 >= num2 },
|
||||
"<": func(num1, num2 float64) bool { return num1 < num2 },
|
||||
"<=": func(num1, num2 float64) bool { return num1 <= num2 },
|
||||
"": func(num1, num2 float64) bool { return false },
|
||||
}
|
||||
@@ -2,9 +2,6 @@ package alerting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"math"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
@@ -14,7 +11,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
resultLogFmt = "Alerting: executor %s %1.2f %s %1.2f : %v"
|
||||
descriptionFmt = "Actual value: %1.2f for %s"
|
||||
)
|
||||
|
||||
@@ -28,63 +24,6 @@ func NewExecutor() *ExecutorImpl {
|
||||
}
|
||||
}
|
||||
|
||||
type compareFn func(float64, float64) bool
|
||||
type aggregationFn func(*tsdb.TimeSeries) float64
|
||||
|
||||
var operators = map[string]compareFn{
|
||||
">": func(num1, num2 float64) bool { return num1 > num2 },
|
||||
">=": func(num1, num2 float64) bool { return num1 >= num2 },
|
||||
"<": func(num1, num2 float64) bool { return num1 < num2 },
|
||||
"<=": func(num1, num2 float64) bool { return num1 <= num2 },
|
||||
"": func(num1, num2 float64) bool { return false },
|
||||
}
|
||||
var aggregator = map[string]aggregationFn{
|
||||
"avg": func(series *tsdb.TimeSeries) float64 {
|
||||
sum := float64(0)
|
||||
|
||||
for _, v := range series.Points {
|
||||
sum += v[0]
|
||||
}
|
||||
|
||||
return sum / float64(len(series.Points))
|
||||
},
|
||||
"sum": func(series *tsdb.TimeSeries) float64 {
|
||||
sum := float64(0)
|
||||
|
||||
for _, v := range series.Points {
|
||||
sum += v[0]
|
||||
}
|
||||
|
||||
return sum
|
||||
},
|
||||
"min": func(series *tsdb.TimeSeries) float64 {
|
||||
min := series.Points[0][0]
|
||||
|
||||
for _, v := range series.Points {
|
||||
if v[0] < min {
|
||||
min = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return min
|
||||
},
|
||||
"max": func(series *tsdb.TimeSeries) float64 {
|
||||
max := series.Points[0][0]
|
||||
|
||||
for _, v := range series.Points {
|
||||
if v[0] > max {
|
||||
max = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return max
|
||||
},
|
||||
"mean": func(series *tsdb.TimeSeries) float64 {
|
||||
midPosition := int64(math.Floor(float64(len(series.Points)) / float64(2)))
|
||||
return series.Points[midPosition][0]
|
||||
},
|
||||
}
|
||||
|
||||
func (e *ExecutorImpl) Execute(job *AlertJob, resultQueue chan *AlertResult) {
|
||||
timeSeries, err := e.executeQuery(job)
|
||||
if err != nil {
|
||||
@@ -102,12 +41,12 @@ func (e *ExecutorImpl) Execute(job *AlertJob, resultQueue chan *AlertResult) {
|
||||
|
||||
func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) {
|
||||
getDsInfo := &m.GetDataSourceByIdQuery{
|
||||
Id: job.Rule.DatasourceId,
|
||||
Id: job.Rule.Query.DatasourceId,
|
||||
OrgId: job.Rule.OrgId,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(getDsInfo); err != nil {
|
||||
return nil, fmt.Errorf("Could not find datasource for %d", job.Rule.DatasourceId)
|
||||
return nil, fmt.Errorf("Could not find datasource")
|
||||
}
|
||||
|
||||
req := e.GetRequestForAlertRule(job.Rule, getDsInfo.Result)
|
||||
@@ -130,16 +69,16 @@ func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error)
|
||||
}
|
||||
|
||||
func (e *ExecutorImpl) 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: "-" + strconv.Itoa(rule.QueryRange) + "s",
|
||||
To: "now",
|
||||
From: "-" + rule.Query.From,
|
||||
To: rule.Query.To,
|
||||
},
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
RefId: rule.QueryRefId,
|
||||
Query: rule.Query,
|
||||
RefId: "A",
|
||||
Query: rule.Query.Query,
|
||||
DataSource: &tsdb.DataSourceInfo{
|
||||
Id: datasource.Id,
|
||||
Name: datasource.Name,
|
||||
@@ -157,33 +96,26 @@ func (e *ExecutorImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice
|
||||
e.log.Debug("Evaluating Alerting Rule", "seriesCount", len(series), "ruleName", rule.Name)
|
||||
|
||||
for _, serie := range series {
|
||||
log.Debug("Evaluating series", "series", serie.Name)
|
||||
e.log.Debug("Evaluating series", "series", serie.Name)
|
||||
transformedValue, _ := rule.Transformer.Transform(serie)
|
||||
|
||||
if aggregator[rule.Aggregator] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var aggValue = aggregator[rule.Aggregator](serie)
|
||||
var critOperartor = operators[rule.CritOperator]
|
||||
var critResult = critOperartor(aggValue, rule.CritLevel)
|
||||
|
||||
log.Trace(resultLogFmt, "Crit", serie.Name, aggValue, rule.CritOperator, rule.CritLevel, critResult)
|
||||
critResult := evalCondition(rule.Critical, transformedValue)
|
||||
e.log.Debug("Alert execution Crit", "name", serie.Name, "transformedValue", transformedValue, "operator", rule.Critical.Operator, "level", rule.Critical.Level, "result", critResult)
|
||||
if critResult {
|
||||
return &AlertResult{
|
||||
State: alertstates.Critical,
|
||||
ActualValue: aggValue,
|
||||
Description: fmt.Sprintf(descriptionFmt, aggValue, serie.Name),
|
||||
ActualValue: transformedValue,
|
||||
Description: fmt.Sprintf(descriptionFmt, transformedValue, serie.Name),
|
||||
}
|
||||
}
|
||||
|
||||
var warnOperartor = operators[rule.CritOperator]
|
||||
var warnResult = warnOperartor(aggValue, rule.CritLevel)
|
||||
log.Trace(resultLogFmt, "Warn", serie.Name, aggValue, rule.WarnOperator, rule.WarnLevel, warnResult)
|
||||
warnResult := evalCondition(rule.Warning, transformedValue)
|
||||
e.log.Debug("Alert execution Warn", "name", serie.Name, "transformedValue", transformedValue, "operator", rule.Warning.Operator, "level", rule.Warning.Level, "result", warnResult)
|
||||
if warnResult {
|
||||
return &AlertResult{
|
||||
State: alertstates.Warn,
|
||||
Description: fmt.Sprintf(descriptionFmt, aggValue, serie.Name),
|
||||
ActualValue: aggValue,
|
||||
Description: fmt.Sprintf(descriptionFmt, transformedValue, serie.Name),
|
||||
ActualValue: transformedValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
|
||||
Convey("single time serie", func() {
|
||||
Convey("Show return ok since avg is above 2", func() {
|
||||
rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "sum"}
|
||||
rule := &AlertRule{
|
||||
Critical: Level{Level: 10, Operator: ">"},
|
||||
Transformer: &AggregationTransformer{Method: "avg"},
|
||||
}
|
||||
|
||||
timeSeries := []*tsdb.TimeSeries{
|
||||
tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}),
|
||||
@@ -25,7 +28,10 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Show return critical since below 2", func() {
|
||||
rule := &AlertRule{CritLevel: 10, CritOperator: "<", Aggregator: "sum"}
|
||||
rule := &AlertRule{
|
||||
Critical: Level{Level: 10, Operator: "<"},
|
||||
Transformer: &AggregationTransformer{Method: "avg"},
|
||||
}
|
||||
|
||||
timeSeries := []*tsdb.TimeSeries{
|
||||
tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}),
|
||||
@@ -36,7 +42,10 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Show return critical since sum is above 10", func() {
|
||||
rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "sum"}
|
||||
rule := &AlertRule{
|
||||
Critical: Level{Level: 10, Operator: ">"},
|
||||
Transformer: &AggregationTransformer{Method: "sum"},
|
||||
}
|
||||
|
||||
timeSeries := []*tsdb.TimeSeries{
|
||||
tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}),
|
||||
@@ -47,7 +56,10 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Show return ok since avg is below 10", func() {
|
||||
rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "avg"}
|
||||
rule := &AlertRule{
|
||||
Critical: Level{Level: 10, Operator: ">"},
|
||||
Transformer: &AggregationTransformer{Method: "avg"},
|
||||
}
|
||||
|
||||
timeSeries := []*tsdb.TimeSeries{
|
||||
tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}),
|
||||
@@ -58,7 +70,10 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Show return ok since min is below 10", func() {
|
||||
rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "min"}
|
||||
rule := &AlertRule{
|
||||
Critical: Level{Level: 10, Operator: ">"},
|
||||
Transformer: &AggregationTransformer{Method: "avg"},
|
||||
}
|
||||
|
||||
timeSeries := []*tsdb.TimeSeries{
|
||||
tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}, {9, 0}}),
|
||||
@@ -69,7 +84,10 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Show return ok since max is above 10", func() {
|
||||
rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "max"}
|
||||
rule := &AlertRule{
|
||||
Critical: Level{Level: 10, Operator: ">"},
|
||||
Transformer: &AggregationTransformer{Method: "max"},
|
||||
}
|
||||
|
||||
timeSeries := []*tsdb.TimeSeries{
|
||||
tsdb.NewTimeSeries("test1", [][2]float64{{1, 0}, {11, 0}}),
|
||||
@@ -78,11 +96,15 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
result := executor.evaluateRule(rule, timeSeries)
|
||||
So(result.State, ShouldEqual, alertstates.Critical)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Convey("muliple time series", func() {
|
||||
Convey("both are ok", func() {
|
||||
rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "sum"}
|
||||
rule := &AlertRule{
|
||||
Critical: Level{Level: 10, Operator: ">"},
|
||||
Transformer: &AggregationTransformer{Method: "avg"},
|
||||
}
|
||||
|
||||
timeSeries := []*tsdb.TimeSeries{
|
||||
tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}),
|
||||
@@ -94,7 +116,10 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("first serie is good, second is critical", func() {
|
||||
rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "sum"}
|
||||
rule := &AlertRule{
|
||||
Critical: Level{Level: 10, Operator: ">"},
|
||||
Transformer: &AggregationTransformer{Method: "avg"},
|
||||
}
|
||||
|
||||
timeSeries := []*tsdb.TimeSeries{
|
||||
tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}),
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type DashAlertExtractor struct {
|
||||
Dash *m.Dashboard
|
||||
OrgId int64
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor {
|
||||
return &DashAlertExtractor{
|
||||
Dash: dash,
|
||||
OrgId: orgId,
|
||||
log: log.New("alerting.extractor"),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (int64, error) {
|
||||
if dsName == "" {
|
||||
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
for _, ds := range query.Result {
|
||||
if ds.IsDefault {
|
||||
return ds.Id, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return query.Result.Id, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New("Could not find datasource id for " + dsName)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
e.log.Debug("GetAlerts")
|
||||
|
||||
alerts := make([]*m.Alert, 0)
|
||||
|
||||
for _, rowObj := range e.Dash.Data.Get("rows").MustArray() {
|
||||
row := simplejson.NewFromAny(rowObj)
|
||||
|
||||
for _, panelObj := range row.Get("panels").MustArray() {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
jsonAlert, hasAlert := panel.CheckGet("alert")
|
||||
|
||||
if !hasAlert {
|
||||
continue
|
||||
}
|
||||
|
||||
alert := &m.Alert{
|
||||
DashboardId: e.Dash.Id,
|
||||
OrgId: e.OrgId,
|
||||
PanelId: panel.Get("id").MustInt64(),
|
||||
Id: jsonAlert.Get("id").MustInt64(),
|
||||
Name: jsonAlert.Get("name").MustString(),
|
||||
Scheduler: jsonAlert.Get("scheduler").MustInt64(),
|
||||
Enabled: jsonAlert.Get("enabled").MustBool(),
|
||||
Description: jsonAlert.Get("description").MustString(),
|
||||
}
|
||||
|
||||
valueQuery := jsonAlert.Get("query")
|
||||
valueQueryRef := valueQuery.Get("refId").MustString()
|
||||
for _, targetsObj := range panel.Get("targets").MustArray() {
|
||||
target := simplejson.NewFromAny(targetsObj)
|
||||
|
||||
if target.Get("refId").MustString() == valueQueryRef {
|
||||
dsName := ""
|
||||
if target.Get("datasource").MustString() != "" {
|
||||
dsName = target.Get("datasource").MustString()
|
||||
} else if panel.Get("datasource").MustString() != "" {
|
||||
dsName = panel.Get("datasource").MustString()
|
||||
}
|
||||
|
||||
if datasourceId, err := e.lookupDatasourceId(dsName); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
valueQuery.SetPath([]string{"datasourceId"}, datasourceId)
|
||||
}
|
||||
|
||||
targetQuery := target.Get("target").MustString()
|
||||
if targetQuery != "" {
|
||||
jsonAlert.SetPath([]string{"query", "query"}, targetQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alert.Expression = jsonAlert
|
||||
|
||||
// validate
|
||||
_, err := NewAlertRuleFromDBModel(alert)
|
||||
if err == nil && alert.ValidToSave() {
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
||||
return alerts, nil
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAlertRuleExtraction(t *testing.T) {
|
||||
|
||||
Convey("Parsing alert rules from dashboard json", t, func() {
|
||||
Convey("Parsing and validating alerts from dashboards", func() {
|
||||
json := `{
|
||||
"id": 57,
|
||||
"title": "Graphite 4",
|
||||
"originalTitle": "Graphite 4",
|
||||
"tags": [
|
||||
"graphite"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active desktop users",
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"id": 3,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
||||
}
|
||||
],
|
||||
"datasource": null,
|
||||
"alert": {
|
||||
"name": "name1",
|
||||
"description": "desc1",
|
||||
"scheduler": 1,
|
||||
"enabled": true,
|
||||
"critical": {
|
||||
"level": 20,
|
||||
"op": ">"
|
||||
},
|
||||
"frequency": "60s",
|
||||
"query": {
|
||||
"from": "5m",
|
||||
"refId": "A",
|
||||
"to": "now"
|
||||
},
|
||||
"transform": {
|
||||
"method": "avg",
|
||||
"type": "aggregation"
|
||||
},
|
||||
"warn": {
|
||||
"level": 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",
|
||||
"scheduler": 0,
|
||||
"enabled": true,
|
||||
"critical": {
|
||||
"level": 20,
|
||||
"op": ">"
|
||||
},
|
||||
"frequency": "60s",
|
||||
"query": {
|
||||
"from": "5m",
|
||||
"refId": "A",
|
||||
"to": "now"
|
||||
},
|
||||
"transform": {
|
||||
"method": "avg",
|
||||
"name": "aggregation"
|
||||
},
|
||||
"warn": {
|
||||
"level": 10,
|
||||
"op": ">"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Row"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
"datasource": "InfluxDB",
|
||||
"id": 2,
|
||||
"targets": [
|
||||
{
|
||||
"dsType": "influxdb",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"measurement": "cpu",
|
||||
"policy": "default",
|
||||
"query": "SELECT mean(\"value\") FROM \"cpu\" WHERE $timeFilter GROUP BY time($interval) fill(null)",
|
||||
"refId": "A",
|
||||
"resultFormat": "table",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": [],
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"title": "Broken influxdb panel",
|
||||
"transform": "table",
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"title": "New row"
|
||||
}
|
||||
]
|
||||
|
||||
}`
|
||||
dashJson, err := simplejson.NewJson([]byte(json))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
|
||||
// mock data
|
||||
defaultDs := &m.DataSource{Id: 12, OrgId: 2, Name: "I am default", IsDefault: true}
|
||||
graphite2Ds := &m.DataSource{Id: 15, OrgId: 2, Name: "graphite2"}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
|
||||
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error {
|
||||
if query.Name == defaultDs.Name {
|
||||
query.Result = defaultDs
|
||||
}
|
||||
if query.Name == graphite2Ds.Name {
|
||||
query.Result = graphite2Ds
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
|
||||
Convey("Get rules without error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("all properties have been set", func() {
|
||||
So(len(alerts), ShouldEqual, 2)
|
||||
|
||||
for _, v := range alerts {
|
||||
So(v.DashboardId, ShouldEqual, 57)
|
||||
So(v.Name, ShouldNotBeEmpty)
|
||||
So(v.Description, ShouldNotBeEmpty)
|
||||
}
|
||||
|
||||
Convey("should extract scheduler property", func() {
|
||||
So(alerts[0].Scheduler, ShouldEqual, 1)
|
||||
So(alerts[1].Scheduler, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("should extract panel idc", func() {
|
||||
So(alerts[0].PanelId, ShouldEqual, 3)
|
||||
So(alerts[1].PanelId, ShouldEqual, 4)
|
||||
})
|
||||
|
||||
Convey("should extract name and desc", func() {
|
||||
So(alerts[0].Name, ShouldEqual, "name1")
|
||||
So(alerts[0].Description, ShouldEqual, "desc1")
|
||||
So(alerts[1].Name, ShouldEqual, "name2")
|
||||
So(alerts[1].Description, ShouldEqual, "desc2")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -17,22 +17,15 @@ type AlertResult struct {
|
||||
AlertJob *AlertJob
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
type Level struct {
|
||||
Operator string
|
||||
Level float64
|
||||
}
|
||||
|
||||
type AlertQuery struct {
|
||||
Query string
|
||||
DatasourceId int64
|
||||
DashboardId int64
|
||||
PanelId int64
|
||||
Query string
|
||||
QueryRefId string
|
||||
WarnLevel float64
|
||||
CritLevel float64
|
||||
WarnOperator string
|
||||
CritOperator string
|
||||
Frequency int64
|
||||
Name string
|
||||
Description string
|
||||
QueryRange int
|
||||
Aggregator string
|
||||
State string
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
@@ -49,22 +49,7 @@ func (arr *AlertRuleReader) Fetch() []*AlertRule {
|
||||
|
||||
res := make([]*AlertRule, len(cmd.Result))
|
||||
for i, ruleDef := range cmd.Result {
|
||||
model := &AlertRule{}
|
||||
model.Id = ruleDef.Id
|
||||
model.OrgId = ruleDef.OrgId
|
||||
model.DatasourceId = ruleDef.DatasourceId
|
||||
model.Query = ruleDef.Query
|
||||
model.QueryRefId = ruleDef.QueryRefId
|
||||
model.WarnLevel = ruleDef.WarnLevel
|
||||
model.WarnOperator = ruleDef.WarnOperator
|
||||
model.CritLevel = ruleDef.CritLevel
|
||||
model.CritOperator = ruleDef.CritOperator
|
||||
model.Frequency = ruleDef.Frequency
|
||||
model.Name = ruleDef.Name
|
||||
model.Description = ruleDef.Description
|
||||
model.Aggregator = ruleDef.Aggregator
|
||||
model.State = ruleDef.State
|
||||
model.QueryRange = ruleDef.QueryRange
|
||||
model, _ := NewAlertRuleFromDBModel(ruleDef)
|
||||
res[i] = model
|
||||
}
|
||||
|
||||
@@ -8,20 +8,22 @@ import (
|
||||
|
||||
type SchedulerImpl struct {
|
||||
jobs map[int64]*AlertJob
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewScheduler() Scheduler {
|
||||
return &SchedulerImpl{
|
||||
jobs: make(map[int64]*AlertJob, 0),
|
||||
log: log.New("alerting.scheduler"),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SchedulerImpl) Update(rules []*AlertRule) {
|
||||
log.Debug("Scheduler: Update()")
|
||||
func (s *SchedulerImpl) Update(alerts []*AlertRule) {
|
||||
s.log.Debug("Scheduling update", "alerts.count", len(alerts))
|
||||
|
||||
jobs := make(map[int64]*AlertJob, 0)
|
||||
|
||||
for i, rule := range rules {
|
||||
for i, rule := range alerts {
|
||||
var job *AlertJob
|
||||
if s.jobs[rule.Id] != nil {
|
||||
job = s.jobs[rule.Id]
|
||||
@@ -38,7 +40,6 @@ func (s *SchedulerImpl) Update(rules []*AlertRule) {
|
||||
jobs[rule.Id] = job
|
||||
}
|
||||
|
||||
log.Debug("Scheduler: Selected %d jobs", len(jobs))
|
||||
s.jobs = jobs
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *AlertJob) {
|
||||
|
||||
for _, job := range s.jobs {
|
||||
if now%job.Rule.Frequency == 0 && job.Running == false {
|
||||
log.Trace("Scheduler: Putting job on to exec queue: %s", job.Rule.Name)
|
||||
s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name)
|
||||
execQueue <- job
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package transformer
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package transformer
|
||||
|
||||
import "github.com/grafana/grafana/pkg/tsdb"
|
||||
|
||||
type Transformer interface {
|
||||
Transform(timeserie *tsdb.TimeSeries) (float64, error)
|
||||
}
|
||||
@@ -44,6 +44,7 @@ func searchHandler(query *Query) error {
|
||||
IsStarred: query.IsStarred,
|
||||
OrgId: query.OrgId,
|
||||
DashboardIds: query.DashboardIds,
|
||||
Limit: query.Limit,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&dashQuery); err != nil {
|
||||
|
||||
@@ -42,6 +42,7 @@ type FindPersistedDashboardsQuery struct {
|
||||
UserId int64
|
||||
IsStarred bool
|
||||
DashboardIds []int
|
||||
Limit int
|
||||
|
||||
Result HitList
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func HeartBeat(query *m.HeartBeatCommand) error {
|
||||
*/
|
||||
|
||||
func GetAlertById(query *m.GetAlertByIdQuery) error {
|
||||
alert := m.AlertRule{}
|
||||
alert := m.Alert{}
|
||||
has, err := x.Id(query.Id).Get(&alert)
|
||||
if !has {
|
||||
return fmt.Errorf("could not find alert")
|
||||
@@ -78,8 +78,8 @@ func GetAlertById(query *m.GetAlertByIdQuery) error {
|
||||
}
|
||||
|
||||
func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error {
|
||||
var alerts []*m.AlertRule
|
||||
err := x.Sql("select * from alert_rule").Find(&alerts)
|
||||
var alerts []*m.Alert
|
||||
err := x.Sql("select * from alert").Find(&alerts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error {
|
||||
|
||||
func DeleteAlertById(cmd *m.DeleteAlertCommand) error {
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
if _, err := sess.Exec("DELETE FROM alert_rule WHERE id = ?", cmd.AlertId); err != nil {
|
||||
if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", cmd.AlertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
sql.WriteString(`SELECT *
|
||||
from alert_rule
|
||||
from alert
|
||||
`)
|
||||
|
||||
sql.WriteString(`WHERE org_id = ?`)
|
||||
@@ -131,7 +131,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
||||
sql.WriteString(")")
|
||||
}
|
||||
|
||||
alerts := make([]*m.AlertRule, 0)
|
||||
alerts := make([]*m.Alert, 0)
|
||||
if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -141,15 +141,17 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
||||
}
|
||||
|
||||
func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error {
|
||||
alerts := make([]*m.AlertRule, 0)
|
||||
alerts := make([]*m.Alert, 0)
|
||||
sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
|
||||
|
||||
for _, alert := range alerts {
|
||||
_, err := sess.Exec("DELETE FROM alert_rule WHERE id = ? ", alert.Id)
|
||||
_, err := sess.Exec("DELETE FROM alert WHERE id = ? ", alert.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlog.Debug("Alert deleted (due to dashboard deletion)", "name", alert.Name, "id", alert.Id)
|
||||
|
||||
if err := SaveAlertChange("DELETED", alert, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -172,21 +174,22 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Session) error {
|
||||
func upsertAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) error {
|
||||
for _, alert := range posted {
|
||||
update := false
|
||||
var alertToUpdate *m.AlertRule
|
||||
var alertToUpdate *m.Alert
|
||||
|
||||
for _, k := range alerts {
|
||||
if alert.PanelId == k.PanelId {
|
||||
update = true
|
||||
alert.Id = k.Id
|
||||
alertToUpdate = k
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if update {
|
||||
if alertToUpdate.Equals(alert) {
|
||||
if alertToUpdate.ContainsUpdates(alert) {
|
||||
alert.Updated = time.Now()
|
||||
alert.State = alertToUpdate.State
|
||||
_, err := sess.Id(alert.Id).Update(alert)
|
||||
@@ -194,6 +197,7 @@ func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Sessi
|
||||
return err
|
||||
}
|
||||
|
||||
sqlog.Debug("Alert updated", "name", alert.Name, "id", alert.Id)
|
||||
SaveAlertChange("UPDATED", alert, sess)
|
||||
}
|
||||
|
||||
@@ -205,6 +209,8 @@ func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Sessi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlog.Debug("Alert inserted", "name", alert.Name, "id", alert.Id)
|
||||
SaveAlertChange("CREATED", alert, sess)
|
||||
}
|
||||
}
|
||||
@@ -212,22 +218,25 @@ func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Sessi
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteMissingAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Session) error {
|
||||
func deleteMissingAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) error {
|
||||
for _, missingAlert := range alerts {
|
||||
missing := true
|
||||
|
||||
for _, k := range posted {
|
||||
if missingAlert.PanelId == k.PanelId {
|
||||
missing = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if missing {
|
||||
_, err := sess.Exec("DELETE FROM alert_rule WHERE id = ?", missingAlert.Id)
|
||||
_, err := sess.Exec("DELETE FROM alert WHERE id = ?", missingAlert.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlog.Debug("Alert deleted", "name", missingAlert.Name, "id", missingAlert.Id)
|
||||
|
||||
err = SaveAlertChange("DELETED", missingAlert, sess)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -238,12 +247,12 @@ func deleteMissingAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xor
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.AlertRule, error) {
|
||||
alerts := make([]*m.AlertRule, 0)
|
||||
func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.Alert, error) {
|
||||
alerts := make([]*m.Alert, 0)
|
||||
err := sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
|
||||
|
||||
if err != nil {
|
||||
return []*m.AlertRule{}, err
|
||||
return []*m.Alert{}, err
|
||||
}
|
||||
|
||||
return alerts, nil
|
||||
@@ -39,7 +39,7 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error {
|
||||
params = append(params, query.Limit)
|
||||
}
|
||||
|
||||
alertChanges := make([]*m.AlertRuleChange, 0)
|
||||
alertChanges := make([]*m.AlertChange, 0)
|
||||
if err := x.Sql(sql.String(), params...).Find(&alertChanges); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -48,8 +48,8 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveAlertChange(change string, alert *m.AlertRule, sess *xorm.Session) error {
|
||||
_, err := sess.Insert(&m.AlertRuleChange{
|
||||
func SaveAlertChange(change string, alert *m.Alert, sess *xorm.Session) error {
|
||||
_, err := sess.Insert(&m.AlertChange{
|
||||
OrgId: alert.OrgId,
|
||||
Type: change,
|
||||
Created: time.Now(),
|
||||
|
||||
@@ -20,22 +20,13 @@ func TestAlertRuleChangesDataAccess(t *testing.T) {
|
||||
var err error
|
||||
|
||||
Convey("When dashboard is removed", func() {
|
||||
items := []*m.AlertRule{
|
||||
items := []*m.Alert{
|
||||
{
|
||||
PanelId: 1,
|
||||
DashboardId: testDash.Id,
|
||||
Query: "Query",
|
||||
QueryRefId: "A",
|
||||
WarnLevel: 30,
|
||||
CritLevel: 50,
|
||||
WarnOperator: ">",
|
||||
CritOperator: ">",
|
||||
Frequency: 10,
|
||||
Name: "Alerting title",
|
||||
Description: "Alerting description",
|
||||
QueryRange: 3600,
|
||||
Aggregator: "avg",
|
||||
OrgId: FakeOrgId,
|
||||
PanelId: 1,
|
||||
DashboardId: testDash.Id,
|
||||
Name: "Alerting title",
|
||||
Description: "Alerting description",
|
||||
OrgId: FakeOrgId,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,34 +3,25 @@ package sqlstore
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAlertingDataAccess(t *testing.T) {
|
||||
|
||||
Convey("Testing Alerting data access", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
testDash := insertTestDashboard("dashboard with alerts", 1, "alert")
|
||||
|
||||
items := []*m.AlertRule{
|
||||
items := []*m.Alert{
|
||||
{
|
||||
PanelId: 1,
|
||||
DashboardId: testDash.Id,
|
||||
OrgId: testDash.OrgId,
|
||||
Query: "Query",
|
||||
QueryRefId: "A",
|
||||
WarnLevel: 30,
|
||||
CritLevel: 50,
|
||||
WarnOperator: ">",
|
||||
CritOperator: ">",
|
||||
Frequency: 10,
|
||||
Name: "Alerting title",
|
||||
Description: "Alerting description",
|
||||
QueryRange: 3600,
|
||||
Aggregator: "avg",
|
||||
DatasourceId: 42,
|
||||
PanelId: 1,
|
||||
DashboardId: testDash.Id,
|
||||
OrgId: testDash.OrgId,
|
||||
Name: "Alerting title",
|
||||
Description: "Alerting description",
|
||||
Expression: simplejson.New(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -58,25 +49,14 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
|
||||
alert := alertQuery.Result[0]
|
||||
So(err2, ShouldBeNil)
|
||||
So(alert.Frequency, ShouldEqual, 10)
|
||||
So(alert.WarnLevel, ShouldEqual, 30)
|
||||
So(alert.CritLevel, ShouldEqual, 50)
|
||||
So(alert.WarnOperator, ShouldEqual, ">")
|
||||
So(alert.CritOperator, ShouldEqual, ">")
|
||||
So(alert.Query, ShouldEqual, "Query")
|
||||
So(alert.QueryRefId, ShouldEqual, "A")
|
||||
So(alert.Name, ShouldEqual, "Alerting title")
|
||||
So(alert.Description, ShouldEqual, "Alerting description")
|
||||
So(alert.QueryRange, ShouldEqual, 3600)
|
||||
So(alert.Aggregator, ShouldEqual, "avg")
|
||||
So(alert.State, ShouldEqual, "OK")
|
||||
So(alert.DatasourceId, ShouldEqual, 42)
|
||||
})
|
||||
|
||||
Convey("Alerts with same dashboard id and panel id should update", func() {
|
||||
modifiedItems := items
|
||||
modifiedItems[0].Query = "Updated Query"
|
||||
modifiedItems[0].State = "ALERT"
|
||||
modifiedItems[0].Name = "Name"
|
||||
|
||||
modifiedCmd := m.SaveAlertsCommand{
|
||||
DashboardId: testDash.Id,
|
||||
@@ -97,7 +77,7 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
|
||||
So(err2, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Query, ShouldEqual, "Updated Query")
|
||||
So(query.Result[0].Name, ShouldEqual, "Name")
|
||||
|
||||
Convey("Alert state should not be updated", func() {
|
||||
So(query.Result[0].State, ShouldEqual, "OK")
|
||||
@@ -116,24 +96,27 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Multiple alerts per dashboard", func() {
|
||||
multipleItems := []*m.AlertRule{
|
||||
multipleItems := []*m.Alert{
|
||||
{
|
||||
DashboardId: testDash.Id,
|
||||
PanelId: 1,
|
||||
Query: "1",
|
||||
Name: "1",
|
||||
OrgId: 1,
|
||||
Expression: simplejson.New(),
|
||||
},
|
||||
{
|
||||
DashboardId: testDash.Id,
|
||||
PanelId: 2,
|
||||
Query: "2",
|
||||
Name: "2",
|
||||
OrgId: 1,
|
||||
Expression: simplejson.New(),
|
||||
},
|
||||
{
|
||||
DashboardId: testDash.Id,
|
||||
PanelId: 3,
|
||||
Query: "3",
|
||||
Name: "3",
|
||||
OrgId: 1,
|
||||
Expression: simplejson.New(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -178,21 +161,12 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When dashboard is removed", func() {
|
||||
items := []*m.AlertRule{
|
||||
items := []*m.Alert{
|
||||
{
|
||||
PanelId: 1,
|
||||
DashboardId: testDash.Id,
|
||||
Query: "Query",
|
||||
QueryRefId: "A",
|
||||
WarnLevel: 30,
|
||||
CritLevel: 50,
|
||||
WarnOperator: ">",
|
||||
CritOperator: ">",
|
||||
Frequency: 10,
|
||||
Name: "Alerting title",
|
||||
Description: "Alerting description",
|
||||
QueryRange: 3600,
|
||||
Aggregator: "avg",
|
||||
PanelId: 1,
|
||||
DashboardId: testDash.Id,
|
||||
Name: "Alerting title",
|
||||
Description: "Alerting description",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -19,7 +20,7 @@ func SetNewAlertState(cmd *m.UpdateAlertStateCommand) error {
|
||||
return fmt.Errorf("new state is invalid")
|
||||
}
|
||||
|
||||
alert := m.AlertRule{}
|
||||
alert := m.Alert{}
|
||||
has, err := sess.Id(cmd.AlertId).Get(&alert)
|
||||
if !has {
|
||||
return fmt.Errorf("Could not find alert")
|
||||
|
||||
@@ -13,22 +13,13 @@ func TestAlertingStateAccess(t *testing.T) {
|
||||
|
||||
testDash := insertTestDashboard("dashboard with alerts", 1, "alert")
|
||||
|
||||
items := []*m.AlertRule{
|
||||
items := []*m.Alert{
|
||||
{
|
||||
PanelId: 1,
|
||||
DashboardId: testDash.Id,
|
||||
OrgId: testDash.OrgId,
|
||||
Query: "Query",
|
||||
QueryRefId: "A",
|
||||
WarnLevel: 30,
|
||||
CritLevel: 50,
|
||||
WarnOperator: ">",
|
||||
CritOperator: ">",
|
||||
Frequency: 10,
|
||||
Name: "Alerting title",
|
||||
Description: "Alerting description",
|
||||
QueryRange: 3600,
|
||||
Aggregator: "avg",
|
||||
PanelId: 1,
|
||||
DashboardId: testDash.Id,
|
||||
OrgId: testDash.OrgId,
|
||||
Name: "Alerting title",
|
||||
Description: "Alerting description",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,11 @@ type DashboardSearchProjection struct {
|
||||
}
|
||||
|
||||
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
|
||||
limit := query.Limit
|
||||
if limit == 0 {
|
||||
limit = 1000
|
||||
}
|
||||
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
@@ -165,7 +170,8 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
|
||||
params = append(params, "%"+query.Title+"%")
|
||||
}
|
||||
|
||||
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
|
||||
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT ?"))
|
||||
params = append(params, limit)
|
||||
|
||||
var res []DashboardSearchProjection
|
||||
|
||||
|
||||
@@ -1,415 +0,0 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAlertModel(t *testing.T) {
|
||||
|
||||
Convey("Parsing alerts from dashboard", t, func() {
|
||||
json := `{
|
||||
"id": 57,
|
||||
"title": "Graphite 4",
|
||||
"originalTitle": "Graphite 4",
|
||||
"tags": [
|
||||
"graphite"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": false,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active desktop users",
|
||||
"error": false,
|
||||
"span": 6,
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"isNew": true,
|
||||
"id": 3,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
||||
}
|
||||
],
|
||||
"datasource": null,
|
||||
"renderer": "flot",
|
||||
"yaxes": [
|
||||
{
|
||||
"label": null,
|
||||
"show": true,
|
||||
"logBase": 1,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"format": "short"
|
||||
},
|
||||
{
|
||||
"label": null,
|
||||
"show": true,
|
||||
"logBase": 1,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"format": "short"
|
||||
}
|
||||
],
|
||||
"xaxis": {
|
||||
"show": true
|
||||
},
|
||||
"grid": {
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": false,
|
||||
"percentage": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"shared": true,
|
||||
"msResolution": false
|
||||
},
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"aliasColors": {},
|
||||
"seriesOverrides": [],
|
||||
"alerting": {
|
||||
"queryRef": "A",
|
||||
"warnLevel": 30,
|
||||
"critLevel": 50,
|
||||
"warnOperator": ">",
|
||||
"critOperator": ">",
|
||||
"aggregator": "sum",
|
||||
"queryRange": 3600,
|
||||
"frequency": 10,
|
||||
"name": "active desktop users",
|
||||
"description": "restart webservers"
|
||||
},
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"title": "Active mobile users",
|
||||
"error": false,
|
||||
"span": 6,
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"isNew": true,
|
||||
"id": 4,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"
|
||||
}
|
||||
],
|
||||
"datasource": "graphite2",
|
||||
"renderer": "flot",
|
||||
"yaxes": [
|
||||
{
|
||||
"label": null,
|
||||
"show": true,
|
||||
"logBase": 1,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"format": "short"
|
||||
},
|
||||
{
|
||||
"label": null,
|
||||
"show": true,
|
||||
"logBase": 1,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"format": "short"
|
||||
}
|
||||
],
|
||||
"xaxis": {
|
||||
"show": true
|
||||
},
|
||||
"grid": {
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": false,
|
||||
"percentage": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"shared": true,
|
||||
"msResolution": false
|
||||
},
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"aliasColors": {
|
||||
"mobile": "#EAB839"
|
||||
},
|
||||
"seriesOverrides": [],
|
||||
"alerting": {
|
||||
"queryRef": "A",
|
||||
"warnOperator": ">",
|
||||
"critOperator": ">",
|
||||
"warnLevel": 300,
|
||||
"critLevel": 500,
|
||||
"aggregator": "avg",
|
||||
"queryRange": 3600,
|
||||
"frequency": 10,
|
||||
"name": "active mobile users",
|
||||
"description": "restart itunes"
|
||||
},
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"title": "Row"
|
||||
},
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
{
|
||||
"columns": [],
|
||||
"datasource": "InfluxDB",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fontSize": "100%",
|
||||
"id": 2,
|
||||
"isNew": true,
|
||||
"pageSize": null,
|
||||
"scroll": true,
|
||||
"showHeader": true,
|
||||
"sort": {
|
||||
"col": 0,
|
||||
"desc": true
|
||||
},
|
||||
"span": 6,
|
||||
"styles": [
|
||||
{
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"pattern": "Time",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"colorMode": null,
|
||||
"colors": [
|
||||
"rgba(245, 54, 54, 0.9)",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"rgba(50, 172, 45, 0.97)"
|
||||
],
|
||||
"decimals": 2,
|
||||
"pattern": "/.*/",
|
||||
"thresholds": [],
|
||||
"type": "number",
|
||||
"unit": "short"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"dsType": "influxdb",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"measurement": "cpu",
|
||||
"policy": "default",
|
||||
"query": "SELECT mean(\"value\") FROM \"cpu\" WHERE $timeFilter GROUP BY time($interval) fill(null)",
|
||||
"refId": "A",
|
||||
"resultFormat": "table",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": [],
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"title": "Broken influxdb panel",
|
||||
"transform": "table",
|
||||
"type": "table",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"title": "New row"
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"now": true,
|
||||
"nowDelay": "5m",
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d",
|
||||
"7d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"schemaVersion": 12,
|
||||
"version": 16,
|
||||
"links": []
|
||||
}`
|
||||
dashboardJson, _ := simplejson.NewJson([]byte(json))
|
||||
cmd := &m.SaveDashboardCommand{
|
||||
Dashboard: dashboardJson,
|
||||
UserId: 1,
|
||||
OrgId: 1,
|
||||
Overwrite: true,
|
||||
Result: &m.Dashboard{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
InitTestDB(t)
|
||||
|
||||
AddDataSource(&m.AddDataSourceCommand{
|
||||
Name: "graphite2",
|
||||
OrgId: 1,
|
||||
Type: m.DS_INFLUXDB,
|
||||
Access: m.DS_ACCESS_DIRECT,
|
||||
Url: "http://test",
|
||||
IsDefault: false,
|
||||
Database: "site",
|
||||
})
|
||||
|
||||
AddDataSource(&m.AddDataSourceCommand{
|
||||
Name: "InfluxDB",
|
||||
OrgId: 1,
|
||||
Type: m.DS_GRAPHITE,
|
||||
Access: m.DS_ACCESS_DIRECT,
|
||||
Url: "http://test",
|
||||
IsDefault: true,
|
||||
})
|
||||
|
||||
alerts := alerting.ParseAlertsFromDashboard(cmd)
|
||||
|
||||
Convey("all properties have been set", func() {
|
||||
So(alerts, ShouldNotBeEmpty)
|
||||
So(len(alerts), ShouldEqual, 2)
|
||||
|
||||
for _, v := range alerts {
|
||||
So(v.DashboardId, ShouldEqual, 1)
|
||||
So(v.PanelId, ShouldNotEqual, 0)
|
||||
|
||||
So(v.WarnLevel, ShouldNotBeEmpty)
|
||||
So(v.CritLevel, ShouldNotBeEmpty)
|
||||
|
||||
So(v.Aggregator, ShouldNotBeEmpty)
|
||||
So(v.Query, ShouldNotBeEmpty)
|
||||
So(v.QueryRefId, ShouldNotBeEmpty)
|
||||
So(v.QueryRange, ShouldNotEqual, 0)
|
||||
So(v.Frequency, ShouldNotEqual, 0)
|
||||
So(v.Name, ShouldNotBeEmpty)
|
||||
So(v.Description, ShouldNotBeEmpty)
|
||||
}
|
||||
|
||||
So(alerts[0].WarnLevel, ShouldEqual, 30)
|
||||
So(alerts[1].WarnLevel, ShouldEqual, 300)
|
||||
|
||||
So(alerts[0].Frequency, ShouldEqual, 10)
|
||||
So(alerts[1].Frequency, ShouldEqual, 10)
|
||||
|
||||
So(alerts[0].CritLevel, ShouldEqual, 50)
|
||||
So(alerts[1].CritLevel, ShouldEqual, 500)
|
||||
|
||||
So(alerts[0].CritOperator, ShouldEqual, ">")
|
||||
So(alerts[1].CritOperator, ShouldEqual, ">")
|
||||
So(alerts[0].WarnOperator, ShouldEqual, ">")
|
||||
So(alerts[1].WarnOperator, ShouldEqual, ">")
|
||||
|
||||
So(alerts[0].Query, ShouldEqual, `{"refId":"A","target":"aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"}`)
|
||||
So(alerts[1].Query, ShouldEqual, `{"refId":"A","target":"aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}`)
|
||||
|
||||
So(alerts[0].DatasourceId, ShouldEqual, 2)
|
||||
So(alerts[1].DatasourceId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
)
|
||||
|
||||
func InitTestDB(t *testing.T) {
|
||||
|
||||
t.Log("InitTestDB")
|
||||
x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
|
||||
//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
|
||||
//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
|
||||
@@ -24,7 +22,7 @@ func InitTestDB(t *testing.T) {
|
||||
|
||||
sqlutil.CleanDB(x)
|
||||
|
||||
if err := SetEngine(x, false); err != nil {
|
||||
if err := SetEngine(x); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,35 +7,29 @@ import (
|
||||
func addAlertMigrations(mg *Migrator) {
|
||||
|
||||
alertV1 := Table{
|
||||
Name: "alert_rule",
|
||||
Name: "alert",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "dashboard_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "datasource_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "panel_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "query", Type: DB_Text, Nullable: false},
|
||||
{Name: "query_ref_id", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "warn_level", Type: DB_Float, Nullable: false},
|
||||
{Name: "warn_operator", Type: DB_NVarchar, Length: 10, Nullable: false},
|
||||
{Name: "crit_level", Type: DB_Float, Nullable: false},
|
||||
{Name: "crit_operator", Type: DB_NVarchar, Length: 10, Nullable: false},
|
||||
{Name: "frequency", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "description", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "query_range", Type: DB_Int, Nullable: false},
|
||||
{Name: "aggregator", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "expression", Type: DB_Text, Nullable: false},
|
||||
{Name: "scheduler", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "frequency", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "enabled", Type: DB_Bool, Nullable: false},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
{Name: "updated", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create alert_rule table v2", NewAddTableMigration(alertV1))
|
||||
mg.AddMigration("create alert table v1", NewAddTableMigration(alertV1))
|
||||
|
||||
alert_changes := Table{
|
||||
Name: "alert_rule_change",
|
||||
Name: "alert_change",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "alert_id", Type: DB_BigInt, Nullable: false},
|
||||
@@ -45,7 +39,7 @@ func addAlertMigrations(mg *Migrator) {
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create alert_rules_updates table v1", NewAddTableMigration(alert_changes))
|
||||
mg.AddMigration("create alert_change table v1", NewAddTableMigration(alert_changes))
|
||||
|
||||
alert_state_log := Table{
|
||||
Name: "alert_state",
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/go-xorm/xorm"
|
||||
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
"github.com/inconshreveable/log15"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
//"github.com/grafana/grafana/pkg/log"
|
||||
@@ -30,7 +29,6 @@ func TestMigrations(t *testing.T) {
|
||||
sqlutil.CleanDB(x)
|
||||
|
||||
mg := NewMigrator(x)
|
||||
mg.Logger.SetHandler(log15.DiscardHandler())
|
||||
AddMigrations(mg)
|
||||
|
||||
err = mg.Start()
|
||||
|
||||
@@ -107,7 +107,7 @@ func (mg *Migrator) Start() error {
|
||||
}
|
||||
|
||||
func (mg *Migrator) exec(m Migration) error {
|
||||
log.Info("Executing migration", "id", m.Id())
|
||||
mg.Logger.Info("Executing migration", "id", m.Id())
|
||||
|
||||
err := mg.inTransaction(func(sess *xorm.Session) error {
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ func NewEngine() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = SetEngine(x, setting.Env == setting.DEV)
|
||||
err = SetEngine(x)
|
||||
|
||||
if err != nil {
|
||||
sqlog.Error("Fail to initialize orm engine", "error", err)
|
||||
@@ -86,7 +86,7 @@ func NewEngine() {
|
||||
}
|
||||
}
|
||||
|
||||
func SetEngine(engine *xorm.Engine, enableLog bool) (err error) {
|
||||
func SetEngine(engine *xorm.Engine) (err error) {
|
||||
x = engine
|
||||
dialect = migrator.NewDialect(x.DriverName())
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
@@ -31,7 +31,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
result := &tsdb.BatchResult{}
|
||||
|
||||
params := url.Values{
|
||||
"from": []string{context.TimeRange.From},
|
||||
"from": []string{formatTimeRange(context.TimeRange.From)},
|
||||
"until": []string{context.TimeRange.To},
|
||||
"format": []string{"json"},
|
||||
"maxDataPoints": []string{"500"},
|
||||
@@ -39,7 +39,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
|
||||
for _, query := range queries {
|
||||
params["target"] = []string{
|
||||
getTargetFromQuery(query.Query),
|
||||
query.Query,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
var data []TargetResponseDTO
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
glog.Info("Failed to unmarshal graphite response", "error", err)
|
||||
glog.Info("Failed to unmarshal graphite response", "error", err, "body", string(body))
|
||||
result.Error = err
|
||||
return result
|
||||
}
|
||||
@@ -78,7 +78,6 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
return result
|
||||
}
|
||||
|
||||
func getTargetFromQuery(query string) string {
|
||||
json, _ := simplejson.NewJson([]byte(query))
|
||||
return json.Get("target").MustString()
|
||||
func formatTimeRange(input string) string {
|
||||
return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export class QueryPartDef {
|
||||
type: string;
|
||||
params: any[];
|
||||
defaultParams: any[];
|
||||
renderer: any;
|
||||
category: any;
|
||||
addStrategy: any;
|
||||
|
||||
constructor(options: any) {
|
||||
this.type = options.type;
|
||||
this.params = options.params;
|
||||
this.defaultParams = options.defaultParams;
|
||||
this.renderer = options.renderer;
|
||||
this.category = options.category;
|
||||
this.addStrategy = options.addStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryPart {
|
||||
part: any;
|
||||
def: QueryPartDef;
|
||||
params: any[];
|
||||
text: string;
|
||||
|
||||
constructor(part: any, def: any) {
|
||||
this.part = part;
|
||||
this.def = def;
|
||||
if (!this.def) {
|
||||
throw {message: 'Could not find query part ' + part.type};
|
||||
}
|
||||
|
||||
part.params = part.params || _.clone(this.def.defaultParams);
|
||||
this.params = part.params;
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
render(innerExpr: string) {
|
||||
return this.def.renderer(this, innerExpr);
|
||||
}
|
||||
|
||||
hasMultipleParamsInString (strValue, index) {
|
||||
if (strValue.indexOf(',') === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.def.params[index + 1] && this.def.params[index + 1].optional;
|
||||
}
|
||||
|
||||
updateParam (strValue, index) {
|
||||
// handle optional parameters
|
||||
// if string contains ',' and next param is optional, split and update both
|
||||
if (this.hasMultipleParamsInString(strValue, index)) {
|
||||
_.each(strValue.split(','), function(partVal: string, idx) {
|
||||
this.updateParam(partVal.trim(), idx);
|
||||
}, this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strValue === '' && this.def.params[index].optional) {
|
||||
this.params.splice(index, 1);
|
||||
} else {
|
||||
this.params[index] = strValue;
|
||||
}
|
||||
|
||||
this.part.params = this.params;
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
updateText() {
|
||||
if (this.params.length === 0) {
|
||||
this.text = this.def.type + '()';
|
||||
return;
|
||||
}
|
||||
|
||||
var text = this.def.type + '(';
|
||||
text += this.params.join(', ');
|
||||
text += ')';
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
export function functionRenderer(part, innerExpr) {
|
||||
var str = part.def.type + '(';
|
||||
var parameters = _.map(part.params, (value, index) => {
|
||||
var paramType = part.def.params[index];
|
||||
if (paramType.type === 'time') {
|
||||
if (value === 'auto') {
|
||||
value = '$interval';
|
||||
}
|
||||
}
|
||||
if (paramType.quote === 'single') {
|
||||
return "'" + value + "'";
|
||||
} else if (paramType.quote === 'double') {
|
||||
return '"' + value + '"';
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
if (innerExpr) {
|
||||
parameters.unshift(innerExpr);
|
||||
}
|
||||
return str + parameters.join(', ') + ')';
|
||||
}
|
||||
|
||||
|
||||
export function suffixRenderer(part, innerExpr) {
|
||||
return innerExpr + ' ' + part.params[0];
|
||||
}
|
||||
|
||||
export function identityRenderer(part, innerExpr) {
|
||||
return part.params[0];
|
||||
}
|
||||
|
||||
export function quotedIdentityRenderer(part, innerExpr) {
|
||||
return '"' + part.params[0] + '"';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
<div class="tight-form-func-controls">
|
||||
<span class="pointer fa fa-remove" ng-click="removeActionInternal()"></span>
|
||||
</div>
|
||||
|
||||
<a ng-click="toggleControls()" class="query-part-name">{{part.def.type}}</a>
|
||||
<span>(</span><span class="query-part-parameters"></span><span>)</span>
|
||||
`;
|
||||
|
||||
/** @ngInject */
|
||||
export function queryPartEditorDirective($compile, templateSrv) {
|
||||
|
||||
var paramTemplate = '<input type="text" style="display:none"' +
|
||||
' class="input-mini tight-form-func-param"></input>';
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
scope: {
|
||||
part: "=",
|
||||
removeAction: "&",
|
||||
partUpdated: "&",
|
||||
getOptions: "&",
|
||||
},
|
||||
link: function postLink($scope, elem) {
|
||||
var part = $scope.part;
|
||||
var partDef = part.def;
|
||||
var $paramsContainer = elem.find('.query-part-parameters');
|
||||
var $controlsContainer = elem.find('.tight-form-func-controls');
|
||||
|
||||
function clickFuncParam(paramIndex) {
|
||||
/*jshint validthis:true */
|
||||
var $link = $(this);
|
||||
var $input = $link.next();
|
||||
|
||||
$input.val(part.params[paramIndex]);
|
||||
$input.css('width', ($link.width() + 16) + 'px');
|
||||
|
||||
$link.hide();
|
||||
$input.show();
|
||||
$input.focus();
|
||||
$input.select();
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
if (typeahead) {
|
||||
$input.val('');
|
||||
typeahead.lookup();
|
||||
}
|
||||
}
|
||||
|
||||
function inputBlur(paramIndex) {
|
||||
/*jshint validthis:true */
|
||||
var $input = $(this);
|
||||
var $link = $input.prev();
|
||||
var newValue = $input.val();
|
||||
|
||||
if (newValue !== '' || part.def.params[paramIndex].optional) {
|
||||
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
|
||||
|
||||
part.updateParam($input.val(), paramIndex);
|
||||
$scope.$apply($scope.partUpdated);
|
||||
}
|
||||
|
||||
$input.hide();
|
||||
$link.show();
|
||||
}
|
||||
|
||||
function inputKeyPress(paramIndex, e) {
|
||||
/*jshint validthis:true */
|
||||
if (e.which === 13) {
|
||||
inputBlur.call(this, paramIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function inputKeyDown() {
|
||||
/*jshint validthis:true */
|
||||
this.style.width = (3 + this.value.length) * 8 + 'px';
|
||||
}
|
||||
|
||||
function addTypeahead($input, param, paramIndex) {
|
||||
if (!param.options && !param.dynamicLookup) {
|
||||
return;
|
||||
}
|
||||
|
||||
var typeaheadSource = function (query, callback) {
|
||||
if (param.options) { return param.options; }
|
||||
|
||||
$scope.$apply(function() {
|
||||
$scope.getOptions().then(function(result) {
|
||||
var dynamicOptions = _.map(result, function(op) { return op.value; });
|
||||
callback(dynamicOptions);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
var options = param.options;
|
||||
if (param.type === 'int') {
|
||||
options = _.map(options, function(val) { return val.toString(); });
|
||||
}
|
||||
|
||||
$input.typeahead({
|
||||
source: typeaheadSource,
|
||||
minLength: 0,
|
||||
items: 1000,
|
||||
updater: function (value) {
|
||||
setTimeout(function() {
|
||||
inputBlur.call($input[0], paramIndex);
|
||||
}, 0);
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
typeahead.lookup = function () {
|
||||
this.query = this.$element.val() || '';
|
||||
var items = this.source(this.query, $.proxy(this.process, this));
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
}
|
||||
|
||||
$scope.toggleControls = function() {
|
||||
var targetDiv = elem.closest('.tight-form');
|
||||
|
||||
if (elem.hasClass('show-function-controls')) {
|
||||
elem.removeClass('show-function-controls');
|
||||
targetDiv.removeClass('has-open-function');
|
||||
$controlsContainer.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
elem.addClass('show-function-controls');
|
||||
targetDiv.addClass('has-open-function');
|
||||
$controlsContainer.show();
|
||||
};
|
||||
|
||||
$scope.removeActionInternal = function() {
|
||||
$scope.toggleControls();
|
||||
$scope.removeAction();
|
||||
};
|
||||
|
||||
function addElementsAndCompile() {
|
||||
_.each(partDef.params, function(param, index) {
|
||||
if (param.optional && part.params.length <= index) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
$('<span>, </span>').appendTo($paramsContainer);
|
||||
}
|
||||
|
||||
var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
|
||||
var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
|
||||
var $input = $(paramTemplate);
|
||||
|
||||
$paramLink.appendTo($paramsContainer);
|
||||
$input.appendTo($paramsContainer);
|
||||
|
||||
$input.blur(_.partial(inputBlur, index));
|
||||
$input.keyup(inputKeyDown);
|
||||
$input.keypress(_.partial(inputKeyPress, index));
|
||||
$paramLink.click(_.partial(clickFuncParam, index));
|
||||
|
||||
addTypeahead($input, param, index);
|
||||
});
|
||||
}
|
||||
|
||||
function relink() {
|
||||
$paramsContainer.empty();
|
||||
addElementsAndCompile();
|
||||
}
|
||||
|
||||
relink();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('queryPartEditor', queryPartEditorDirective);
|
||||
@@ -33,6 +33,7 @@ import {Emitter} from './utils/emitter';
|
||||
import {layoutSelector} from './components/layout_selector/layout_selector';
|
||||
import {switchDirective} from './components/switch';
|
||||
import {dashboardSelector} from './components/dashboard_selector';
|
||||
import {queryPartEditorDirective} from './components/query_part/query_part_editor';
|
||||
import 'app/core/controllers/all';
|
||||
import 'app/core/services/all';
|
||||
import 'app/core/routes/routes';
|
||||
@@ -56,4 +57,5 @@ export {
|
||||
Emitter,
|
||||
appEvents,
|
||||
dashboardSelector,
|
||||
queryPartEditorDirective,
|
||||
};
|
||||
|
||||
@@ -120,25 +120,28 @@ function (angular, _, $) {
|
||||
if (this.panelScopes.length === 0) { return; }
|
||||
|
||||
if (this.dashboard.meta.fullscreen) {
|
||||
if (this.fullscreenPanel) {
|
||||
this.leaveFullscreen(false);
|
||||
}
|
||||
var panelScope = this.getPanelScope(this.state.panelId);
|
||||
// panel could be about to be created/added and scope does
|
||||
// not exist yet
|
||||
if (!panelScope) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.fullscreenPanel) {
|
||||
// if already fullscreen
|
||||
if (this.fullscreenPanel === panelScope) {
|
||||
return;
|
||||
} else {
|
||||
this.leaveFullscreen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!panelScope.ctrl.editModeInitiated) {
|
||||
panelScope.ctrl.initEditMode();
|
||||
}
|
||||
|
||||
this.enterFullscreen(panelScope);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.fullscreenPanel) {
|
||||
if (!panelScope.ctrl.fullscreen) {
|
||||
this.enterFullscreen(panelScope);
|
||||
}
|
||||
} else if (this.fullscreenPanel) {
|
||||
this.leaveFullscreen(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
|
||||
Current
|
||||
</span>
|
||||
<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
|
||||
<a ng-click="ctrl.setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
|
||||
Select
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -152,8 +152,8 @@ export class PanelCtrl {
|
||||
calculatePanelHeight() {
|
||||
if (this.fullscreen) {
|
||||
var docHeight = $(window).height();
|
||||
var editHeight = Math.floor(docHeight * 0.3);
|
||||
var fullscreenHeight = Math.floor(docHeight * 0.7);
|
||||
var editHeight = Math.floor(docHeight * 0.4);
|
||||
var fullscreenHeight = Math.floor(docHeight * 0.6);
|
||||
this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
|
||||
} else {
|
||||
this.containerHeight = this.panel.height || this.row.height;
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-repeat="part in selectParts">
|
||||
<influx-query-part-editor
|
||||
<query-part-editor
|
||||
class="gf-form-label query-part"
|
||||
part="part"
|
||||
remove-action="ctrl.removeSelectPart(selectParts, part)"
|
||||
part-updated="ctrl.selectPartUpdated(selectParts, part)"
|
||||
get-options="ctrl.getPartOptions(part)">
|
||||
</influx-query-part-editor>
|
||||
</query-part-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
@@ -62,12 +62,12 @@
|
||||
<span>GROUP BY</span>
|
||||
</label>
|
||||
|
||||
<influx-query-part-editor
|
||||
<query-part-editor
|
||||
ng-repeat="part in ctrl.queryModel.groupByParts"
|
||||
part="part"
|
||||
class="gf-form-label query-part"
|
||||
remove-action="ctrl.removeGroupByPart(part, $index)" part-updated="ctrl.refresh();" get-options="ctrl.getPartOptions(part)">
|
||||
</influx-query-part-editor>
|
||||
</query-part-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<div class="tight-form-func-controls">
|
||||
<span class="pointer fa fa-remove" ng-click="removeActionInternal()" ></span>
|
||||
</div>
|
||||
|
||||
<a ng-click="toggleControls()" class="query-part-name">{{part.def.type}}</a><span>(</span><span class="query-part-parameters"></span><span>)</span>
|
||||
@@ -1,8 +1,5 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import './query_part_editor';
|
||||
import './query_part_editor';
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import InfluxQueryBuilder from './query_builder';
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
QueryPartDef,
|
||||
QueryPart,
|
||||
functionRenderer,
|
||||
suffixRenderer,
|
||||
identityRenderer,
|
||||
quotedIdentityRenderer,
|
||||
} from 'app/core/components/query_part/query_part';
|
||||
|
||||
var index = [];
|
||||
var categories = {
|
||||
@@ -12,71 +20,26 @@ var categories = {
|
||||
Fields: [],
|
||||
};
|
||||
|
||||
function createPart(part): any {
|
||||
var def = index[part.type];
|
||||
if (!def) {
|
||||
throw {message: 'Could not find query part ' + part.type};
|
||||
}
|
||||
|
||||
return new QueryPart(part, def);
|
||||
};
|
||||
|
||||
function register(options: any) {
|
||||
index[options.type] = new QueryPartDef(options);
|
||||
options.category.push(index[options.type]);
|
||||
}
|
||||
|
||||
var groupByTimeFunctions = [];
|
||||
|
||||
class QueryPartDef {
|
||||
type: string;
|
||||
params: any[];
|
||||
defaultParams: any[];
|
||||
renderer: any;
|
||||
category: any;
|
||||
addStrategy: any;
|
||||
|
||||
constructor(options: any) {
|
||||
this.type = options.type;
|
||||
this.params = options.params;
|
||||
this.defaultParams = options.defaultParams;
|
||||
this.renderer = options.renderer;
|
||||
this.category = options.category;
|
||||
this.addStrategy = options.addStrategy;
|
||||
}
|
||||
|
||||
static register(options: any) {
|
||||
index[options.type] = new QueryPartDef(options);
|
||||
options.category.push(index[options.type]);
|
||||
}
|
||||
}
|
||||
|
||||
function functionRenderer(part, innerExpr) {
|
||||
var str = part.def.type + '(';
|
||||
var parameters = _.map(part.params, (value, index) => {
|
||||
var paramType = part.def.params[index];
|
||||
if (paramType.type === 'time') {
|
||||
if (value === 'auto') {
|
||||
value = '$interval';
|
||||
}
|
||||
}
|
||||
if (paramType.quote === 'single') {
|
||||
return "'" + value + "'";
|
||||
} else if (paramType.quote === 'double') {
|
||||
return '"' + value + '"';
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
if (innerExpr) {
|
||||
parameters.unshift(innerExpr);
|
||||
}
|
||||
return str + parameters.join(', ') + ')';
|
||||
}
|
||||
|
||||
function aliasRenderer(part, innerExpr) {
|
||||
return innerExpr + ' AS ' + '"' + part.params[0] + '"';
|
||||
}
|
||||
|
||||
function suffixRenderer(part, innerExpr) {
|
||||
return innerExpr + ' ' + part.params[0];
|
||||
}
|
||||
|
||||
function identityRenderer(part, innerExpr) {
|
||||
return part.params[0];
|
||||
}
|
||||
|
||||
function quotedIdentityRenderer(part, innerExpr) {
|
||||
return '"' + part.params[0] + '"';
|
||||
}
|
||||
|
||||
function fieldRenderer(part, innerExpr) {
|
||||
if (part.params[0] === '*') {
|
||||
return '*';
|
||||
@@ -149,13 +112,13 @@ function addAliasStrategy(selectParts, partModel) {
|
||||
function addFieldStrategy(selectParts, partModel, query) {
|
||||
// copy all parts
|
||||
var parts = _.map(selectParts, function(part: any) {
|
||||
return new QueryPart({type: part.def.type, params: _.clone(part.params)});
|
||||
return createPart({type: part.def.type, params: _.clone(part.params)});
|
||||
});
|
||||
|
||||
query.selectModels.push(parts);
|
||||
}
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'field',
|
||||
addStrategy: addFieldStrategy,
|
||||
category: categories.Fields,
|
||||
@@ -165,7 +128,7 @@ QueryPartDef.register({
|
||||
});
|
||||
|
||||
// Aggregations
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'count',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Aggregations,
|
||||
@@ -174,7 +137,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'distinct',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Aggregations,
|
||||
@@ -183,7 +146,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'integral',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Aggregations,
|
||||
@@ -192,7 +155,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'mean',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Aggregations,
|
||||
@@ -201,7 +164,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'median',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Aggregations,
|
||||
@@ -210,7 +173,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'sum',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Aggregations,
|
||||
@@ -221,7 +184,7 @@ QueryPartDef.register({
|
||||
|
||||
// transformations
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'derivative',
|
||||
addStrategy: addTransformationStrategy,
|
||||
category: categories.Transformations,
|
||||
@@ -230,7 +193,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'spread',
|
||||
addStrategy: addTransformationStrategy,
|
||||
category: categories.Transformations,
|
||||
@@ -239,7 +202,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'non_negative_derivative',
|
||||
addStrategy: addTransformationStrategy,
|
||||
category: categories.Transformations,
|
||||
@@ -248,7 +211,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'difference',
|
||||
addStrategy: addTransformationStrategy,
|
||||
category: categories.Transformations,
|
||||
@@ -257,7 +220,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'moving_average',
|
||||
addStrategy: addTransformationStrategy,
|
||||
category: categories.Transformations,
|
||||
@@ -266,7 +229,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'stddev',
|
||||
addStrategy: addTransformationStrategy,
|
||||
category: categories.Transformations,
|
||||
@@ -275,7 +238,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'time',
|
||||
category: groupByTimeFunctions,
|
||||
params: [{ name: "interval", type: "time", options: ['auto', '1s', '10s', '1m', '5m', '10m', '15m', '1h'] }],
|
||||
@@ -283,7 +246,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'fill',
|
||||
category: groupByTimeFunctions,
|
||||
params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }],
|
||||
@@ -292,7 +255,7 @@ QueryPartDef.register({
|
||||
});
|
||||
|
||||
// Selectors
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'bottom',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Selectors,
|
||||
@@ -301,7 +264,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'first',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Selectors,
|
||||
@@ -310,7 +273,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'last',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Selectors,
|
||||
@@ -319,7 +282,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'max',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Selectors,
|
||||
@@ -328,7 +291,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'min',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Selectors,
|
||||
@@ -337,7 +300,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'percentile',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Selectors,
|
||||
@@ -346,7 +309,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'top',
|
||||
addStrategy: replaceAggregationAddStrategy,
|
||||
category: categories.Selectors,
|
||||
@@ -355,7 +318,7 @@ QueryPartDef.register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'tag',
|
||||
category: groupByTimeFunctions,
|
||||
params: [{name: 'tag', type: 'string', dynamicLookup: true}],
|
||||
@@ -363,7 +326,7 @@ QueryPartDef.register({
|
||||
renderer: fieldRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'math',
|
||||
addStrategy: addMathStrategy,
|
||||
category: categories.Math,
|
||||
@@ -372,7 +335,7 @@ QueryPartDef.register({
|
||||
renderer: suffixRenderer,
|
||||
});
|
||||
|
||||
QueryPartDef.register({
|
||||
register({
|
||||
type: 'alias',
|
||||
addStrategy: addAliasStrategy,
|
||||
category: categories.Aliasing,
|
||||
@@ -382,74 +345,9 @@ QueryPartDef.register({
|
||||
renderer: aliasRenderer,
|
||||
});
|
||||
|
||||
class QueryPart {
|
||||
part: any;
|
||||
def: QueryPartDef;
|
||||
params: any[];
|
||||
text: string;
|
||||
|
||||
constructor(part: any) {
|
||||
this.part = part;
|
||||
this.def = index[part.type];
|
||||
if (!this.def) {
|
||||
throw {message: 'Could not find query part ' + part.type};
|
||||
}
|
||||
|
||||
part.params = part.params || _.clone(this.def.defaultParams);
|
||||
this.params = part.params;
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
render(innerExpr: string) {
|
||||
return this.def.renderer(this, innerExpr);
|
||||
}
|
||||
|
||||
hasMultipleParamsInString (strValue, index) {
|
||||
if (strValue.indexOf(',') === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.def.params[index + 1] && this.def.params[index + 1].optional;
|
||||
}
|
||||
|
||||
updateParam (strValue, index) {
|
||||
// handle optional parameters
|
||||
// if string contains ',' and next param is optional, split and update both
|
||||
if (this.hasMultipleParamsInString(strValue, index)) {
|
||||
_.each(strValue.split(','), function(partVal: string, idx) {
|
||||
this.updateParam(partVal.trim(), idx);
|
||||
}, this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strValue === '' && this.def.params[index].optional) {
|
||||
this.params.splice(index, 1);
|
||||
} else {
|
||||
this.params[index] = strValue;
|
||||
}
|
||||
|
||||
this.part.params = this.params;
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
updateText() {
|
||||
if (this.params.length === 0) {
|
||||
this.text = this.def.type + '()';
|
||||
return;
|
||||
}
|
||||
|
||||
var text = this.def.type + '(';
|
||||
text += this.params.join(', ');
|
||||
text += ')';
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
create: function(part): any {
|
||||
return new QueryPart(part);
|
||||
},
|
||||
|
||||
create: createPart,
|
||||
getCategories: function() {
|
||||
return categories;
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('influxQueryPartEditor', function($compile, templateSrv) {
|
||||
|
||||
var paramTemplate = '<input type="text" style="display:none"' +
|
||||
' class="input-mini tight-form-func-param"></input>';
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/plugins/datasource/influxdb/partials/query_part.html',
|
||||
scope: {
|
||||
part: "=",
|
||||
removeAction: "&",
|
||||
partUpdated: "&",
|
||||
getOptions: "&",
|
||||
},
|
||||
link: function postLink($scope, elem) {
|
||||
var part = $scope.part;
|
||||
var partDef = part.def;
|
||||
var $paramsContainer = elem.find('.query-part-parameters');
|
||||
var $controlsContainer = elem.find('.tight-form-func-controls');
|
||||
|
||||
function clickFuncParam(paramIndex) {
|
||||
/*jshint validthis:true */
|
||||
var $link = $(this);
|
||||
var $input = $link.next();
|
||||
|
||||
$input.val(part.params[paramIndex]);
|
||||
$input.css('width', ($link.width() + 16) + 'px');
|
||||
|
||||
$link.hide();
|
||||
$input.show();
|
||||
$input.focus();
|
||||
$input.select();
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
if (typeahead) {
|
||||
$input.val('');
|
||||
typeahead.lookup();
|
||||
}
|
||||
}
|
||||
|
||||
function inputBlur(paramIndex) {
|
||||
/*jshint validthis:true */
|
||||
var $input = $(this);
|
||||
var $link = $input.prev();
|
||||
var newValue = $input.val();
|
||||
|
||||
if (newValue !== '' || part.def.params[paramIndex].optional) {
|
||||
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
|
||||
|
||||
part.updateParam($input.val(), paramIndex);
|
||||
$scope.$apply($scope.partUpdated);
|
||||
}
|
||||
|
||||
$input.hide();
|
||||
$link.show();
|
||||
}
|
||||
|
||||
function inputKeyPress(paramIndex, e) {
|
||||
/*jshint validthis:true */
|
||||
if(e.which === 13) {
|
||||
inputBlur.call(this, paramIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function inputKeyDown() {
|
||||
/*jshint validthis:true */
|
||||
this.style.width = (3 + this.value.length) * 8 + 'px';
|
||||
}
|
||||
|
||||
function addTypeahead($input, param, paramIndex) {
|
||||
if (!param.options && !param.dynamicLookup) {
|
||||
return;
|
||||
}
|
||||
|
||||
var typeaheadSource = function (query, callback) {
|
||||
if (param.options) { return param.options; }
|
||||
|
||||
$scope.$apply(function() {
|
||||
$scope.getOptions().then(function(result) {
|
||||
var dynamicOptions = _.map(result, function(op) { return op.value; });
|
||||
callback(dynamicOptions);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
var options = param.options;
|
||||
if (param.type === 'int') {
|
||||
options = _.map(options, function(val) { return val.toString(); });
|
||||
}
|
||||
|
||||
$input.typeahead({
|
||||
source: typeaheadSource,
|
||||
minLength: 0,
|
||||
items: 1000,
|
||||
updater: function (value) {
|
||||
setTimeout(function() {
|
||||
inputBlur.call($input[0], paramIndex);
|
||||
}, 0);
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
typeahead.lookup = function () {
|
||||
this.query = this.$element.val() || '';
|
||||
var items = this.source(this.query, $.proxy(this.process, this));
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
}
|
||||
|
||||
$scope.toggleControls = function() {
|
||||
var targetDiv = elem.closest('.tight-form');
|
||||
|
||||
if (elem.hasClass('show-function-controls')) {
|
||||
elem.removeClass('show-function-controls');
|
||||
targetDiv.removeClass('has-open-function');
|
||||
$controlsContainer.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
elem.addClass('show-function-controls');
|
||||
targetDiv.addClass('has-open-function');
|
||||
$controlsContainer.show();
|
||||
};
|
||||
|
||||
$scope.removeActionInternal = function() {
|
||||
$scope.toggleControls();
|
||||
$scope.removeAction();
|
||||
};
|
||||
|
||||
function addElementsAndCompile() {
|
||||
_.each(partDef.params, function(param, index) {
|
||||
if (param.optional && part.params.length <= index) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
$('<span>, </span>').appendTo($paramsContainer);
|
||||
}
|
||||
|
||||
var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
|
||||
var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
|
||||
var $input = $(paramTemplate);
|
||||
|
||||
$paramLink.appendTo($paramsContainer);
|
||||
$input.appendTo($paramsContainer);
|
||||
|
||||
$input.blur(_.partial(inputBlur, index));
|
||||
$input.keyup(inputKeyDown);
|
||||
$input.keypress(_.partial(inputKeyPress, index));
|
||||
$paramLink.click(_.partial(clickFuncParam, index));
|
||||
|
||||
addTypeahead($input, param, index);
|
||||
});
|
||||
}
|
||||
|
||||
function relink() {
|
||||
$paramsContainer.empty();
|
||||
addElementsAndCompile();
|
||||
}
|
||||
|
||||
relink();
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -403,10 +403,7 @@ function (angular, _, dateMath) {
|
||||
} else {
|
||||
return _.findIndex(options.targets, function(target) {
|
||||
if (target.filters && target.filters.length > 0) {
|
||||
return target.metric === metricData.metric &&
|
||||
_.all(target.filters, function(filter) {
|
||||
return filter.tagk === interpolatedTagValue === "*";
|
||||
});
|
||||
return target.metric === metricData.metric;
|
||||
} else {
|
||||
return target.metric === metricData.metric &&
|
||||
_.all(target.tags, function(tagV, tagK) {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import 'jquery.flot';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class AlertHandleManager {
|
||||
plot: any;
|
||||
placeholder: any;
|
||||
height: any;
|
||||
alert: any;
|
||||
|
||||
constructor(private panelCtrl) {
|
||||
this.alert = panelCtrl.panel.alert;
|
||||
}
|
||||
|
||||
getHandleInnerHtml(type, op, value) {
|
||||
if (op === '>') { op = '>'; }
|
||||
if (op === '<') { op = '<'; }
|
||||
|
||||
return `
|
||||
<div class="alert-handle-line">
|
||||
</div>
|
||||
<div class="alert-handle">
|
||||
<i class="icon-gf icon-gf-${type} alert-icon-${type}"></i>
|
||||
${op} ${value}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
getFullHandleHtml(type, op, value) {
|
||||
var innerTemplate = this.getHandleInnerHtml(type, op, value);
|
||||
return `
|
||||
<div class="alert-handle-wrapper alert-handle-wrapper--${type}">
|
||||
${innerTemplate}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupDragging(handleElem, levelModel) {
|
||||
var isMoving = false;
|
||||
var lastY = null;
|
||||
var posTop;
|
||||
var plot = this.plot;
|
||||
var panelCtrl = this.panelCtrl;
|
||||
|
||||
function dragging(evt) {
|
||||
if (lastY === null) {
|
||||
lastY = evt.clientY;
|
||||
} else {
|
||||
var diff = evt.clientY - lastY;
|
||||
posTop = posTop + diff;
|
||||
lastY = evt.clientY;
|
||||
handleElem.css({top: posTop + diff});
|
||||
}
|
||||
}
|
||||
|
||||
function stopped() {
|
||||
isMoving = false;
|
||||
// calculate graph level
|
||||
var graphLevel = plot.c2p({left: 0, top: posTop}).y;
|
||||
console.log('canvasPos:' + posTop + ' Graph level: ' + graphLevel);
|
||||
graphLevel = parseInt(graphLevel.toFixed(0));
|
||||
levelModel.level = graphLevel;
|
||||
console.log(levelModel);
|
||||
|
||||
var levelCanvasPos = plot.p2c({x: 0, y: graphLevel});
|
||||
console.log('canvas pos', levelCanvasPos);
|
||||
|
||||
console.log('stopped');
|
||||
handleElem.off("mousemove", dragging);
|
||||
handleElem.off("mouseup", dragging);
|
||||
|
||||
// trigger digest and render
|
||||
panelCtrl.$scope.$apply(function() {
|
||||
panelCtrl.render();
|
||||
});
|
||||
}
|
||||
|
||||
handleElem.bind('mousedown', function() {
|
||||
isMoving = true;
|
||||
lastY = null;
|
||||
posTop = handleElem.position().top;
|
||||
console.log('start pos', posTop);
|
||||
|
||||
handleElem.on("mousemove", dragging);
|
||||
handleElem.on("mouseup", stopped);
|
||||
});
|
||||
}
|
||||
|
||||
cleanUp() {
|
||||
if (this.placeholder) {
|
||||
this.placeholder.find(".alert-handle-wrapper").remove();
|
||||
}
|
||||
}
|
||||
|
||||
renderHandle(type, model, defaultHandleTopPos) {
|
||||
var handleElem = this.placeholder.find(`.alert-handle-wrapper--${type}`);
|
||||
var level = model.level;
|
||||
var levelStr = level;
|
||||
var handleTopPos = 0;
|
||||
|
||||
// handle no value
|
||||
if (!_.isNumber(level)) {
|
||||
levelStr = '';
|
||||
handleTopPos = defaultHandleTopPos;
|
||||
} else {
|
||||
var levelCanvasPos = this.plot.p2c({x: 0, y: level});
|
||||
handleTopPos = Math.min(Math.max(levelCanvasPos.top, 0), this.height) - 6;
|
||||
}
|
||||
|
||||
if (handleElem.length === 0) {
|
||||
console.log('creating handle');
|
||||
handleElem = $(this.getFullHandleHtml(type, model.op, levelStr));
|
||||
this.placeholder.append(handleElem);
|
||||
this.setupDragging(handleElem, model);
|
||||
} else {
|
||||
console.log('reusing handle!');
|
||||
handleElem.html(this.getHandleInnerHtml(type, model.op, levelStr));
|
||||
}
|
||||
|
||||
handleElem.toggleClass('alert-handle-wrapper--no-value', levelStr === '');
|
||||
handleElem.css({top: handleTopPos});
|
||||
}
|
||||
|
||||
draw(plot) {
|
||||
this.plot = plot;
|
||||
this.placeholder = plot.getPlaceholder();
|
||||
this.height = plot.height();
|
||||
|
||||
this.renderHandle('critical', this.alert.critical, 10);
|
||||
this.renderHandle('warn', this.alert.warn, this.height-30);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,78 +1,171 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import angular from 'angular';
|
||||
|
||||
import {
|
||||
QueryPartDef,
|
||||
QueryPart,
|
||||
} from 'app/core/components/query_part/query_part';
|
||||
|
||||
var alertQueryDef = new QueryPartDef({
|
||||
type: 'query',
|
||||
params: [
|
||||
{name: "queryRefId", type: 'string', options: ['#A', '#B', '#C', '#D']},
|
||||
{name: "from", type: "string", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']},
|
||||
{name: "to", type: "string", options: ['now']},
|
||||
],
|
||||
defaultParams: ['#A', '5m', 'now', 'avg']
|
||||
});
|
||||
|
||||
export class AlertTabCtrl {
|
||||
panel: any;
|
||||
panelCtrl: any;
|
||||
alerting: any;
|
||||
metricTargets = [{ refId: '- select query -' } ];
|
||||
operators = ['>', '<', '<=', '>='];
|
||||
aggregators = ['avg', 'sum', 'min', 'max', 'median'];
|
||||
schedulers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}];
|
||||
transforms = [
|
||||
{
|
||||
text: 'Aggregation',
|
||||
type: 'aggregation',
|
||||
},
|
||||
{
|
||||
text: 'Linear Forecast',
|
||||
type: 'forecast',
|
||||
},
|
||||
];
|
||||
aggregators = ['avg', 'sum', 'min', 'max', 'last'];
|
||||
alert: any;
|
||||
query: any;
|
||||
queryParams: any;
|
||||
transformDef: any;
|
||||
levelOpList = [
|
||||
{text: '>', value: '>'},
|
||||
{text: '<', value: '<'},
|
||||
{text: '=', value: '='},
|
||||
];
|
||||
|
||||
defaultValues = {
|
||||
aggregator: 'avg',
|
||||
frequency: 10,
|
||||
queryRange: 3600,
|
||||
warnOperator: '>',
|
||||
critOperator: '>',
|
||||
queryRef: '- select query -'
|
||||
frequency: '60s',
|
||||
notify: [],
|
||||
enabled: false,
|
||||
scheduler: 1,
|
||||
warn: { op: '>', level: undefined },
|
||||
critical: { op: '>', level: undefined },
|
||||
query: {
|
||||
refId: 'A',
|
||||
from: '5m',
|
||||
to: 'now',
|
||||
},
|
||||
transform: {
|
||||
type: 'aggregation',
|
||||
method: 'avg'
|
||||
}
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, private $timeout) {
|
||||
$scope.alertTab = this; //HACK ATTACK!
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
this.panel = this.panelCtrl.panel;
|
||||
$scope.ctrl = this;
|
||||
|
||||
_.defaults(this.panel.alerting, this.defaultValues);
|
||||
this.metricTargets = this.panel.targets.map(val => val);
|
||||
this.initAlertModel();
|
||||
|
||||
// set panel alert edit mode
|
||||
$scope.$on("$destroy", () => {
|
||||
this.panelCtrl.editingAlert = false;
|
||||
this.panelCtrl.render();
|
||||
});
|
||||
}
|
||||
|
||||
initAlertModel() {
|
||||
if (!this.panel.alert) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.alert = this.panel.alert;
|
||||
|
||||
// set defaults
|
||||
_.defaults(this.alert, this.defaultValues);
|
||||
|
||||
var defaultName = (this.panelCtrl.dashboard.title + ' ' + this.panel.title + ' alert');
|
||||
this.panel.alerting.name = this.panel.alerting.name || defaultName;
|
||||
this.alert.name = this.alert.name || defaultName;
|
||||
this.alert.description = this.alert.description || defaultName;
|
||||
|
||||
this.panel.targets.map(target => {
|
||||
this.metricTargets.push(target);
|
||||
});
|
||||
this.panel.alerting.queryRef = this.panel.alerting.queryRef || this.metricTargets[0].refId;
|
||||
// great temp working model
|
||||
this.queryParams = {
|
||||
params: [
|
||||
this.alert.query.refId,
|
||||
this.alert.query.from,
|
||||
this.alert.query.to
|
||||
]
|
||||
};
|
||||
|
||||
// init the query part components model
|
||||
this.query = new QueryPart(this.queryParams, alertQueryDef);
|
||||
this.convertThresholdsToAlertThresholds();
|
||||
this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type});
|
||||
|
||||
this.panelCtrl.editingAlert = true;
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
queryUpdated() {
|
||||
this.alert.query = {
|
||||
refId: this.query.params[0],
|
||||
from: this.query.params[1],
|
||||
to: this.query.params[2],
|
||||
};
|
||||
}
|
||||
|
||||
transformChanged() {
|
||||
// clear model
|
||||
this.alert.transform = {type: this.alert.transform.type};
|
||||
this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type});
|
||||
|
||||
switch (this.alert.transform.type) {
|
||||
case 'aggregation': {
|
||||
this.alert.transform.method = 'avg';
|
||||
break;
|
||||
}
|
||||
case "forecast": {
|
||||
this.alert.transform.timespan = '7d';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
convertThresholdsToAlertThresholds() {
|
||||
if (this.panel.grid
|
||||
&& this.panel.grid.threshold1
|
||||
&& this.panel.alerting.warnLevel === undefined
|
||||
) {
|
||||
this.panel.alerting.warnOperator = '>';
|
||||
this.panel.alerting.warnLevel = this.panel.grid.threshold1;
|
||||
}
|
||||
|
||||
if (this.panel.grid
|
||||
&& this.panel.grid.threshold2
|
||||
&& this.panel.alerting.critLevel === undefined
|
||||
) {
|
||||
this.panel.alerting.critOperator = '>';
|
||||
this.panel.alerting.critLevel = this.panel.grid.threshold2;
|
||||
}
|
||||
// if (this.panel.grid
|
||||
// && this.panel.grid.threshold1
|
||||
// && this.alert.warnLevel === undefined
|
||||
// ) {
|
||||
// this.alert.warning.op = '>';
|
||||
// this.alert.warning.level = this.panel.grid.threshold1;
|
||||
// }
|
||||
//
|
||||
// if (this.panel.grid
|
||||
// && this.panel.grid.threshold2
|
||||
// && this.alert.critical.level === undefined
|
||||
// ) {
|
||||
// this.alert.critical.op = '>';
|
||||
// this.alert.critical.level = this.panel.grid.threshold2;
|
||||
// }
|
||||
}
|
||||
|
||||
markAsDeleted() {
|
||||
if (this.panel.alerting) {
|
||||
this.panel.alerting = this.defaultValues;
|
||||
}
|
||||
delete() {
|
||||
delete this.panel.alert;
|
||||
this.panelCtrl.editingAlert = false;
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
thresholdsUpdated() {
|
||||
if (this.panel.alerting.warnLevel) {
|
||||
this.panel.grid.threshold1 = parseInt(this.panel.alerting.warnLevel);
|
||||
}
|
||||
|
||||
if (this.panel.alerting.critLevel) {
|
||||
this.panel.grid.threshold2 = parseInt(this.panel.alerting.critLevel);
|
||||
}
|
||||
enable() {
|
||||
this.panel.alert = {};
|
||||
this.initAlertModel();
|
||||
}
|
||||
|
||||
levelsUpdated() {
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ define([
|
||||
'lodash',
|
||||
'app/core/utils/kbn',
|
||||
'./graph_tooltip',
|
||||
'./alert_handle',
|
||||
'jquery.flot',
|
||||
'jquery.flot.selection',
|
||||
'jquery.flot.time',
|
||||
@@ -14,13 +15,16 @@ define([
|
||||
'jquery.flot.crosshair',
|
||||
'./jquery.flot.events',
|
||||
],
|
||||
function (angular, $, moment, _, kbn, GraphTooltip) {
|
||||
function (angular, $, moment, _, kbn, GraphTooltip, AlertHandle) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.directives');
|
||||
var labelWidthCache = {};
|
||||
var panelWidthCache = {};
|
||||
|
||||
// systemjs export
|
||||
var AlertHandleManager = AlertHandle.AlertHandleManager;
|
||||
|
||||
module.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -34,6 +38,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
|
||||
var legendSideLastValue = null;
|
||||
var rootScope = scope.$root;
|
||||
var panelWidth = 0;
|
||||
var alertHandles;
|
||||
|
||||
rootScope.onAppEvent('setCrosshair', function(event, info) {
|
||||
// do not need to to this if event is from this panel
|
||||
@@ -161,6 +166,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
|
||||
|
||||
rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
|
||||
}
|
||||
|
||||
if (alertHandles) {
|
||||
alertHandles.draw(plot);
|
||||
}
|
||||
}
|
||||
|
||||
function processOffsetHook(plot, gridMargin) {
|
||||
@@ -181,6 +190,18 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
// give space to alert editing
|
||||
if (ctrl.editingAlert) {
|
||||
if (!alertHandles) {
|
||||
elem.css('margin-right', '220px');
|
||||
alertHandles = new AlertHandleManager(ctrl);
|
||||
}
|
||||
} else if (alertHandles) {
|
||||
elem.css('margin-right', '0');
|
||||
alertHandles.cleanUp();
|
||||
alertHandles = null;
|
||||
}
|
||||
|
||||
var stack = panel.stack ? true : null;
|
||||
|
||||
// Populate element
|
||||
@@ -259,6 +280,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
|
||||
|
||||
function callPlot(incrementRenderCounter) {
|
||||
try {
|
||||
console.log('rendering');
|
||||
$.plot(elem, sortedSeries, options);
|
||||
} catch (e) {
|
||||
console.log('flotcharts error', e);
|
||||
@@ -311,6 +333,50 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
|
||||
}
|
||||
|
||||
function addGridThresholds(options, panel) {
|
||||
if (panel.alert) {
|
||||
var crit = panel.alert.critical;
|
||||
var warn = panel.alert.warn;
|
||||
var critEdge = Infinity;
|
||||
var warnEdge = crit.level;
|
||||
|
||||
if (_.isNumber(crit.level)) {
|
||||
if (crit.op === '<') {
|
||||
critEdge = -Infinity;
|
||||
}
|
||||
|
||||
// fill
|
||||
options.grid.markings.push({
|
||||
yaxis: {from: crit.level, to: critEdge},
|
||||
color: 'rgba(234, 112, 112, 0.10)',
|
||||
});
|
||||
|
||||
// line
|
||||
options.grid.markings.push({
|
||||
yaxis: {from: crit.level, to: crit.level},
|
||||
color: '#ed2e18'
|
||||
});
|
||||
}
|
||||
|
||||
if (_.isNumber(warn.level)) {
|
||||
// if (warn.op === '<') {
|
||||
// }
|
||||
|
||||
// fill
|
||||
options.grid.markings.push({
|
||||
yaxis: {from: warn.level, to: warnEdge},
|
||||
color: 'rgba(216, 200, 27, 0.10)',
|
||||
});
|
||||
|
||||
// line
|
||||
options.grid.markings.push({
|
||||
yaxis: {from: warn.level, to: warn.level},
|
||||
color: '#F79520'
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isNumber(panel.grid.threshold1)) {
|
||||
var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null);
|
||||
options.grid.markings.push({
|
||||
|
||||
@@ -1,63 +1,99 @@
|
||||
<div class="editor-row">
|
||||
<div class="gf-form-group section" >
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<div class="gf-form" style="margin-bottom: 2rem">
|
||||
<span class="gf-form-label width-9">Query to watch</span>
|
||||
<div class="gf-form-select-wrapper max-width-12">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.panel.alerting.queryRef"
|
||||
ng-options="target.refId as target.refId for target in alertTab.metricTargets"></select>
|
||||
|
||||
<div ng-if="ctrl.panel.alert">
|
||||
<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>
|
||||
|
||||
<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-select-wrapper max-width-10">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.alerting.warnOperator" ng-options="oper as oper for oper in alertTab.operators"></select>
|
||||
<div class="gf-form-group section">
|
||||
<h5 class="section-heading">Levels</h5>
|
||||
<div class="gf-form-inline">
|
||||
<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.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.warn.level" ng-change="ctrl.levelsUpdated()"></input>
|
||||
</div>
|
||||
<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.critical.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.critical.level" ng-change="ctrl.levelsUpdated()"></input>
|
||||
</div>
|
||||
</div>
|
||||
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.panel.alerting.warnLevel" ng-change="alertTab.thresholdsUpdated()"></input>
|
||||
</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-select-wrapper max-width-10">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.alerting.critOperator" ng-options="oper as oper for oper in alertTab.operators"></select>
|
||||
</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">Scheduler</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.alert.scheduler"
|
||||
ng-options="f.value as f.text for f in ctrl.schedulers">
|
||||
</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>
|
||||
<bootstrap-tagsinput ng-model="ctrl.alert.notify" tagclass="label label-tag" placeholder="add tags">
|
||||
</bootstrap-tagsinput>
|
||||
</div>
|
||||
</div>
|
||||
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.panel.alerting.critLevel" ng-change="alertTab.thresholdsUpdated()"></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group section">
|
||||
<h5 class="section-heading">Aggregation settings</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-12">Aggregation method</span>
|
||||
<div class="gf-form-select-wrapper max-width-10">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.panel.alerting.aggregator"
|
||||
ng-options="oper as oper for oper in alertTab.aggregators"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-12">Query range (seconds)</span>
|
||||
<input class="gf-form-input max-width-10" type="number"
|
||||
ng-model="ctrl.panel.alerting.queryRange" placeholder="3600"></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-12">Frequency (seconds)</span>
|
||||
<input class="gf-form-input max-width-10" type="number"
|
||||
ng-model="ctrl.panel.alerting.frequency" placeholder="60"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group section">
|
||||
<h5 class="section-heading">Alert info</h5>
|
||||
<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.panel.alerting.name">
|
||||
@@ -72,8 +108,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-warning" ng-click="alertTab.markAsDeleted()">Delete Alert</button>
|
||||
<button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.panel.alert">Delete</button>
|
||||
<button class="btn btn-inverse" ng-click="ctrl.enable()" ng-hide="ctrl.panel.alert">
|
||||
<i class="icon-gf icon-gf-alert"></i>
|
||||
Add Alert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ $brand-text-highlight: #f7941d;
|
||||
// Status colors
|
||||
// -------------------------
|
||||
$online: #10a345;
|
||||
$warn: #ffc03c;
|
||||
$warn: #F79520;
|
||||
$critical: #ed2e18;
|
||||
|
||||
// Scaffolding
|
||||
|
||||
@@ -315,3 +315,67 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alert-handle-wrapper {
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
|
||||
.alert-handle {
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
float: right;
|
||||
padding: 0.4rem 0.6rem 0.4rem 0.4rem;
|
||||
background-color: $btn-inverse-bg;
|
||||
box-shadow: $search-shadow;
|
||||
cursor: row-resize;
|
||||
width: 100px;
|
||||
font-size: $font-size-sm;
|
||||
box-shadow: 4px 4px 3px 0px $body-bg;
|
||||
border-radius: 4px;
|
||||
border-width: 0 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: $black;
|
||||
text-align: right;
|
||||
color: $text-muted;
|
||||
|
||||
.icon-gf {
|
||||
font-size: 17px;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-handle-line {
|
||||
float: left;
|
||||
height: 2px;
|
||||
margin-top: 13px;
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--warn {
|
||||
right: -222px;
|
||||
width: 238px;
|
||||
|
||||
.alert-handle-line {
|
||||
width: 138px;
|
||||
background-color: $warn;
|
||||
}
|
||||
}
|
||||
|
||||
&--critical {
|
||||
right: -105px;
|
||||
width: 123px;
|
||||
|
||||
.alert-handle-line {
|
||||
width: 23px;
|
||||
background-color: $critical;
|
||||
}
|
||||
}
|
||||
|
||||
&--no-value {
|
||||
.alert-handle-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
background-color: $input-bg;
|
||||
|
||||
input {
|
||||
display: inline-block;
|
||||
border: none;
|
||||
border-right: 1px solid $tight-form-border;
|
||||
margin: 0px;
|
||||
|
||||
@@ -197,12 +197,6 @@ div.flot-text {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.panel-fullscreen {
|
||||
.panel-title-container {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-full-edit {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
Vendored
+1
-1
@@ -1322,7 +1322,7 @@ Licensed under the MIT license.
|
||||
|
||||
placeholder.css("padding", 0) // padding messes up the positioning
|
||||
.children().filter(function(){
|
||||
return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base');
|
||||
return $(this).hasClass("flot-text");
|
||||
}).remove();
|
||||
|
||||
if (placeholder.css("position") == 'static')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@
|
||||
this.inputSize = Math.max(1, this.placeholderText.length);
|
||||
|
||||
this.$container = $('<div class="bootstrap-tagsinput"></div>');
|
||||
this.$input = $('<input class="tight-form-input" size="' +
|
||||
this.$input = $('<input class="gf-form-input" size="' +
|
||||
this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
|
||||
|
||||
this.$element.after(this.$container);
|
||||
|
||||
Reference in New Issue
Block a user