Merge branch 'alert_ui_take2' into alerting

This commit is contained in:
bergquist
2016-06-13 09:20:00 +02:00
67 changed files with 1975 additions and 1394 deletions
+2 -3
View File
@@ -1,4 +1,3 @@
{
"url": "https://floobits.com/raintank/grafana"
}
"url": "https://floobits.com/raintank/grafana"
}
+1
View File
@@ -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.
+167
View File
@@ -0,0 +1,167 @@
{
"alert": {
"name": "Majority servers down",
"frequency": 60,
"notify": ["group1", "group2"],
"expressions": [
{
"left": [
{
"type": "query",
"refId": "A",
"timeRange": {"from": "5m", "to": "now-1m"},
},
{
"type": "function",
"name": "max"
}
],
"operator": ">",
"right": [
{
"type": "constant",
"value": 100
}
],
"level": 2,
}
]
},
"alert": {
"name": "Majority servers down take2",
"frequency": 60,
"notify": ["group1", "group2"],
"expressions": [
{
"left": [
{
"type": "query",
"refId": "A",
"timeRange": {"from": "5m", "to": "now-1m"},
},
{
"type": "function",
"name": "max"
}
],
"operator": ">",
"right": [
{
"type": "query",
"refId": "A",
"timeRange": {"from": "now-1d-5m", "to": "now-1d"},
},
{
"type": "function",
"name": "max"
}
],
"level": 2,
}
]
},
"alert": {
"name": "CPU usage last 5min above 90%",
"frequency": 60,
"expressions": [
{
"expr": "query(#A, 5m, now, avg)",
"operator": ">",
"critLevel": 90,
}
]
},
"alert": {
"name": "Series count above 10",
"frequency": "1m",
"expressions": [
{
"expr": "query(#A, 5m, now, avg) | countSeries()",
"operator": ">",
"critLevel": 10,
}
]
},
"alert": {
"name": "Disk Free Zero in 3 days",
"frequency": "1d",
"expressions": [
{
"expr": "query(#A, 1d, now, trend(3d))",
"operator": ">",
"critLevel": 0,
}
]
},
"alert": {
"name": "Server requests is zero for more than 10min",
"frequency": "1d",
"expressions": [
{
"expr": "query(#A, 10m, now, sum)",
"operator": "=",
"critLevel": 0,
}
]
},
"alert": {
"name": "Timeouts should not be more than 0.1% of requests",
"frequency": "1d",
"expressions": [
{
"expr": "query(#A, 10m, now, sum) | subtract | query(#B, 10m, now, sum)",
"operator": ">",
"critLevel": 0,
}
]
},
"alert": {
"name": "CPU usage last 5min changed by more than 20% compared to last 24hours",
"frequency": "1m",
"value": "query(#A, 5m, now, avg)",
"operator": "percent change",
"threshold": "query(#A, 1d, now, avg)",
},
"alert": {
"name": "CPU higher than 90%",
"frequency": "1m",
"valueExpr": "query(#A, 5m, now, avg)",
"evalType": "greater than",
"critLevel": 20,
"warnLevel": 10,
},
"alert": {
"name": "CPU usage last 5min changed by more than 20% compared to last 24hours",
"frequency": "1m",
"expr": "query(#A, 5m, now, avg) percentGreaterThan()",
"evalType": "percentscre change",
"evalExpr": "query(#A, 1d, now, avg)",
"critLevel": 20,
"warnLevel": 10,
},
"alert": {
"name": "CPU usage last 5min changed by more than 20% compared to last 24hours",
"frequency": "1m",
"valueQuery": "query(#A, 5m, now, avg) ",
"evalType": "simple", "// other options are: percent change, trend"
"evalQuery": "query(#A, 1d, now, avg)",
"comparison": "greater than",
"critLevel": 20,
"warnLevel": 10,
},
"alert": {
"name": "CPU usage last 5min changed by more than 20% compared to last 24hours",
"frequency": "1m",
"valueQuery": "query(#A, 5m, now, avg) | Evaluate Against: Static Threshold | >200 Warn | >300 Critical",
"valueQuery": "query(#A, 5m, now, avg) | Evaluate Against: Percent Change Compared To | query(#B, 5m, now, avg) | >200 Warn | >300 Critical",
"valueQuery": "query(#A, 5m, now, trend) | Evaluate Against: Forcast | 7days | >200 Warn | >300 Critical",
"evalType": "simple", "// other options are: percent change, trend"
"evalQuery": "query(#A, 1d, now, avg)",
"comparison": "greater than",
"critLevel": 20,
"warnLevel": 10,
},
}
+1 -8
View File
@@ -43,7 +43,7 @@ func GetAlertChanges(c *middleware.Context) Response {
return Json(200, query.Result)
}
// GET /api/alerts
// GET /api/alerts/rules/
func GetAlerts(c *middleware.Context) Response {
query := models.GetAlertsQuery{
OrgId: c.OrgId,
@@ -64,15 +64,8 @@ func GetAlerts(c *middleware.Context) Response {
Id: alert.Id,
DashboardId: alert.DashboardId,
PanelId: alert.PanelId,
Query: alert.Query,
QueryRefId: alert.QueryRefId,
WarnLevel: alert.WarnLevel,
CritLevel: alert.CritLevel,
Frequency: alert.Frequency,
Name: alert.Name,
Description: alert.Description,
QueryRange: alert.QueryRange,
Aggregator: alert.Aggregator,
State: alert.State,
})
}
+33 -23
View File
@@ -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 {
+5 -7
View File
@@ -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
}
+8
View File
@@ -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()
}
+5 -1
View File
@@ -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())
}
}
}
}
+5 -7
View File
@@ -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()
}
-2
View File
@@ -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
})
+2 -5
View File
@@ -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
+116
View File
@@ -0,0 +1,116 @@
package models
import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
)
type Alert struct {
Id int64
OrgId int64
DashboardId int64
PanelId int64
Name string
Description string
State string
Scheduler int64
Enabled bool
Frequency int
Created time.Time
Updated time.Time
Expression *simplejson.Json
}
func (alert *Alert) ValidToSave() bool {
return alert.DashboardId != 0
}
func (this *Alert) ContainsUpdates(other *Alert) bool {
result := false
result = result || this.Name != other.Name
result = result || this.Description != other.Description
if this.Expression != nil && other.Expression != nil {
json1, err1 := this.Expression.Encode()
json2, err2 := other.Expression.Encode()
if err1 != nil || err2 != nil {
return false
}
result = result || string(json1) != string(json2)
}
//don't compare .State! That would be insane.
return result
}
type AlertingClusterInfo struct {
ServerId string
ClusterSize int
UptimePosition int
}
type HeartBeat struct {
Id int64
ServerId string
Updated time.Time
Created time.Time
}
type HeartBeatCommand struct {
ServerId string
Result AlertingClusterInfo
}
type AlertChange struct {
Id int64 `json:"id"`
OrgId int64 `json:"-"`
AlertId int64 `json:"alertId"`
Type string `json:"type"`
Created time.Time `json:"created"`
}
// Commands
type SaveAlertsCommand struct {
DashboardId int64
UserId int64
OrgId int64
Alerts []*Alert
}
type DeleteAlertCommand struct {
AlertId int64
}
//Queries
type GetAlertsQuery struct {
OrgId int64
State []string
DashboardId int64
PanelId int64
Result []*Alert
}
type GetAllAlertsQuery struct {
Result []*Alert
}
type GetAlertByIdQuery struct {
Id int64
Result *Alert
}
type GetAlertChangesQuery struct {
OrgId int64
Limit int64
SinceId int64
Result []*AlertChange
}
@@ -31,7 +31,7 @@ type UpdateAlertStateCommand struct {
NewState string `json:"newState" binding:"Required"`
Info string `json:"info"`
Result *AlertRule
Result *Alert
}
// Queries
+39
View File
@@ -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)
})
})
}
-125
View File
@@ -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
}
+80
View File
@@ -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
}
+38
View File
@@ -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
}
-75
View File
@@ -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
}
+6
View File
@@ -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 {
+15
View File
@@ -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 },
}
+17 -85
View File
@@ -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,
}
}
}
+33 -8
View File
@@ -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}}),
+119
View File
@@ -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
}
+219
View File
@@ -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")
})
})
})
})
}
+9 -16
View File
@@ -17,22 +17,15 @@ type AlertResult struct {
AlertJob *AlertJob
}
type AlertRule struct {
Id int64
OrgId int64
type Level struct {
Operator string
Level float64
}
type AlertQuery struct {
Query string
DatasourceId int64
DashboardId int64
PanelId int64
Query string
QueryRefId string
WarnLevel float64
CritLevel float64
WarnOperator string
CritOperator string
Frequency int64
Name string
Description string
QueryRange int
Aggregator string
State string
From string
To string
}
@@ -49,22 +49,7 @@ func (arr *AlertRuleReader) Fetch() []*AlertRule {
res := make([]*AlertRule, len(cmd.Result))
for i, ruleDef := range cmd.Result {
model := &AlertRule{}
model.Id = ruleDef.Id
model.OrgId = ruleDef.OrgId
model.DatasourceId = ruleDef.DatasourceId
model.Query = ruleDef.Query
model.QueryRefId = ruleDef.QueryRefId
model.WarnLevel = ruleDef.WarnLevel
model.WarnOperator = ruleDef.WarnOperator
model.CritLevel = ruleDef.CritLevel
model.CritOperator = ruleDef.CritOperator
model.Frequency = ruleDef.Frequency
model.Name = ruleDef.Name
model.Description = ruleDef.Description
model.Aggregator = ruleDef.Aggregator
model.State = ruleDef.State
model.QueryRange = ruleDef.QueryRange
model, _ := NewAlertRuleFromDBModel(ruleDef)
res[i] = model
}
+6 -5
View File
@@ -8,20 +8,22 @@ import (
type SchedulerImpl struct {
jobs map[int64]*AlertJob
log log.Logger
}
func NewScheduler() Scheduler {
return &SchedulerImpl{
jobs: make(map[int64]*AlertJob, 0),
log: log.New("alerting.scheduler"),
}
}
func (s *SchedulerImpl) Update(rules []*AlertRule) {
log.Debug("Scheduler: Update()")
func (s *SchedulerImpl) Update(alerts []*AlertRule) {
s.log.Debug("Scheduling update", "alerts.count", len(alerts))
jobs := make(map[int64]*AlertJob, 0)
for i, rule := range rules {
for i, rule := range alerts {
var job *AlertJob
if s.jobs[rule.Id] != nil {
job = s.jobs[rule.Id]
@@ -38,7 +40,6 @@ func (s *SchedulerImpl) Update(rules []*AlertRule) {
jobs[rule.Id] = job
}
log.Debug("Scheduler: Selected %d jobs", len(jobs))
s.jobs = jobs
}
@@ -47,7 +48,7 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *AlertJob) {
for _, job := range s.jobs {
if now%job.Rule.Frequency == 0 && job.Running == false {
log.Trace("Scheduler: Putting job on to exec queue: %s", job.Rule.Name)
s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name)
execQueue <- job
}
}
@@ -0,0 +1,71 @@
package transformer
import (
"fmt"
"math"
"github.com/grafana/grafana/pkg/tsdb"
)
func NewAggregationTransformer(method string) *AggregationTransformer {
return &AggregationTransformer{
Method: method,
}
}
type AggregationTransformer struct {
Method string
}
func (at *AggregationTransformer) Transform(timeserie *tsdb.TimeSeries) (float64, error) {
if at.Method == "avg" {
sum := float64(0)
for _, point := range timeserie.Points {
sum += point[0]
}
return sum / float64(len(timeserie.Points)), nil
}
if at.Method == "sum" {
sum := float64(0)
for _, v := range timeserie.Points {
sum += v[0]
}
return sum, nil
}
if at.Method == "min" {
min := timeserie.Points[0][0]
for _, v := range timeserie.Points {
if v[0] < min {
min = v[0]
}
}
return min, nil
}
if at.Method == "max" {
max := timeserie.Points[0][0]
for _, v := range timeserie.Points {
if v[0] > max {
max = v[0]
}
}
return max, nil
}
if at.Method == "mean" {
midPosition := int64(math.Floor(float64(len(timeserie.Points)) / float64(2)))
return timeserie.Points[midPosition][0], nil
}
return float64(0), fmt.Errorf("Missing method")
}
@@ -0,0 +1,7 @@
package transformer
import "github.com/grafana/grafana/pkg/tsdb"
type Transformer interface {
Transform(timeserie *tsdb.TimeSeries) (float64, error)
}
+1
View File
@@ -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 {
+1
View File
@@ -42,6 +42,7 @@ type FindPersistedDashboardsQuery struct {
UserId int64
IsStarred bool
DashboardIds []int
Limit int
Result HitList
}
@@ -64,7 +64,7 @@ func HeartBeat(query *m.HeartBeatCommand) error {
*/
func GetAlertById(query *m.GetAlertByIdQuery) error {
alert := m.AlertRule{}
alert := m.Alert{}
has, err := x.Id(query.Id).Get(&alert)
if !has {
return fmt.Errorf("could not find alert")
@@ -78,8 +78,8 @@ func GetAlertById(query *m.GetAlertByIdQuery) error {
}
func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error {
var alerts []*m.AlertRule
err := x.Sql("select * from alert_rule").Find(&alerts)
var alerts []*m.Alert
err := x.Sql("select * from alert").Find(&alerts)
if err != nil {
return err
}
@@ -90,7 +90,7 @@ func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error {
func DeleteAlertById(cmd *m.DeleteAlertCommand) error {
return inTransaction(func(sess *xorm.Session) error {
if _, err := sess.Exec("DELETE FROM alert_rule WHERE id = ?", cmd.AlertId); err != nil {
if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", cmd.AlertId); err != nil {
return err
}
@@ -103,7 +103,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
params := make([]interface{}, 0)
sql.WriteString(`SELECT *
from alert_rule
from alert
`)
sql.WriteString(`WHERE org_id = ?`)
@@ -131,7 +131,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
sql.WriteString(")")
}
alerts := make([]*m.AlertRule, 0)
alerts := make([]*m.Alert, 0)
if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
return err
}
@@ -141,15 +141,17 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
}
func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error {
alerts := make([]*m.AlertRule, 0)
alerts := make([]*m.Alert, 0)
sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
for _, alert := range alerts {
_, err := sess.Exec("DELETE FROM alert_rule WHERE id = ? ", alert.Id)
_, err := sess.Exec("DELETE FROM alert WHERE id = ? ", alert.Id)
if err != nil {
return err
}
sqlog.Debug("Alert deleted (due to dashboard deletion)", "name", alert.Name, "id", alert.Id)
if err := SaveAlertChange("DELETED", alert, sess); err != nil {
return err
}
@@ -172,21 +174,22 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error {
})
}
func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Session) error {
func upsertAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) error {
for _, alert := range posted {
update := false
var alertToUpdate *m.AlertRule
var alertToUpdate *m.Alert
for _, k := range alerts {
if alert.PanelId == k.PanelId {
update = true
alert.Id = k.Id
alertToUpdate = k
break
}
}
if update {
if alertToUpdate.Equals(alert) {
if alertToUpdate.ContainsUpdates(alert) {
alert.Updated = time.Now()
alert.State = alertToUpdate.State
_, err := sess.Id(alert.Id).Update(alert)
@@ -194,6 +197,7 @@ func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Sessi
return err
}
sqlog.Debug("Alert updated", "name", alert.Name, "id", alert.Id)
SaveAlertChange("UPDATED", alert, sess)
}
@@ -205,6 +209,8 @@ func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Sessi
if err != nil {
return err
}
sqlog.Debug("Alert inserted", "name", alert.Name, "id", alert.Id)
SaveAlertChange("CREATED", alert, sess)
}
}
@@ -212,22 +218,25 @@ func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Sessi
return nil
}
func deleteMissingAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Session) error {
func deleteMissingAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) error {
for _, missingAlert := range alerts {
missing := true
for _, k := range posted {
if missingAlert.PanelId == k.PanelId {
missing = false
break
}
}
if missing {
_, err := sess.Exec("DELETE FROM alert_rule WHERE id = ?", missingAlert.Id)
_, err := sess.Exec("DELETE FROM alert WHERE id = ?", missingAlert.Id)
if err != nil {
return err
}
sqlog.Debug("Alert deleted", "name", missingAlert.Name, "id", missingAlert.Id)
err = SaveAlertChange("DELETED", missingAlert, sess)
if err != nil {
return err
@@ -238,12 +247,12 @@ func deleteMissingAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xor
return nil
}
func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.AlertRule, error) {
alerts := make([]*m.AlertRule, 0)
func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.Alert, error) {
alerts := make([]*m.Alert, 0)
err := sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
if err != nil {
return []*m.AlertRule{}, err
return []*m.Alert{}, err
}
return alerts, nil
+3 -3
View File
@@ -39,7 +39,7 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error {
params = append(params, query.Limit)
}
alertChanges := make([]*m.AlertRuleChange, 0)
alertChanges := make([]*m.AlertChange, 0)
if err := x.Sql(sql.String(), params...).Find(&alertChanges); err != nil {
return err
}
@@ -48,8 +48,8 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error {
return nil
}
func SaveAlertChange(change string, alert *m.AlertRule, sess *xorm.Session) error {
_, err := sess.Insert(&m.AlertRuleChange{
func SaveAlertChange(change string, alert *m.Alert, sess *xorm.Session) error {
_, err := sess.Insert(&m.AlertChange{
OrgId: alert.OrgId,
Type: change,
Created: time.Now(),
@@ -20,22 +20,13 @@ func TestAlertRuleChangesDataAccess(t *testing.T) {
var err error
Convey("When dashboard is removed", func() {
items := []*m.AlertRule{
items := []*m.Alert{
{
PanelId: 1,
DashboardId: testDash.Id,
Query: "Query",
QueryRefId: "A",
WarnLevel: 30,
CritLevel: 50,
WarnOperator: ">",
CritOperator: ">",
Frequency: 10,
Name: "Alerting title",
Description: "Alerting description",
QueryRange: 3600,
Aggregator: "avg",
OrgId: FakeOrgId,
PanelId: 1,
DashboardId: testDash.Id,
Name: "Alerting title",
Description: "Alerting description",
OrgId: FakeOrgId,
},
}
+22 -48
View File
@@ -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",
},
}
+3 -2
View File
@@ -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")
+6 -15
View File
@@ -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",
},
}
+7 -1
View File
@@ -123,6 +123,11 @@ type DashboardSearchProjection struct {
}
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
limit := query.Limit
if limit == 0 {
limit = 1000
}
var sql bytes.Buffer
params := make([]interface{}, 0)
@@ -165,7 +170,8 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
params = append(params, "%"+query.Title+"%")
}
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT ?"))
params = append(params, limit)
var res []DashboardSearchProjection
@@ -1,415 +0,0 @@
package sqlstore
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertModel(t *testing.T) {
Convey("Parsing alerts from dashboard", t, func() {
json := `{
"id": 57,
"title": "Graphite 4",
"originalTitle": "Graphite 4",
"tags": [
"graphite"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [
{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [
{
"title": "Active desktop users",
"error": false,
"span": 6,
"editable": true,
"type": "graph",
"isNew": true,
"id": 3,
"targets": [
{
"refId": "A",
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
}
],
"datasource": null,
"renderer": "flot",
"yaxes": [
{
"label": null,
"show": true,
"logBase": 1,
"min": null,
"max": null,
"format": "short"
},
{
"label": null,
"show": true,
"logBase": 1,
"min": null,
"max": null,
"format": "short"
}
],
"xaxis": {
"show": true
},
"grid": {
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"lines": true,
"fill": 1,
"linewidth": 2,
"points": false,
"pointradius": 5,
"bars": false,
"stack": false,
"percentage": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"shared": true,
"msResolution": false
},
"timeFrom": null,
"timeShift": null,
"aliasColors": {},
"seriesOverrides": [],
"alerting": {
"queryRef": "A",
"warnLevel": 30,
"critLevel": 50,
"warnOperator": ">",
"critOperator": ">",
"aggregator": "sum",
"queryRange": 3600,
"frequency": 10,
"name": "active desktop users",
"description": "restart webservers"
},
"links": []
},
{
"title": "Active mobile users",
"error": false,
"span": 6,
"editable": true,
"type": "graph",
"isNew": true,
"id": 4,
"targets": [
{
"refId": "A",
"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"
}
],
"datasource": "graphite2",
"renderer": "flot",
"yaxes": [
{
"label": null,
"show": true,
"logBase": 1,
"min": null,
"max": null,
"format": "short"
},
{
"label": null,
"show": true,
"logBase": 1,
"min": null,
"max": null,
"format": "short"
}
],
"xaxis": {
"show": true
},
"grid": {
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"lines": true,
"fill": 1,
"linewidth": 2,
"points": false,
"pointradius": 5,
"bars": false,
"stack": false,
"percentage": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"shared": true,
"msResolution": false
},
"timeFrom": null,
"timeShift": null,
"aliasColors": {
"mobile": "#EAB839"
},
"seriesOverrides": [],
"alerting": {
"queryRef": "A",
"warnOperator": ">",
"critOperator": ">",
"warnLevel": 300,
"critLevel": 500,
"aggregator": "avg",
"queryRange": 3600,
"frequency": 10,
"name": "active mobile users",
"description": "restart itunes"
},
"links": []
}
],
"title": "Row"
},
{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [
{
"columns": [],
"datasource": "InfluxDB",
"editable": true,
"error": false,
"fontSize": "100%",
"id": 2,
"isNew": true,
"pageSize": null,
"scroll": true,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"span": 6,
"styles": [
{
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
}
],
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "cpu",
"policy": "default",
"query": "SELECT mean(\"value\") FROM \"cpu\" WHERE $timeFilter GROUP BY time($interval) fill(null)",
"refId": "A",
"resultFormat": "table",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": [],
"target": ""
}
],
"title": "Broken influxdb panel",
"transform": "table",
"type": "table",
"links": []
}
],
"title": "New row"
}
],
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"now": true,
"nowDelay": "5m",
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d",
"7d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"schemaVersion": 12,
"version": 16,
"links": []
}`
dashboardJson, _ := simplejson.NewJson([]byte(json))
cmd := &m.SaveDashboardCommand{
Dashboard: dashboardJson,
UserId: 1,
OrgId: 1,
Overwrite: true,
Result: &m.Dashboard{
Id: 1,
},
}
InitTestDB(t)
AddDataSource(&m.AddDataSourceCommand{
Name: "graphite2",
OrgId: 1,
Type: m.DS_INFLUXDB,
Access: m.DS_ACCESS_DIRECT,
Url: "http://test",
IsDefault: false,
Database: "site",
})
AddDataSource(&m.AddDataSourceCommand{
Name: "InfluxDB",
OrgId: 1,
Type: m.DS_GRAPHITE,
Access: m.DS_ACCESS_DIRECT,
Url: "http://test",
IsDefault: true,
})
alerts := alerting.ParseAlertsFromDashboard(cmd)
Convey("all properties have been set", func() {
So(alerts, ShouldNotBeEmpty)
So(len(alerts), ShouldEqual, 2)
for _, v := range alerts {
So(v.DashboardId, ShouldEqual, 1)
So(v.PanelId, ShouldNotEqual, 0)
So(v.WarnLevel, ShouldNotBeEmpty)
So(v.CritLevel, ShouldNotBeEmpty)
So(v.Aggregator, ShouldNotBeEmpty)
So(v.Query, ShouldNotBeEmpty)
So(v.QueryRefId, ShouldNotBeEmpty)
So(v.QueryRange, ShouldNotEqual, 0)
So(v.Frequency, ShouldNotEqual, 0)
So(v.Name, ShouldNotBeEmpty)
So(v.Description, ShouldNotBeEmpty)
}
So(alerts[0].WarnLevel, ShouldEqual, 30)
So(alerts[1].WarnLevel, ShouldEqual, 300)
So(alerts[0].Frequency, ShouldEqual, 10)
So(alerts[1].Frequency, ShouldEqual, 10)
So(alerts[0].CritLevel, ShouldEqual, 50)
So(alerts[1].CritLevel, ShouldEqual, 500)
So(alerts[0].CritOperator, ShouldEqual, ">")
So(alerts[1].CritOperator, ShouldEqual, ">")
So(alerts[0].WarnOperator, ShouldEqual, ">")
So(alerts[1].WarnOperator, ShouldEqual, ">")
So(alerts[0].Query, ShouldEqual, `{"refId":"A","target":"aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"}`)
So(alerts[1].Query, ShouldEqual, `{"refId":"A","target":"aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}`)
So(alerts[0].DatasourceId, ShouldEqual, 2)
So(alerts[1].DatasourceId, ShouldEqual, 1)
})
})
}
+1 -3
View File
@@ -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)
}
}
+8 -14
View File
@@ -7,35 +7,29 @@ import (
func addAlertMigrations(mg *Migrator) {
alertV1 := Table{
Name: "alert_rule",
Name: "alert",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "dashboard_id", Type: DB_BigInt, Nullable: false},
{Name: "datasource_id", Type: DB_BigInt, Nullable: false},
{Name: "panel_id", Type: DB_BigInt, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "query", Type: DB_Text, Nullable: false},
{Name: "query_ref_id", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "warn_level", Type: DB_Float, Nullable: false},
{Name: "warn_operator", Type: DB_NVarchar, Length: 10, Nullable: false},
{Name: "crit_level", Type: DB_Float, Nullable: false},
{Name: "crit_operator", Type: DB_NVarchar, Length: 10, Nullable: false},
{Name: "frequency", Type: DB_BigInt, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "description", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "query_range", Type: DB_Int, Nullable: false},
{Name: "aggregator", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "expression", Type: DB_Text, Nullable: false},
{Name: "scheduler", Type: DB_BigInt, Nullable: false},
{Name: "frequency", Type: DB_BigInt, Nullable: false},
{Name: "enabled", Type: DB_Bool, Nullable: false},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
}
// create table
mg.AddMigration("create alert_rule table v2", NewAddTableMigration(alertV1))
mg.AddMigration("create alert table v1", NewAddTableMigration(alertV1))
alert_changes := Table{
Name: "alert_rule_change",
Name: "alert_change",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "alert_id", Type: DB_BigInt, Nullable: false},
@@ -45,7 +39,7 @@ func addAlertMigrations(mg *Migrator) {
},
}
mg.AddMigration("create alert_rules_updates table v1", NewAddTableMigration(alert_changes))
mg.AddMigration("create alert_change table v1", NewAddTableMigration(alert_changes))
alert_state_log := Table{
Name: "alert_state",
@@ -6,7 +6,6 @@ import (
"github.com/go-xorm/xorm"
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/inconshreveable/log15"
. "github.com/smartystreets/goconvey/convey"
//"github.com/grafana/grafana/pkg/log"
@@ -30,7 +29,6 @@ func TestMigrations(t *testing.T) {
sqlutil.CleanDB(x)
mg := NewMigrator(x)
mg.Logger.SetHandler(log15.DiscardHandler())
AddMigrations(mg)
err = mg.Start()
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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())
+6 -7
View File
@@ -5,9 +5,9 @@ import (
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/tsdb"
)
@@ -31,7 +31,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
result := &tsdb.BatchResult{}
params := url.Values{
"from": []string{context.TimeRange.From},
"from": []string{formatTimeRange(context.TimeRange.From)},
"until": []string{context.TimeRange.To},
"format": []string{"json"},
"maxDataPoints": []string{"500"},
@@ -39,7 +39,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
for _, query := range queries {
params["target"] = []string{
getTargetFromQuery(query.Query),
query.Query,
}
}
@@ -60,7 +60,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
var data []TargetResponseDTO
err = json.Unmarshal(body, &data)
if err != nil {
glog.Info("Failed to unmarshal graphite response", "error", err)
glog.Info("Failed to unmarshal graphite response", "error", err, "body", string(body))
result.Error = err
return result
}
@@ -78,7 +78,6 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
return result
}
func getTargetFromQuery(query string) string {
json, _ := simplejson.NewJson([]byte(query))
return json.Get("target").MustString()
func formatTimeRange(input string) string {
return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
}
@@ -0,0 +1,123 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
export class QueryPartDef {
type: string;
params: any[];
defaultParams: any[];
renderer: any;
category: any;
addStrategy: any;
constructor(options: any) {
this.type = options.type;
this.params = options.params;
this.defaultParams = options.defaultParams;
this.renderer = options.renderer;
this.category = options.category;
this.addStrategy = options.addStrategy;
}
}
export class QueryPart {
part: any;
def: QueryPartDef;
params: any[];
text: string;
constructor(part: any, def: any) {
this.part = part;
this.def = def;
if (!this.def) {
throw {message: 'Could not find query part ' + part.type};
}
part.params = part.params || _.clone(this.def.defaultParams);
this.params = part.params;
this.updateText();
}
render(innerExpr: string) {
return this.def.renderer(this, innerExpr);
}
hasMultipleParamsInString (strValue, index) {
if (strValue.indexOf(',') === -1) {
return false;
}
return this.def.params[index + 1] && this.def.params[index + 1].optional;
}
updateParam (strValue, index) {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this.hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal: string, idx) {
this.updateParam(partVal.trim(), idx);
}, this);
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
} else {
this.params[index] = strValue;
}
this.part.params = this.params;
this.updateText();
}
updateText() {
if (this.params.length === 0) {
this.text = this.def.type + '()';
return;
}
var text = this.def.type + '(';
text += this.params.join(', ');
text += ')';
this.text = text;
}
}
export function functionRenderer(part, innerExpr) {
var str = part.def.type + '(';
var parameters = _.map(part.params, (value, index) => {
var paramType = part.def.params[index];
if (paramType.type === 'time') {
if (value === 'auto') {
value = '$interval';
}
}
if (paramType.quote === 'single') {
return "'" + value + "'";
} else if (paramType.quote === 'double') {
return '"' + value + '"';
}
return value;
});
if (innerExpr) {
parameters.unshift(innerExpr);
}
return str + parameters.join(', ') + ')';
}
export function suffixRenderer(part, innerExpr) {
return innerExpr + ' ' + part.params[0];
}
export function identityRenderer(part, innerExpr) {
return part.params[0];
}
export function quotedIdentityRenderer(part, innerExpr) {
return '"' + part.params[0] + '"';
}
@@ -0,0 +1,183 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import $ from 'jquery';
import coreModule from 'app/core/core_module';
var template = `
<div class="tight-form-func-controls">
<span class="pointer fa fa-remove" ng-click="removeActionInternal()"></span>
</div>
<a ng-click="toggleControls()" class="query-part-name">{{part.def.type}}</a>
<span>(</span><span class="query-part-parameters"></span><span>)</span>
`;
/** @ngInject */
export function queryPartEditorDirective($compile, templateSrv) {
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
return {
restrict: 'E',
template: template,
scope: {
part: "=",
removeAction: "&",
partUpdated: "&",
getOptions: "&",
},
link: function postLink($scope, elem) {
var part = $scope.part;
var partDef = part.def;
var $paramsContainer = elem.find('.query-part-parameters');
var $controlsContainer = elem.find('.tight-form-func-controls');
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $input = $link.next();
$input.val(part.params[paramIndex]);
$input.css('width', ($link.width() + 16) + 'px');
$link.hide();
$input.show();
$input.focus();
$input.select();
var typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if (newValue !== '' || part.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
part.updateParam($input.val(), paramIndex);
$scope.$apply($scope.partUpdated);
}
$input.hide();
$link.show();
}
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if (e.which === 13) {
inputBlur.call(this, paramIndex);
}
}
function inputKeyDown() {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, param, paramIndex) {
if (!param.options && !param.dynamicLookup) {
return;
}
var typeaheadSource = function (query, callback) {
if (param.options) { return param.options; }
$scope.$apply(function() {
$scope.getOptions().then(function(result) {
var dynamicOptions = _.map(result, function(op) { return op.value; });
callback(dynamicOptions);
});
});
};
$input.attr('data-provide', 'typeahead');
var options = param.options;
if (param.type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: typeaheadSource,
minLength: 0,
items: 1000,
updater: function (value) {
setTimeout(function() {
inputBlur.call($input[0], paramIndex);
}, 0);
return value;
}
});
var typeahead = $input.data('typeahead');
typeahead.lookup = function () {
this.query = this.$element.val() || '';
var items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
}
$scope.toggleControls = function() {
var targetDiv = elem.closest('.tight-form');
if (elem.hasClass('show-function-controls')) {
elem.removeClass('show-function-controls');
targetDiv.removeClass('has-open-function');
$controlsContainer.hide();
return;
}
elem.addClass('show-function-controls');
targetDiv.addClass('has-open-function');
$controlsContainer.show();
};
$scope.removeActionInternal = function() {
$scope.toggleControls();
$scope.removeAction();
};
function addElementsAndCompile() {
_.each(partDef.params, function(param, index) {
if (param.optional && part.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo($paramsContainer);
}
var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
var $input = $(paramTemplate);
$paramLink.appendTo($paramsContainer);
$input.appendTo($paramsContainer);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
addTypeahead($input, param, index);
});
}
function relink() {
$paramsContainer.empty();
addElementsAndCompile();
}
relink();
}
};
}
coreModule.directive('queryPartEditor', queryPartEditorDirective);
+2
View File
@@ -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,
};
+13 -10
View File
@@ -120,25 +120,28 @@ function (angular, _, $) {
if (this.panelScopes.length === 0) { return; }
if (this.dashboard.meta.fullscreen) {
if (this.fullscreenPanel) {
this.leaveFullscreen(false);
}
var panelScope = this.getPanelScope(this.state.panelId);
// panel could be about to be created/added and scope does
// not exist yet
if (!panelScope) {
return;
}
if (this.fullscreenPanel) {
// if already fullscreen
if (this.fullscreenPanel === panelScope) {
return;
} else {
this.leaveFullscreen(false);
}
}
if (!panelScope.ctrl.editModeInitiated) {
panelScope.ctrl.initEditMode();
}
this.enterFullscreen(panelScope);
return;
}
if (this.fullscreenPanel) {
if (!panelScope.ctrl.fullscreen) {
this.enterFullscreen(panelScope);
}
} else if (this.fullscreenPanel) {
this.leaveFullscreen(true);
}
};
@@ -51,7 +51,7 @@
<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
Current
</span>
<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
<a ng-click="ctrl.setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
Select
</a>
</td>
+2 -2
View File
@@ -152,8 +152,8 @@ export class PanelCtrl {
calculatePanelHeight() {
if (this.fullscreen) {
var docHeight = $(window).height();
var editHeight = Math.floor(docHeight * 0.3);
var fullscreenHeight = Math.floor(docHeight * 0.7);
var editHeight = Math.floor(docHeight * 0.4);
var fullscreenHeight = Math.floor(docHeight * 0.6);
this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
} else {
this.containerHeight = this.panel.height || this.row.height;
@@ -35,13 +35,13 @@
</div>
<div class="gf-form" ng-repeat="part in selectParts">
<influx-query-part-editor
<query-part-editor
class="gf-form-label query-part"
part="part"
remove-action="ctrl.removeSelectPart(selectParts, part)"
part-updated="ctrl.selectPartUpdated(selectParts, part)"
get-options="ctrl.getPartOptions(part)">
</influx-query-part-editor>
</query-part-editor>
</div>
<div class="gf-form">
@@ -62,12 +62,12 @@
<span>GROUP BY</span>
</label>
<influx-query-part-editor
<query-part-editor
ng-repeat="part in ctrl.queryModel.groupByParts"
part="part"
class="gf-form-label query-part"
remove-action="ctrl.removeGroupByPart(part, $index)" part-updated="ctrl.refresh();" get-options="ctrl.getPartOptions(part)">
</influx-query-part-editor>
</query-part-editor>
</div>
<div class="gf-form">
@@ -1,5 +0,0 @@
<div class="tight-form-func-controls">
<span class="pointer fa fa-remove" ng-click="removeActionInternal()" ></span>
</div>
<a ng-click="toggleControls()" class="query-part-name">{{part.def.type}}</a><span>(</span><span class="query-part-parameters"></span><span>)</span>
@@ -1,8 +1,5 @@
///<reference path="../../../headers/common.d.ts" />
import './query_part_editor';
import './query_part_editor';
import angular from 'angular';
import _ from 'lodash';
import InfluxQueryBuilder from './query_builder';
@@ -1,6 +1,14 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import {
QueryPartDef,
QueryPart,
functionRenderer,
suffixRenderer,
identityRenderer,
quotedIdentityRenderer,
} from 'app/core/components/query_part/query_part';
var index = [];
var categories = {
@@ -12,71 +20,26 @@ var categories = {
Fields: [],
};
function createPart(part): any {
var def = index[part.type];
if (!def) {
throw {message: 'Could not find query part ' + part.type};
}
return new QueryPart(part, def);
};
function register(options: any) {
index[options.type] = new QueryPartDef(options);
options.category.push(index[options.type]);
}
var groupByTimeFunctions = [];
class QueryPartDef {
type: string;
params: any[];
defaultParams: any[];
renderer: any;
category: any;
addStrategy: any;
constructor(options: any) {
this.type = options.type;
this.params = options.params;
this.defaultParams = options.defaultParams;
this.renderer = options.renderer;
this.category = options.category;
this.addStrategy = options.addStrategy;
}
static register(options: any) {
index[options.type] = new QueryPartDef(options);
options.category.push(index[options.type]);
}
}
function functionRenderer(part, innerExpr) {
var str = part.def.type + '(';
var parameters = _.map(part.params, (value, index) => {
var paramType = part.def.params[index];
if (paramType.type === 'time') {
if (value === 'auto') {
value = '$interval';
}
}
if (paramType.quote === 'single') {
return "'" + value + "'";
} else if (paramType.quote === 'double') {
return '"' + value + '"';
}
return value;
});
if (innerExpr) {
parameters.unshift(innerExpr);
}
return str + parameters.join(', ') + ')';
}
function aliasRenderer(part, innerExpr) {
return innerExpr + ' AS ' + '"' + part.params[0] + '"';
}
function suffixRenderer(part, innerExpr) {
return innerExpr + ' ' + part.params[0];
}
function identityRenderer(part, innerExpr) {
return part.params[0];
}
function quotedIdentityRenderer(part, innerExpr) {
return '"' + part.params[0] + '"';
}
function fieldRenderer(part, innerExpr) {
if (part.params[0] === '*') {
return '*';
@@ -149,13 +112,13 @@ function addAliasStrategy(selectParts, partModel) {
function addFieldStrategy(selectParts, partModel, query) {
// copy all parts
var parts = _.map(selectParts, function(part: any) {
return new QueryPart({type: part.def.type, params: _.clone(part.params)});
return createPart({type: part.def.type, params: _.clone(part.params)});
});
query.selectModels.push(parts);
}
QueryPartDef.register({
register({
type: 'field',
addStrategy: addFieldStrategy,
category: categories.Fields,
@@ -165,7 +128,7 @@ QueryPartDef.register({
});
// Aggregations
QueryPartDef.register({
register({
type: 'count',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
@@ -174,7 +137,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'distinct',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
@@ -183,7 +146,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'integral',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
@@ -192,7 +155,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'mean',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
@@ -201,7 +164,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'median',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
@@ -210,7 +173,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'sum',
addStrategy: replaceAggregationAddStrategy,
category: categories.Aggregations,
@@ -221,7 +184,7 @@ QueryPartDef.register({
// transformations
QueryPartDef.register({
register({
type: 'derivative',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
@@ -230,7 +193,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'spread',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
@@ -239,7 +202,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'non_negative_derivative',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
@@ -248,7 +211,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'difference',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
@@ -257,7 +220,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'moving_average',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
@@ -266,7 +229,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'stddev',
addStrategy: addTransformationStrategy,
category: categories.Transformations,
@@ -275,7 +238,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'time',
category: groupByTimeFunctions,
params: [{ name: "interval", type: "time", options: ['auto', '1s', '10s', '1m', '5m', '10m', '15m', '1h'] }],
@@ -283,7 +246,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'fill',
category: groupByTimeFunctions,
params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }],
@@ -292,7 +255,7 @@ QueryPartDef.register({
});
// Selectors
QueryPartDef.register({
register({
type: 'bottom',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
@@ -301,7 +264,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'first',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
@@ -310,7 +273,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'last',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
@@ -319,7 +282,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'max',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
@@ -328,7 +291,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'min',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
@@ -337,7 +300,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'percentile',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
@@ -346,7 +309,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'top',
addStrategy: replaceAggregationAddStrategy,
category: categories.Selectors,
@@ -355,7 +318,7 @@ QueryPartDef.register({
renderer: functionRenderer,
});
QueryPartDef.register({
register({
type: 'tag',
category: groupByTimeFunctions,
params: [{name: 'tag', type: 'string', dynamicLookup: true}],
@@ -363,7 +326,7 @@ QueryPartDef.register({
renderer: fieldRenderer,
});
QueryPartDef.register({
register({
type: 'math',
addStrategy: addMathStrategy,
category: categories.Math,
@@ -372,7 +335,7 @@ QueryPartDef.register({
renderer: suffixRenderer,
});
QueryPartDef.register({
register({
type: 'alias',
addStrategy: addAliasStrategy,
category: categories.Aliasing,
@@ -382,74 +345,9 @@ QueryPartDef.register({
renderer: aliasRenderer,
});
class QueryPart {
part: any;
def: QueryPartDef;
params: any[];
text: string;
constructor(part: any) {
this.part = part;
this.def = index[part.type];
if (!this.def) {
throw {message: 'Could not find query part ' + part.type};
}
part.params = part.params || _.clone(this.def.defaultParams);
this.params = part.params;
this.updateText();
}
render(innerExpr: string) {
return this.def.renderer(this, innerExpr);
}
hasMultipleParamsInString (strValue, index) {
if (strValue.indexOf(',') === -1) {
return false;
}
return this.def.params[index + 1] && this.def.params[index + 1].optional;
}
updateParam (strValue, index) {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this.hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal: string, idx) {
this.updateParam(partVal.trim(), idx);
}, this);
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
} else {
this.params[index] = strValue;
}
this.part.params = this.params;
this.updateText();
}
updateText() {
if (this.params.length === 0) {
this.text = this.def.type + '()';
return;
}
var text = this.def.type + '(';
text += this.params.join(', ');
text += ')';
this.text = text;
}
}
export default {
create: function(part): any {
return new QueryPart(part);
},
create: createPart,
getCategories: function() {
return categories;
}
@@ -1,178 +0,0 @@
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
'use strict';
angular
.module('grafana.directives')
.directive('influxQueryPartEditor', function($compile, templateSrv) {
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
return {
restrict: 'E',
templateUrl: 'public/app/plugins/datasource/influxdb/partials/query_part.html',
scope: {
part: "=",
removeAction: "&",
partUpdated: "&",
getOptions: "&",
},
link: function postLink($scope, elem) {
var part = $scope.part;
var partDef = part.def;
var $paramsContainer = elem.find('.query-part-parameters');
var $controlsContainer = elem.find('.tight-form-func-controls');
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $input = $link.next();
$input.val(part.params[paramIndex]);
$input.css('width', ($link.width() + 16) + 'px');
$link.hide();
$input.show();
$input.focus();
$input.select();
var typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if (newValue !== '' || part.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
part.updateParam($input.val(), paramIndex);
$scope.$apply($scope.partUpdated);
}
$input.hide();
$link.show();
}
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if(e.which === 13) {
inputBlur.call(this, paramIndex);
}
}
function inputKeyDown() {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, param, paramIndex) {
if (!param.options && !param.dynamicLookup) {
return;
}
var typeaheadSource = function (query, callback) {
if (param.options) { return param.options; }
$scope.$apply(function() {
$scope.getOptions().then(function(result) {
var dynamicOptions = _.map(result, function(op) { return op.value; });
callback(dynamicOptions);
});
});
};
$input.attr('data-provide', 'typeahead');
var options = param.options;
if (param.type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: typeaheadSource,
minLength: 0,
items: 1000,
updater: function (value) {
setTimeout(function() {
inputBlur.call($input[0], paramIndex);
}, 0);
return value;
}
});
var typeahead = $input.data('typeahead');
typeahead.lookup = function () {
this.query = this.$element.val() || '';
var items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
}
$scope.toggleControls = function() {
var targetDiv = elem.closest('.tight-form');
if (elem.hasClass('show-function-controls')) {
elem.removeClass('show-function-controls');
targetDiv.removeClass('has-open-function');
$controlsContainer.hide();
return;
}
elem.addClass('show-function-controls');
targetDiv.addClass('has-open-function');
$controlsContainer.show();
};
$scope.removeActionInternal = function() {
$scope.toggleControls();
$scope.removeAction();
};
function addElementsAndCompile() {
_.each(partDef.params, function(param, index) {
if (param.optional && part.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo($paramsContainer);
}
var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
var $input = $(paramTemplate);
$paramLink.appendTo($paramsContainer);
$input.appendTo($paramsContainer);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
addTypeahead($input, param, index);
});
}
function relink() {
$paramsContainer.empty();
addElementsAndCompile();
}
relink();
}
};
});
});
@@ -403,10 +403,7 @@ function (angular, _, dateMath) {
} else {
return _.findIndex(options.targets, function(target) {
if (target.filters && target.filters.length > 0) {
return target.metric === metricData.metric &&
_.all(target.filters, function(filter) {
return filter.tagk === interpolatedTagValue === "*";
});
return target.metric === metricData.metric;
} else {
return target.metric === metricData.metric &&
_.all(target.tags, function(tagV, tagK) {
@@ -0,0 +1,135 @@
///<reference path="../../../headers/common.d.ts" />
import 'jquery.flot';
import $ from 'jquery';
import _ from 'lodash';
export class AlertHandleManager {
plot: any;
placeholder: any;
height: any;
alert: any;
constructor(private panelCtrl) {
this.alert = panelCtrl.panel.alert;
}
getHandleInnerHtml(type, op, value) {
if (op === '>') { op = '&gt;'; }
if (op === '<') { op = '&lt;'; }
return `
<div class="alert-handle-line">
</div>
<div class="alert-handle">
<i class="icon-gf icon-gf-${type} alert-icon-${type}"></i>
${op} ${value}
</div>`;
}
getFullHandleHtml(type, op, value) {
var innerTemplate = this.getHandleInnerHtml(type, op, value);
return `
<div class="alert-handle-wrapper alert-handle-wrapper--${type}">
${innerTemplate}
</div>
`;
}
setupDragging(handleElem, levelModel) {
var isMoving = false;
var lastY = null;
var posTop;
var plot = this.plot;
var panelCtrl = this.panelCtrl;
function dragging(evt) {
if (lastY === null) {
lastY = evt.clientY;
} else {
var diff = evt.clientY - lastY;
posTop = posTop + diff;
lastY = evt.clientY;
handleElem.css({top: posTop + diff});
}
}
function stopped() {
isMoving = false;
// calculate graph level
var graphLevel = plot.c2p({left: 0, top: posTop}).y;
console.log('canvasPos:' + posTop + ' Graph level: ' + graphLevel);
graphLevel = parseInt(graphLevel.toFixed(0));
levelModel.level = graphLevel;
console.log(levelModel);
var levelCanvasPos = plot.p2c({x: 0, y: graphLevel});
console.log('canvas pos', levelCanvasPos);
console.log('stopped');
handleElem.off("mousemove", dragging);
handleElem.off("mouseup", dragging);
// trigger digest and render
panelCtrl.$scope.$apply(function() {
panelCtrl.render();
});
}
handleElem.bind('mousedown', function() {
isMoving = true;
lastY = null;
posTop = handleElem.position().top;
console.log('start pos', posTop);
handleElem.on("mousemove", dragging);
handleElem.on("mouseup", stopped);
});
}
cleanUp() {
if (this.placeholder) {
this.placeholder.find(".alert-handle-wrapper").remove();
}
}
renderHandle(type, model, defaultHandleTopPos) {
var handleElem = this.placeholder.find(`.alert-handle-wrapper--${type}`);
var level = model.level;
var levelStr = level;
var handleTopPos = 0;
// handle no value
if (!_.isNumber(level)) {
levelStr = '';
handleTopPos = defaultHandleTopPos;
} else {
var levelCanvasPos = this.plot.p2c({x: 0, y: level});
handleTopPos = Math.min(Math.max(levelCanvasPos.top, 0), this.height) - 6;
}
if (handleElem.length === 0) {
console.log('creating handle');
handleElem = $(this.getFullHandleHtml(type, model.op, levelStr));
this.placeholder.append(handleElem);
this.setupDragging(handleElem, model);
} else {
console.log('reusing handle!');
handleElem.html(this.getHandleInnerHtml(type, model.op, levelStr));
}
handleElem.toggleClass('alert-handle-wrapper--no-value', levelStr === '');
handleElem.css({top: handleTopPos});
}
draw(plot) {
this.plot = plot;
this.placeholder = plot.getPlaceholder();
this.height = plot.height();
this.renderHandle('critical', this.alert.critical, 10);
this.renderHandle('warn', this.alert.warn, this.height-30);
}
}
+137 -44
View File
@@ -1,78 +1,171 @@
///<reference path="../../../headers/common.d.ts" />
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import $ from 'jquery';
import angular from 'angular';
import {
QueryPartDef,
QueryPart,
} from 'app/core/components/query_part/query_part';
var alertQueryDef = new QueryPartDef({
type: 'query',
params: [
{name: "queryRefId", type: 'string', options: ['#A', '#B', '#C', '#D']},
{name: "from", type: "string", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']},
{name: "to", type: "string", options: ['now']},
],
defaultParams: ['#A', '5m', 'now', 'avg']
});
export class AlertTabCtrl {
panel: any;
panelCtrl: any;
alerting: any;
metricTargets = [{ refId: '- select query -' } ];
operators = ['>', '<', '<=', '>='];
aggregators = ['avg', 'sum', 'min', 'max', 'median'];
schedulers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}];
transforms = [
{
text: 'Aggregation',
type: 'aggregation',
},
{
text: 'Linear Forecast',
type: 'forecast',
},
];
aggregators = ['avg', 'sum', 'min', 'max', 'last'];
alert: any;
query: any;
queryParams: any;
transformDef: any;
levelOpList = [
{text: '>', value: '>'},
{text: '<', value: '<'},
{text: '=', value: '='},
];
defaultValues = {
aggregator: 'avg',
frequency: 10,
queryRange: 3600,
warnOperator: '>',
critOperator: '>',
queryRef: '- select query -'
frequency: '60s',
notify: [],
enabled: false,
scheduler: 1,
warn: { op: '>', level: undefined },
critical: { op: '>', level: undefined },
query: {
refId: 'A',
from: '5m',
to: 'now',
},
transform: {
type: 'aggregation',
method: 'avg'
}
};
/** @ngInject */
constructor($scope, private $timeout) {
$scope.alertTab = this; //HACK ATTACK!
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
$scope.ctrl = this;
_.defaults(this.panel.alerting, this.defaultValues);
this.metricTargets = this.panel.targets.map(val => val);
this.initAlertModel();
// set panel alert edit mode
$scope.$on("$destroy", () => {
this.panelCtrl.editingAlert = false;
this.panelCtrl.render();
});
}
initAlertModel() {
if (!this.panel.alert) {
return;
}
this.alert = this.panel.alert;
// set defaults
_.defaults(this.alert, this.defaultValues);
var defaultName = (this.panelCtrl.dashboard.title + ' ' + this.panel.title + ' alert');
this.panel.alerting.name = this.panel.alerting.name || defaultName;
this.alert.name = this.alert.name || defaultName;
this.alert.description = this.alert.description || defaultName;
this.panel.targets.map(target => {
this.metricTargets.push(target);
});
this.panel.alerting.queryRef = this.panel.alerting.queryRef || this.metricTargets[0].refId;
// great temp working model
this.queryParams = {
params: [
this.alert.query.refId,
this.alert.query.from,
this.alert.query.to
]
};
// init the query part components model
this.query = new QueryPart(this.queryParams, alertQueryDef);
this.convertThresholdsToAlertThresholds();
this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type});
this.panelCtrl.editingAlert = true;
this.panelCtrl.render();
}
queryUpdated() {
this.alert.query = {
refId: this.query.params[0],
from: this.query.params[1],
to: this.query.params[2],
};
}
transformChanged() {
// clear model
this.alert.transform = {type: this.alert.transform.type};
this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type});
switch (this.alert.transform.type) {
case 'aggregation': {
this.alert.transform.method = 'avg';
break;
}
case "forecast": {
this.alert.transform.timespan = '7d';
break;
}
}
}
convertThresholdsToAlertThresholds() {
if (this.panel.grid
&& this.panel.grid.threshold1
&& this.panel.alerting.warnLevel === undefined
) {
this.panel.alerting.warnOperator = '>';
this.panel.alerting.warnLevel = this.panel.grid.threshold1;
}
if (this.panel.grid
&& this.panel.grid.threshold2
&& this.panel.alerting.critLevel === undefined
) {
this.panel.alerting.critOperator = '>';
this.panel.alerting.critLevel = this.panel.grid.threshold2;
}
// if (this.panel.grid
// && this.panel.grid.threshold1
// && this.alert.warnLevel === undefined
// ) {
// this.alert.warning.op = '>';
// this.alert.warning.level = this.panel.grid.threshold1;
// }
//
// if (this.panel.grid
// && this.panel.grid.threshold2
// && this.alert.critical.level === undefined
// ) {
// this.alert.critical.op = '>';
// this.alert.critical.level = this.panel.grid.threshold2;
// }
}
markAsDeleted() {
if (this.panel.alerting) {
this.panel.alerting = this.defaultValues;
}
delete() {
delete this.panel.alert;
this.panelCtrl.editingAlert = false;
this.panelCtrl.render();
}
thresholdsUpdated() {
if (this.panel.alerting.warnLevel) {
this.panel.grid.threshold1 = parseInt(this.panel.alerting.warnLevel);
}
if (this.panel.alerting.critLevel) {
this.panel.grid.threshold2 = parseInt(this.panel.alerting.critLevel);
}
enable() {
this.panel.alert = {};
this.initAlertModel();
}
levelsUpdated() {
this.panelCtrl.render();
}
}
+67 -1
View File
@@ -5,6 +5,7 @@ define([
'lodash',
'app/core/utils/kbn',
'./graph_tooltip',
'./alert_handle',
'jquery.flot',
'jquery.flot.selection',
'jquery.flot.time',
@@ -14,13 +15,16 @@ define([
'jquery.flot.crosshair',
'./jquery.flot.events',
],
function (angular, $, moment, _, kbn, GraphTooltip) {
function (angular, $, moment, _, kbn, GraphTooltip, AlertHandle) {
'use strict';
var module = angular.module('grafana.directives');
var labelWidthCache = {};
var panelWidthCache = {};
// systemjs export
var AlertHandleManager = AlertHandle.AlertHandleManager;
module.directive('grafanaGraph', function($rootScope, timeSrv) {
return {
restrict: 'A',
@@ -34,6 +38,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
var legendSideLastValue = null;
var rootScope = scope.$root;
var panelWidth = 0;
var alertHandles;
rootScope.onAppEvent('setCrosshair', function(event, info) {
// do not need to to this if event is from this panel
@@ -161,6 +166,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
}
if (alertHandles) {
alertHandles.draw(plot);
}
}
function processOffsetHook(plot, gridMargin) {
@@ -181,6 +190,18 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
return;
}
// give space to alert editing
if (ctrl.editingAlert) {
if (!alertHandles) {
elem.css('margin-right', '220px');
alertHandles = new AlertHandleManager(ctrl);
}
} else if (alertHandles) {
elem.css('margin-right', '0');
alertHandles.cleanUp();
alertHandles = null;
}
var stack = panel.stack ? true : null;
// Populate element
@@ -259,6 +280,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
function callPlot(incrementRenderCounter) {
try {
console.log('rendering');
$.plot(elem, sortedSeries, options);
} catch (e) {
console.log('flotcharts error', e);
@@ -311,6 +333,50 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
}
function addGridThresholds(options, panel) {
if (panel.alert) {
var crit = panel.alert.critical;
var warn = panel.alert.warn;
var critEdge = Infinity;
var warnEdge = crit.level;
if (_.isNumber(crit.level)) {
if (crit.op === '<') {
critEdge = -Infinity;
}
// fill
options.grid.markings.push({
yaxis: {from: crit.level, to: critEdge},
color: 'rgba(234, 112, 112, 0.10)',
});
// line
options.grid.markings.push({
yaxis: {from: crit.level, to: crit.level},
color: '#ed2e18'
});
}
if (_.isNumber(warn.level)) {
// if (warn.op === '<') {
// }
// fill
options.grid.markings.push({
yaxis: {from: warn.level, to: warnEdge},
color: 'rgba(216, 200, 27, 0.10)',
});
// line
options.grid.markings.push({
yaxis: {from: warn.level, to: warn.level},
color: '#F79520'
});
}
return;
}
if (_.isNumber(panel.grid.threshold1)) {
var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null);
options.grid.markings.push({
@@ -1,63 +1,99 @@
<div class="editor-row">
<div class="gf-form-group section" >
<h5 class="section-heading">Query</h5>
<div class="gf-form" style="margin-bottom: 2rem">
<span class="gf-form-label width-9">Query to watch</span>
<div class="gf-form-select-wrapper max-width-12">
<select class="gf-form-input"
ng-model="ctrl.panel.alerting.queryRef"
ng-options="target.refId as target.refId for target in alertTab.metricTargets"></select>
<div ng-if="ctrl.panel.alert">
<div class="editor-row">
<div class="gf-form-group section" >
<h5 class="section-heading">Alert Query</h5>
<div class="gf-form-inline">
<div class="gf-form">
<query-part-editor
class="gf-form-label query-part"
part="ctrl.query"
part-updated="ctrl.queryUpdated()">
</query-part-editor>
</div>
<div class="gf-form">
<span class="gf-form-label">Transform using</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-model="ctrl.alert.transform.type"
ng-options="f.type as f.text for f in ctrl.transforms"
ng-change="ctrl.transformChanged()"
>
</select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.transformDef.type === 'aggregation'">
<span class="gf-form-label">Method</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-model="ctrl.alert.transform.method"
ng-options="f for f in ctrl.aggregators">
</select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.transformDef.type === 'forecast'">
<span class="gf-form-label">Timespan</span>
<input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
</div>
</div>
</div>
<h5 class="section-heading">Thresholds</h5>
<div class="gf-form">
<span class="gf-form-label width-9">
<i class="icon-gf icon-gf-warn alert-icon-warn"></i>
Warn level
</span>
<div class="gf-form-select-wrapper max-width-10">
<select class="gf-form-input" ng-model="ctrl.panel.alerting.warnOperator" ng-options="oper as oper for oper in alertTab.operators"></select>
<div class="gf-form-group section">
<h5 class="section-heading">Levels</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">
<i class="icon-gf icon-gf-warn alert-icon-warn"></i>
Warn if
</span>
<metric-segment-model property="ctrl.alert.warn.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.warn.level" ng-change="ctrl.levelsUpdated()"></input>
</div>
<div class="gf-form">
<span class="gf-form-label">
<i class="icon-gf icon-gf-warn alert-icon-critical"></i>
Critcal if
</span>
<metric-segment-model property="ctrl.alert.critical.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.critical.level" ng-change="ctrl.levelsUpdated()"></input>
</div>
</div>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.panel.alerting.warnLevel" ng-change="alertTab.thresholdsUpdated()"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-9">
<i class="icon-gf icon-gf-critical alert-icon-critical"></i>
Critical level
</span>
<div class="gf-form-select-wrapper max-width-10">
<select class="gf-form-input" ng-model="ctrl.panel.alerting.critOperator" ng-options="oper as oper for oper in alertTab.operators"></select>
</div>
<div class="editor-row">
<div class="gf-form-group section">
<h5 class="section-heading">Execution</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">Scheduler</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-model="ctrl.alert.scheduler"
ng-options="f.value as f.text for f in ctrl.schedulers">
</select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label">Evaluate every</span>
<input class="gf-form-input max-width-7" type="text" ng-model="ctrl.alert.frequency"></input>
</div>
</div>
</div>
<div class="gf-form-group section">
<h5 class="section-heading">Notifications</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">Groups</span>
<bootstrap-tagsinput ng-model="ctrl.alert.notify" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
</div>
</div>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.panel.alerting.critLevel" ng-change="alertTab.thresholdsUpdated()"></input>
</div>
</div>
<div class="gf-form-group section">
<h5 class="section-heading">Aggregation settings</h5>
<div class="gf-form">
<span class="gf-form-label width-12">Aggregation method</span>
<div class="gf-form-select-wrapper max-width-10">
<select class="gf-form-input"
ng-model="ctrl.panel.alerting.aggregator"
ng-options="oper as oper for oper in alertTab.aggregators"></select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-12">Query range (seconds)</span>
<input class="gf-form-input max-width-10" type="number"
ng-model="ctrl.panel.alerting.queryRange" placeholder="3600"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-12">Frequency (seconds)</span>
<input class="gf-form-input max-width-10" type="number"
ng-model="ctrl.panel.alerting.frequency" placeholder="60"></input>
</div>
</div>
<div class="gf-form-group section">
<h5 class="section-heading">Alert info</h5>
<h5 class="section-heading">Information</h5>
<div class="gf-form">
<span class="gf-form-label width-10">Alert name</span>
<input type="text" class="gf-form-input width-22" ng-model="ctrl.panel.alerting.name">
@@ -72,8 +108,13 @@
</div>
</div>
</div>
<div class="editor-row">
<div class="gf-form-button-row">
<button class="btn btn-warning" ng-click="alertTab.markAsDeleted()">Delete Alert</button>
<button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.panel.alert">Delete</button>
<button class="btn btn-inverse" ng-click="ctrl.enable()" ng-hide="ctrl.panel.alert">
<i class="icon-gf icon-gf-alert"></i>
Add Alert
</button>
</div>
</div>
+1 -1
View File
@@ -44,7 +44,7 @@ $brand-text-highlight: #f7941d;
// Status colors
// -------------------------
$online: #10a345;
$warn: #ffc03c;
$warn: #F79520;
$critical: #ed2e18;
// Scaffolding
+64
View File
@@ -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;
}
}
}
+1
View File
@@ -7,6 +7,7 @@
background-color: $input-bg;
input {
display: inline-block;
border: none;
border-right: 1px solid $tight-form-border;
margin: 0px;
-6
View File
@@ -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;
+1 -1
View File
@@ -1322,7 +1322,7 @@ Licensed under the MIT license.
placeholder.css("padding", 0) // padding messes up the positioning
.children().filter(function(){
return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base');
return $(this).hasClass("flot-text");
}).remove();
if (placeholder.css("position") == 'static')
+1 -1
View File
@@ -35,7 +35,7 @@
this.inputSize = Math.max(1, this.placeholderText.length);
this.$container = $('<div class="bootstrap-tagsinput"></div>');
this.$input = $('<input class="tight-form-input" size="' +
this.$input = $('<input class="gf-form-input" size="' +
this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
this.$element.after(this.$container);