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