diff --git a/.floo b/.floo index a8fb87025ec..1201c5e93b9 100644 --- a/.floo +++ b/.floo @@ -1,4 +1,3 @@ { - "url": "https://floobits.com/raintank/grafana" -} - + "url": "https://floobits.com/raintank/grafana" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b25f025d1d..eb1cb5c55ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/alerting_model.json b/alerting_model.json new file mode 100644 index 00000000000..20c6cfb2fcd --- /dev/null +++ b/alerting_model.json @@ -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, + }, +} diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 297a4b5fe59..3f2c38486a4 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -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, }) } diff --git a/pkg/api/common.go b/pkg/api/common.go index f8740201143..9d3ad90783b 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -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 { diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 7b5c1f9f764..3ca7a406832 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -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 } diff --git a/pkg/log/log.go b/pkg/log/log.go index f74511e4f45..a515f5c1062 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -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() +} diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index c39ae2b62b2..c6405ef80f9 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -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()) + } } } } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 3ca6f156341..5d52c68722e 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -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() } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index cb37a809212..f8e4aa374e8 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -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 }) diff --git a/pkg/middleware/quota.go b/pkg/middleware/quota.go index d9e68f7560a..23f98e78a7e 100644 --- a/pkg/middleware/quota.go +++ b/pkg/middleware/quota.go @@ -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 diff --git a/pkg/models/alert.go b/pkg/models/alert.go new file mode 100644 index 00000000000..034efbe9ca7 --- /dev/null +++ b/pkg/models/alert.go @@ -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 +} diff --git a/pkg/models/alerts_state.go b/pkg/models/alert_state.go similarity index 97% rename from pkg/models/alerts_state.go rename to pkg/models/alert_state.go index 171eb754412..30d442ed625 100644 --- a/pkg/models/alerts_state.go +++ b/pkg/models/alert_state.go @@ -31,7 +31,7 @@ type UpdateAlertStateCommand struct { NewState string `json:"newState" binding:"Required"` Info string `json:"info"` - Result *AlertRule + Result *Alert } // Queries diff --git a/pkg/models/alert_test.go b/pkg/models/alert_test.go new file mode 100644 index 00000000000..19766640629 --- /dev/null +++ b/pkg/models/alert_test.go @@ -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) + }) + }) +} diff --git a/pkg/models/alerts.go b/pkg/models/alerts.go deleted file mode 100644 index 030aabea1b9..00000000000 --- a/pkg/models/alerts.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go new file mode 100644 index 00000000000..26d8fab092a --- /dev/null +++ b/pkg/services/alerting/alert_rule.go @@ -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 +} diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go new file mode 100644 index 00000000000..4e269aca695 --- /dev/null +++ b/pkg/services/alerting/commands.go @@ -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 +} diff --git a/pkg/services/alerting/dashboard_parser.go b/pkg/services/alerting/dashboard_parser.go deleted file mode 100644 index 66a719373db..00000000000 --- a/pkg/services/alerting/dashboard_parser.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 11eee8d8302..862e6993e44 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -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 { diff --git a/pkg/services/alerting/evaluator.go b/pkg/services/alerting/evaluator.go new file mode 100644 index 00000000000..efa7231b435 --- /dev/null +++ b/pkg/services/alerting/evaluator.go @@ -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 }, +} diff --git a/pkg/services/alerting/executor.go b/pkg/services/alerting/executor.go index 08a2125bc4f..3ab3e26cb70 100644 --- a/pkg/services/alerting/executor.go +++ b/pkg/services/alerting/executor.go @@ -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, } } } diff --git a/pkg/services/alerting/executor_test.go b/pkg/services/alerting/executor_test.go index 2ccbf18ad74..753f5dd244c 100644 --- a/pkg/services/alerting/executor_test.go +++ b/pkg/services/alerting/executor_test.go @@ -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}}), diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go new file mode 100644 index 00000000000..ae360973fad --- /dev/null +++ b/pkg/services/alerting/extractor.go @@ -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 +} diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go new file mode 100644 index 00000000000..7cab1d94c7c --- /dev/null +++ b/pkg/services/alerting/extractor_test.go @@ -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") + }) + }) + }) + }) +} diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 7b0fb616a1c..364950ee9fa 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -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 } diff --git a/pkg/services/alerting/rule_reader.go b/pkg/services/alerting/reader.go similarity index 71% rename from pkg/services/alerting/rule_reader.go rename to pkg/services/alerting/reader.go index 49b781a01c6..db7da930746 100644 --- a/pkg/services/alerting/rule_reader.go +++ b/pkg/services/alerting/reader.go @@ -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 } diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index ffa2b2b900c..c8c99e19032 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -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 } } diff --git a/pkg/services/alerting/transformers/aggregation.go b/pkg/services/alerting/transformers/aggregation.go new file mode 100644 index 00000000000..25d9c5c73f4 --- /dev/null +++ b/pkg/services/alerting/transformers/aggregation.go @@ -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") +} diff --git a/pkg/services/alerting/transformers/transformer.go b/pkg/services/alerting/transformers/transformer.go new file mode 100644 index 00000000000..d5fa9df45c9 --- /dev/null +++ b/pkg/services/alerting/transformers/transformer.go @@ -0,0 +1,7 @@ +package transformer + +import "github.com/grafana/grafana/pkg/tsdb" + +type Transformer interface { + Transform(timeserie *tsdb.TimeSeries) (float64, error) +} diff --git a/pkg/services/search/handlers.go b/pkg/services/search/handlers.go index a4905d6fa58..a8344ba05a5 100644 --- a/pkg/services/search/handlers.go +++ b/pkg/services/search/handlers.go @@ -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 { diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index 159637013f5..ada3e1ccdfa 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -42,6 +42,7 @@ type FindPersistedDashboardsQuery struct { UserId int64 IsStarred bool DashboardIds []int + Limit int Result HitList } diff --git a/pkg/services/sqlstore/alert_rule.go b/pkg/services/sqlstore/alert.go similarity index 81% rename from pkg/services/sqlstore/alert_rule.go rename to pkg/services/sqlstore/alert.go index 36f142df7bb..9d92f0ebb47 100644 --- a/pkg/services/sqlstore/alert_rule.go +++ b/pkg/services/sqlstore/alert.go @@ -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 diff --git a/pkg/services/sqlstore/alert_rule_changes.go b/pkg/services/sqlstore/alert_rule_changes.go index aa03e8e607d..cb5fe83cab0 100644 --- a/pkg/services/sqlstore/alert_rule_changes.go +++ b/pkg/services/sqlstore/alert_rule_changes.go @@ -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(), diff --git a/pkg/services/sqlstore/alert_rule_changes_test.go b/pkg/services/sqlstore/alert_rule_changes_test.go index e04d46b7fe2..0e0ca253ca6 100644 --- a/pkg/services/sqlstore/alert_rule_changes_test.go +++ b/pkg/services/sqlstore/alert_rule_changes_test.go @@ -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, }, } diff --git a/pkg/services/sqlstore/alert_rule_test.go b/pkg/services/sqlstore/alert_rule_test.go index 9a3b0088b5c..be15bad3229 100644 --- a/pkg/services/sqlstore/alert_rule_test.go +++ b/pkg/services/sqlstore/alert_rule_test.go @@ -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", }, } diff --git a/pkg/services/sqlstore/alert_state.go b/pkg/services/sqlstore/alert_state.go index d2f3d4a4265..c8aba5ab7c9 100644 --- a/pkg/services/sqlstore/alert_state.go +++ b/pkg/services/sqlstore/alert_state.go @@ -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") diff --git a/pkg/services/sqlstore/alert_state_test.go b/pkg/services/sqlstore/alert_state_test.go index 97a785eceac..2fbf652353c 100644 --- a/pkg/services/sqlstore/alert_state_test.go +++ b/pkg/services/sqlstore/alert_state_test.go @@ -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", }, } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index fbf245a951b..3fac1672dff 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -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 diff --git a/pkg/services/sqlstore/dashboard_parser_test.go b/pkg/services/sqlstore/dashboard_parser_test.go deleted file mode 100644 index 7dc508c50f2..00000000000 --- a/pkg/services/sqlstore/dashboard_parser_test.go +++ /dev/null @@ -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) - }) - }) -} diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index 4142602c472..b14c7ed9a24 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -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) } } diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index e5b5d783886..6d2dc489413 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -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", diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go index f959bf0ec8a..5bddf6ff605 100644 --- a/pkg/services/sqlstore/migrations/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -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() diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 399a87273d5..e704826bed3 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -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 { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 81b19717ddf..823a0b18421 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -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()) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index b7c09715a8d..98f4c648048 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -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) } diff --git a/public/app/core/components/query_part/query_part.ts b/public/app/core/components/query_part/query_part.ts new file mode 100644 index 00000000000..90724f65d2d --- /dev/null +++ b/public/app/core/components/query_part/query_part.ts @@ -0,0 +1,123 @@ +/// + +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] + '"'; +} + + diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts new file mode 100644 index 00000000000..f9122ee283b --- /dev/null +++ b/public/app/core/components/query_part/query_part_editor.ts @@ -0,0 +1,183 @@ +/// + +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; + +var template = ` +
+ +
+ +{{part.def.type}} +() +`; + + /** @ngInject */ +export function queryPartEditorDirective($compile, templateSrv) { + + var paramTemplate = ''; + 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) { + $(', ').appendTo($paramsContainer); + } + + var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); + var $paramLink = $('' + paramValue + ''); + 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); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index abebb5ce560..9c8ae9cdad2 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -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, }; diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 035bfb6ae6e..fe1277c59c0 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -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); } }; diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html index 811891ae317..02fa2878a17 100644 --- a/public/app/features/org/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -51,7 +51,7 @@ Current - + Select diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 0f253b5048a..bcb1980f854 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -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; diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 0d6dd5c5c3b..68b8ee60d98 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -35,13 +35,13 @@
- - +
@@ -62,12 +62,12 @@ GROUP BY - - +
diff --git a/public/app/plugins/datasource/influxdb/partials/query_part.html b/public/app/plugins/datasource/influxdb/partials/query_part.html deleted file mode 100644 index 478edfe5c29..00000000000 --- a/public/app/plugins/datasource/influxdb/partials/query_part.html +++ /dev/null @@ -1,5 +0,0 @@ -
- -
- -{{part.def.type}}() diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index 8d6a03fc4a1..69895e53c25 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -1,8 +1,5 @@ /// -import './query_part_editor'; -import './query_part_editor'; - import angular from 'angular'; import _ from 'lodash'; import InfluxQueryBuilder from './query_builder'; diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts index 130174c3084..0081481437d 100644 --- a/public/app/plugins/datasource/influxdb/query_part.ts +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -1,6 +1,14 @@ /// 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; } diff --git a/public/app/plugins/datasource/influxdb/query_part_editor.js b/public/app/plugins/datasource/influxdb/query_part_editor.js deleted file mode 100644 index 4e044eca304..00000000000 --- a/public/app/plugins/datasource/influxdb/query_part_editor.js +++ /dev/null @@ -1,178 +0,0 @@ -define([ - 'angular', - 'lodash', - 'jquery', -], -function (angular, _, $) { - 'use strict'; - - angular - .module('grafana.directives') - .directive('influxQueryPartEditor', function($compile, templateSrv) { - - var paramTemplate = ''; - 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) { - $(', ').appendTo($paramsContainer); - } - - var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); - var $paramLink = $('' + paramValue + ''); - 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(); - } - }; - - }); - -}); diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index b683aa02068..1d3db75658c 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -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) { diff --git a/public/app/plugins/panel/graph/alert_handle.ts b/public/app/plugins/panel/graph/alert_handle.ts new file mode 100644 index 00000000000..c1914196e89 --- /dev/null +++ b/public/app/plugins/panel/graph/alert_handle.ts @@ -0,0 +1,135 @@ +/// + +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 ` +
+
+
+ + ${op} ${value} +
`; + } + + getFullHandleHtml(type, op, value) { + var innerTemplate = this.getHandleInnerHtml(type, op, value); + return ` +
+ ${innerTemplate} +
+ `; + } + + 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); + } + +} + diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 11aebe7e4e7..82ac33e3d47 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -1,78 +1,171 @@ -/// + /// 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(); } } diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 3a42df657a5..599bbe554bf 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -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({ diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index aeafebf2f83..c2cce48a073 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,63 +1,99 @@ -
-
-
Query
-
- Query to watch -
- + +
+
+
+
Alert Query
+
+
+ + +
+
+ Transform using +
+ +
+
+
+ Method +
+ +
+
+
+ Timespan + +
-
Thresholds
-
- - - Warn level - -
- +
+
Levels
+
+
+ + + Warn if + + + +
+
+ + + Critcal if + + + +
-
-
- - - Critical level - -
- +
+ +
+
+
Execution
+
+
+ Scheduler +
+ +
+
+
+ Evaluate every + +
+
+
+
+
Notifications
+
+
+ Groups + + +
-
-
Aggregation settings
-
- Aggregation method -
- -
-
- -
- Query range (seconds) - -
- -
- Frequency (seconds) - -
-
-
-
Alert info
+
Information
Alert name @@ -72,8 +108,13 @@
+
- + +
diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 047e50faa73..99070cad837 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -44,7 +44,7 @@ $brand-text-highlight: #f7941d; // Status colors // ------------------------- $online: #10a345; -$warn: #ffc03c; +$warn: #F79520; $critical: #ed2e18; // Scaffolding diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index b830561f816..5c85ac4d465 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -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; + } + } +} diff --git a/public/sass/components/_tagsinput.scss b/public/sass/components/_tagsinput.scss index 8092446e2e5..698302e6f1e 100644 --- a/public/sass/components/_tagsinput.scss +++ b/public/sass/components/_tagsinput.scss @@ -7,6 +7,7 @@ background-color: $input-bg; input { + display: inline-block; border: none; border-right: 1px solid $tight-form-border; margin: 0px; diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 8fb1e6bcdaa..eec86e78234 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -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; diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 380030a77c9..e2c460ddbd0 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -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') diff --git a/public/vendor/tagsinput/bootstrap-tagsinput.js b/public/vendor/tagsinput/bootstrap-tagsinput.js index b3a3c3bc386..702b6416962 100644 --- a/public/vendor/tagsinput/bootstrap-tagsinput.js +++ b/public/vendor/tagsinput/bootstrap-tagsinput.js @@ -35,7 +35,7 @@ this.inputSize = Math.max(1, this.placeholderText.length); this.$container = $('
'); - this.$input = $('').appendTo(this.$container); this.$element.after(this.$container);