diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c5b9fe3751..d54903c54fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,23 @@
# 5.1.0 (unreleased)
* **Postgres/MySQL**: Ability to insert 0s or nulls for missing intervals [#9487](https://github.com/grafana/grafana/issues/9487), thanks [@svenklemm](https://github.com/svenklemm)
+* **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar)
+* **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942)
+* **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda)
+### Minor
+* **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
# 5.0.1 (unreleased)
* **Postgres**: PostgreSQL error when using ipv6 address as hostname in connection string [#11055](https://github.com/grafana/grafana/issues/11055), thanks [@svenklemm](https://github.com/svenklemm)
+* **Dashboards**: Changing templated value from dropdown is causing unsaved changes [#11063](https://github.com/grafana/grafana/issues/11063)
+* **Prometheus**: Fixes bundled Prometheus 2.0 dashboard [#11016](https://github.com/grafana/grafana/issues/11016), thx [@roidelapluie](https://github.com/roidelapluie)
+* **Sidemenu**: Profile menu "invisible" when gravatar is disabled [#11097](https://github.com/grafana/grafana/issues/11097)
+* **Dashboard**: Fixes a bug with resizeable handles for panels [#11103](https://github.com/grafana/grafana/issues/11103)
+* **Alerting**: Telegram inline image mode fails when caption too long [#10975](https://github.com/grafana/grafana/issues/10975)
+* **Alerting**: Fixes silent failing validation [#11145](https://github.com/grafana/grafana/pull/11145)
+* **OAuth**: Only use jwt token if it contains an email address [#11127](https://github.com/grafana/grafana/pull/11127)
# 5.0.0-stable (2018-03-01)
diff --git a/README.md b/README.md
index 81fb1f8d42b..9db746cc5ea 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
### Building the backend
```bash
go get github.com/grafana/grafana
-cd ~/go/src/github.com/grafana/grafana
+cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup
go run build.go build
```
diff --git a/ROADMAP.md b/ROADMAP.md
index c8dc3186c73..67d7093263d 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -8,7 +8,6 @@ But it will give you an idea of our current vision and plan.
- v5.1
- Crossplatform builds & build speed improvements
- Enterprise LDAP
- - New template interpolation syntax
- Provisioning workflow
- First login registration view
- IFQL Initial support
diff --git a/circle.yml b/circle.yml
index bf013e3f5b1..b429911ceb8 100644
--- a/circle.yml
+++ b/circle.yml
@@ -9,7 +9,7 @@ machine:
GOPATH: "/home/ubuntu/.go_workspace"
ORG_PATH: "github.com/grafana"
REPO_PATH: "${ORG_PATH}/grafana"
- GODIST: "go1.9.3.linux-amd64.tar.gz"
+ GODIST: "go1.10.linux-amd64.tar.gz"
post:
- mkdir -p ~/download
- mkdir -p ~/docker
diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md
index 140053ec541..135973df52a 100644
--- a/docs/sources/administration/provisioning.md
+++ b/docs/sources/administration/provisioning.md
@@ -144,7 +144,7 @@ Since not all datasources have the same configuration settings we only have the
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
| graphiteVersion | string | Graphite | Graphite version |
| timeInterval | string | Elastic, Influxdb & Prometheus | Lowest interval/step value that should be used for this data source |
-| esVersion | string | Elastic | Elasticsearch version |
+| esVersion | string | Elastic | Elasticsearch version as an number (2/5/56) |
| timeField | string | Elastic | Which field that should be used as timestamp |
| interval | string | Elastic | Index date time format |
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md
index bd5b95da856..9bbbd70641d 100644
--- a/docs/sources/alerting/rules.md
+++ b/docs/sources/alerting/rules.md
@@ -59,7 +59,7 @@ avg() OF query(A, 5m, now) IS BELOW 14
```
- `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function.
-- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes from now to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes from now to 2 minutes from now. This is useful if you want to ignore the last 2 minutes of data.
+- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
- `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold.
The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 5beecefab88..dbfe1414246 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -74,7 +74,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
if setting.DisableGravatar {
- data.User.GravatarUrl = setting.AppSubUrl + "/public/img/transparent.png"
+ data.User.GravatarUrl = setting.AppSubUrl + "/public/img/user_profile.png"
}
if len(data.User.Name) == 0 {
diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go
index a609824cbc8..5206c81642e 100644
--- a/pkg/services/alerting/extractor.go
+++ b/pkg/services/alerting/extractor.go
@@ -143,10 +143,15 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
// validate
_, err = NewRuleFromDBAlert(alert)
- if err == nil && alert.ValidToSave() {
+ if err != nil {
+ return nil, err
+ }
+
+ if alert.ValidToSave() {
alerts = append(alerts, alert)
} else {
- return nil, err
+ e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
+ return nil, m.ErrDashboardContainsInvalidAlertData
}
}
diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go
index 71f3026025d..f8b678e66bd 100644
--- a/pkg/services/alerting/extractor_test.go
+++ b/pkg/services/alerting/extractor_test.go
@@ -150,6 +150,22 @@ func TestAlertRuleExtraction(t *testing.T) {
})
})
+ Convey("Panel with id set to zero should return error", func() {
+ panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
+ So(err, ShouldBeNil)
+
+ dashJson, err := simplejson.NewJson([]byte(panelWithIdZero))
+ So(err, ShouldBeNil)
+ dash := m.NewDashboardFromJson(dashJson)
+ extractor := NewDashAlertExtractor(dash, 1)
+
+ _, err = extractor.GetAlerts()
+
+ Convey("panel with id 0 should return error", func() {
+ So(err, ShouldNotBeNil)
+ })
+ })
+
Convey("Parse alerts from dashboard without rows", func() {
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
So(err, ShouldBeNil)
diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go
index 82be9d1df4e..863b4f1c286 100644
--- a/pkg/services/alerting/notifiers/opsgenie.go
+++ b/pkg/services/alerting/notifiers/opsgenie.go
@@ -99,11 +99,16 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
return err
}
+ customData := "Triggered metrics:\n\n"
+ for _, evt := range evalContext.EvalMatches {
+ customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
+ }
+
bodyJSON := simplejson.New()
bodyJSON.Set("message", evalContext.Rule.Name)
bodyJSON.Set("source", "Grafana")
bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
- bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message))
+ bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message, customData))
details := simplejson.New()
details.Set("url", ruleUrl)
diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go
index 0ca1ad3dfe0..88100afe7a1 100644
--- a/pkg/services/alerting/notifiers/telegram.go
+++ b/pkg/services/alerting/notifiers/telegram.go
@@ -12,6 +12,10 @@ import (
"os"
)
+const (
+ captionLengthLimit = 200
+)
+
var (
telegramApiUrl string = "https://api.telegram.org/bot%s/%s"
)
@@ -82,88 +86,81 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}
func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync {
- var imageFile *os.File
- var err error
-
if sendImageInline {
- imageFile, err = os.Open(evalContext.ImageOnDiskPath)
- defer imageFile.Close()
- if err != nil {
- sendImageInline = false // fall back to text message
+ cmd, err := this.buildMessageInlineImage(evalContext)
+ if err == nil {
+ return cmd
+ } else {
+ this.log.Error("Could not generate Telegram message with inline image.", "err", err)
}
}
- message := ""
+ return this.buildMessageLinkedImage(evalContext)
+}
- if sendImageInline {
- // Telegram's API does not allow HTML formatting for image captions.
- message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
- } else {
- message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
- }
+func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *m.SendWebhookSync {
+ message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
ruleUrl, err := evalContext.GetRuleUrl()
if err == nil {
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
}
- if !sendImageInline {
- // only attach this if we are not sending it inline.
- if evalContext.ImagePublicUrl != "" {
- message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
- }
- }
-
- metrics := ""
- fieldLimitCount := 4
- for index, evt := range evalContext.EvalMatches {
- metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
- if index > fieldLimitCount {
- break
- }
+ if evalContext.ImagePublicUrl != "" {
+ message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
}
+ metrics := generateMetricsMessage(evalContext)
if metrics != "" {
- if sendImageInline {
- // Telegram's API does not allow HTML formatting for image captions.
- message = message + fmt.Sprintf("\nMetrics:%s", metrics)
- } else {
- message = message + fmt.Sprintf("\nMetrics:%s", metrics)
- }
+ message = message + fmt.Sprintf("\nMetrics:%s", metrics)
}
- var body bytes.Buffer
+ cmd := this.generateTelegramCmd(message, "text", "sendMessage", func(w *multipart.Writer) {
+ fw, _ := w.CreateFormField("parse_mode")
+ fw.Write([]byte("html"))
+ })
+ return cmd
+}
+func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*m.SendWebhookSync, error) {
+ var imageFile *os.File
+ var err error
+
+ imageFile, err = os.Open(evalContext.ImageOnDiskPath)
+ defer imageFile.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ ruleUrl, err := evalContext.GetRuleUrl()
+
+ metrics := generateMetricsMessage(evalContext)
+ message := generateImageCaption(evalContext, ruleUrl, metrics)
+
+ cmd := this.generateTelegramCmd(message, "caption", "sendPhoto", func(w *multipart.Writer) {
+ fw, _ := w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
+ io.Copy(fw, imageFile)
+ })
+ return cmd, nil
+}
+
+func (this *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) *m.SendWebhookSync {
+ var body bytes.Buffer
w := multipart.NewWriter(&body)
+
fw, _ := w.CreateFormField("chat_id")
fw.Write([]byte(this.ChatID))
- if sendImageInline {
- fw, _ = w.CreateFormField("caption")
- fw.Write([]byte(message))
+ fw, _ = w.CreateFormField(messageField)
+ fw.Write([]byte(message))
- fw, _ = w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
- io.Copy(fw, imageFile)
- } else {
- fw, _ = w.CreateFormField("text")
- fw.Write([]byte(message))
-
- fw, _ = w.CreateFormField("parse_mode")
- fw.Write([]byte("html"))
- }
+ extraConf(w)
w.Close()
- apiMethod := ""
- if sendImageInline {
- this.log.Info("Sending telegram image notification", "photo", evalContext.ImageOnDiskPath, "chat_id", this.ChatID, "bot_token", this.BotToken)
- apiMethod = "sendPhoto"
- } else {
- this.log.Info("Sending telegram text notification", "chat_id", this.ChatID, "bot_token", this.BotToken)
- apiMethod = "sendMessage"
- }
+ this.log.Info("Sending telegram notification", "chat_id", this.ChatID, "bot_token", this.BotToken, "apiAction", apiAction)
+ url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiAction)
- url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiMethod)
cmd := &m.SendWebhookSync{
Url: url,
Body: body.String(),
@@ -175,6 +172,50 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se
return cmd
}
+func generateMetricsMessage(evalContext *alerting.EvalContext) string {
+ metrics := ""
+ fieldLimitCount := 4
+ for index, evt := range evalContext.EvalMatches {
+ metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
+ if index > fieldLimitCount {
+ break
+ }
+ }
+ return metrics
+}
+
+func generateImageCaption(evalContext *alerting.EvalContext, ruleUrl string, metrics string) string {
+ message := evalContext.GetNotificationTitle()
+
+ if len(evalContext.Rule.Message) > 0 {
+ message = fmt.Sprintf("%s\nMessage: %s", message, evalContext.Rule.Message)
+ }
+
+ if len(message) > captionLengthLimit {
+ message = message[0:captionLengthLimit]
+
+ }
+
+ if len(ruleUrl) > 0 {
+ urlLine := fmt.Sprintf("\nURL: %s", ruleUrl)
+ message = appendIfPossible(message, urlLine, captionLengthLimit)
+ }
+
+ if metrics != "" {
+ metricsLines := fmt.Sprintf("\n\nMetrics:%s", metrics)
+ message = appendIfPossible(message, metricsLines, captionLengthLimit)
+ }
+
+ return message
+}
+func appendIfPossible(message string, extra string, sizeLimit int) string {
+ if len(extra)+len(message) <= sizeLimit {
+ return message + extra
+ }
+ log.Debug("Line too long for image caption.", "value", extra)
+ return message
+}
+
func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go
index 3e8066e273b..05be787dced 100644
--- a/pkg/services/alerting/notifiers/telegram_test.go
+++ b/pkg/services/alerting/notifiers/telegram_test.go
@@ -5,6 +5,7 @@ import (
"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"
)
@@ -50,6 +51,71 @@ func TestTelegramNotifier(t *testing.T) {
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
})
+ Convey("generateCaption should generate a message with all pertinent details", func() {
+ evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
+ Name: "This is an alarm",
+ Message: "Some kind of message.",
+ State: m.AlertStateOK,
+ })
+
+ caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
+ So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+ So(caption, ShouldContainSubstring, "Some kind of message.")
+ So(caption, ShouldContainSubstring, "[OK] This is an alarm")
+ So(caption, ShouldContainSubstring, "http://grafa.url/abcdef")
+ })
+
+ Convey("When generating a message", func() {
+
+ Convey("URL should be skipped if it's too long", func() {
+ evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
+ Name: "This is an alarm",
+ Message: "Some kind of message.",
+ State: m.AlertStateOK,
+ })
+
+ caption := generateImageCaption(evalContext,
+ "http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "foo bar")
+ So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+ So(caption, ShouldContainSubstring, "Some kind of message.")
+ So(caption, ShouldContainSubstring, "[OK] This is an alarm")
+ So(caption, ShouldContainSubstring, "foo bar")
+ So(caption, ShouldNotContainSubstring, "http")
+ })
+
+ Convey("Message should be trimmed if it's too long", func() {
+ evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
+ Name: "This is an alarm",
+ Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
+ State: m.AlertStateOK,
+ })
+
+ caption := generateImageCaption(evalContext,
+ "http://grafa.url/foo",
+ "")
+ So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+ So(caption, ShouldContainSubstring, "[OK] This is an alarm")
+ So(caption, ShouldNotContainSubstring, "http")
+ So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
+ })
+
+ Convey("Metrics should be skipped if they dont fit", func() {
+ evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
+ Name: "This is an alarm",
+ Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
+ State: m.AlertStateOK,
+ })
+
+ caption := generateImageCaption(evalContext,
+ "http://grafa.url/foo",
+ "foo bar long song")
+ So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+ So(caption, ShouldContainSubstring, "[OK] This is an alarm")
+ So(caption, ShouldNotContainSubstring, "http")
+ So(caption, ShouldNotContainSubstring, "foo bar")
+ })
+ })
})
})
}
diff --git a/pkg/services/alerting/test-data/panel-with-id-0.json b/pkg/services/alerting/test-data/panel-with-id-0.json
new file mode 100644
index 00000000000..d1f314a4f55
--- /dev/null
+++ b/pkg/services/alerting/test-data/panel-with-id-0.json
@@ -0,0 +1,63 @@
+{
+ "id": 57,
+ "title": "Graphite 4",
+ "originalTitle": "Graphite 4",
+ "tags": ["graphite"],
+ "rows": [
+ {
+ "panels": [
+ {
+ "title": "Active desktop users",
+ "id": 0,
+ "editable": true,
+ "type": "graph",
+ "targets": [
+ {
+ "refId": "A",
+ "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
+ }
+ ],
+ "datasource": null,
+ "alert": {
+ "name": "name1",
+ "message": "desc1",
+ "handler": 1,
+ "frequency": "60s",
+ "conditions": [
+ {
+ "type": "query",
+ "query": {"params": ["A", "5m", "now"]},
+ "reducer": {"type": "avg", "params": []},
+ "evaluator": {"type": ">", "params": [100]}
+ }
+ ]
+ }
+ },
+ {
+ "title": "Active mobile users",
+ "id": 4,
+ "targets": [
+ {"refId": "A", "target": ""},
+ {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
+ ],
+ "datasource": "graphite2",
+ "alert": {
+ "name": "name2",
+ "message": "desc2",
+ "handler": 0,
+ "frequency": "60s",
+ "severity": "warning",
+ "conditions": [
+ {
+ "type": "query",
+ "query": {"params": ["B", "5m", "now"]},
+ "reducer": {"type": "avg", "params": []},
+ "evaluator": {"type": ">", "params": [100]}
+ }
+ ]
+ }
+ }
+ ]
+ }
+]
+ }
diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go
index 8c751f0cada..6342496ed26 100644
--- a/pkg/services/sqlstore/alert.go
+++ b/pkg/services/sqlstore/alert.go
@@ -10,6 +10,9 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
+// timeNow makes it possible to test usage of time
+var timeNow = time.Now
+
func init() {
bus.AddHandler("sql", SaveAlerts)
bus.AddHandler("sql", HandleAlertsQuery)
@@ -147,7 +150,7 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error {
return err
}
- if err := upsertAlerts(existingAlerts, cmd, sess); err != nil {
+ if err := updateAlerts(existingAlerts, cmd, sess); err != nil {
return err
}
@@ -159,7 +162,7 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error {
})
}
-func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBSession) error {
+func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBSession) error {
for _, alert := range cmd.Alerts {
update := false
var alertToUpdate *m.Alert
@@ -175,7 +178,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
if update {
if alertToUpdate.ContainsUpdates(alert) {
- alert.Updated = time.Now()
+ alert.Updated = timeNow()
alert.State = alertToUpdate.State
sess.MustCols("message")
_, err := sess.Id(alert.Id).Update(alert)
@@ -186,10 +189,10 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
sqlog.Debug("Alert updated", "name", alert.Name, "id", alert.Id)
}
} else {
- alert.Updated = time.Now()
- alert.Created = time.Now()
+ alert.Updated = timeNow()
+ alert.Created = timeNow()
alert.State = m.AlertStatePending
- alert.NewStateDate = time.Now()
+ alert.NewStateDate = timeNow()
_, err := sess.Insert(alert)
if err != nil {
@@ -253,7 +256,7 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
alert.State = cmd.State
alert.StateChanges += 1
- alert.NewStateDate = time.Now()
+ alert.NewStateDate = timeNow()
alert.EvalData = cmd.EvalData
if cmd.Error == "" {
@@ -276,11 +279,13 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
var buffer bytes.Buffer
params := make([]interface{}, 0)
- buffer.WriteString(`UPDATE alert SET state = ?`)
+ buffer.WriteString(`UPDATE alert SET state = ?, new_state_date = ?`)
if cmd.Paused {
params = append(params, string(m.AlertStatePaused))
+ params = append(params, timeNow())
} else {
params = append(params, string(m.AlertStatePending))
+ params = append(params, timeNow())
}
buffer.WriteString(` WHERE id IN (?` + strings.Repeat(",?", len(cmd.AlertIds)-1) + `)`)
@@ -306,7 +311,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error {
newState = string(m.AlertStatePending)
}
- res, err := sess.Exec(`UPDATE alert SET state = ?`, newState)
+ res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow())
if err != nil {
return err
}
diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go
index de86ae87a4f..296d16c2f45 100644
--- a/pkg/services/sqlstore/alert_test.go
+++ b/pkg/services/sqlstore/alert_test.go
@@ -6,9 +6,26 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
+ "time"
)
+func mockTimeNow() {
+ var timeSeed int64
+ timeNow = func() time.Time {
+ fakeNow := time.Unix(timeSeed, 0)
+ timeSeed += 1
+ return fakeNow
+ }
+}
+
+func resetTimeNow() {
+ timeNow = time.Now
+}
+
func TestAlertingDataAccess(t *testing.T) {
+ mockTimeNow()
+ defer resetTimeNow()
+
Convey("Testing Alerting data access", t, func() {
InitTestDB(t)
@@ -50,13 +67,11 @@ func TestAlertingDataAccess(t *testing.T) {
So(err, ShouldBeNil)
})
- Convey("can pause alert", func() {
- cmd := &m.PauseAllAlertCommand{
- Paused: true,
- }
+ alert, _ := getAlertById(1)
+ stateDateBeforePause := alert.NewStateDate
- err = PauseAllAlerts(cmd)
- So(err, ShouldBeNil)
+ Convey("can pause all alerts", func() {
+ pauseAllAlerts(true)
Convey("cannot updated paused alert", func() {
cmd := &m.SetAlertStateCommand{
@@ -67,6 +82,19 @@ func TestAlertingDataAccess(t *testing.T) {
err = SetAlertState(cmd)
So(err, ShouldNotBeNil)
})
+
+ Convey("pausing alerts should update their NewStateDate", func() {
+ alert, _ = getAlertById(1)
+ stateDateAfterPause := alert.NewStateDate
+ So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause)
+ })
+
+ Convey("unpausing alerts should update their NewStateDate again", func() {
+ pauseAllAlerts(false)
+ alert, _ = getAlertById(1)
+ stateDateAfterUnpause := alert.NewStateDate
+ So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterUnpause)
+ })
})
})
@@ -214,3 +242,90 @@ func TestAlertingDataAccess(t *testing.T) {
})
})
}
+
+func TestPausingAlerts(t *testing.T) {
+ mockTimeNow()
+ defer resetTimeNow()
+
+ Convey("Given an alert", t, func() {
+ InitTestDB(t)
+
+ testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
+ alert, _ := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New())
+
+ stateDateBeforePause := alert.NewStateDate
+ stateDateAfterPause := stateDateBeforePause
+ Convey("when paused", func() {
+ pauseAlert(testDash.OrgId, 1, true)
+
+ Convey("the NewStateDate should be updated", func() {
+ alert, _ := getAlertById(1)
+
+ stateDateAfterPause = alert.NewStateDate
+ So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause)
+ })
+ })
+
+ Convey("when unpaused", func() {
+ pauseAlert(testDash.OrgId, 1, false)
+
+ Convey("the NewStateDate should be updated again", func() {
+ alert, _ := getAlertById(1)
+
+ stateDateAfterUnpause := alert.NewStateDate
+ So(stateDateAfterPause, ShouldHappenBefore, stateDateAfterUnpause)
+ })
+ })
+ })
+}
+func pauseAlert(orgId int64, alertId int64, pauseState bool) (int64, error) {
+ cmd := &m.PauseAlertCommand{
+ OrgId: orgId,
+ AlertIds: []int64{alertId},
+ Paused: pauseState,
+ }
+ err := PauseAlert(cmd)
+ So(err, ShouldBeNil)
+ return cmd.ResultCount, err
+}
+func insertTestAlert(title string, message string, orgId int64, dashId int64, settings *simplejson.Json) (*m.Alert, error) {
+ items := []*m.Alert{
+ {
+ PanelId: 1,
+ DashboardId: dashId,
+ OrgId: orgId,
+ Name: title,
+ Message: message,
+ Settings: settings,
+ Frequency: 1,
+ },
+ }
+
+ cmd := m.SaveAlertsCommand{
+ Alerts: items,
+ DashboardId: dashId,
+ OrgId: orgId,
+ UserId: 1,
+ }
+
+ err := SaveAlerts(&cmd)
+ return cmd.Alerts[0], err
+}
+
+func getAlertById(id int64) (*m.Alert, error) {
+ q := &m.GetAlertByIdQuery{
+ Id: id,
+ }
+ err := GetAlertById(q)
+ So(err, ShouldBeNil)
+ return q.Result, err
+}
+
+func pauseAllAlerts(pauseState bool) error {
+ cmd := &m.PauseAllAlertCommand{
+ Paused: pauseState,
+ }
+ err := PauseAllAlerts(cmd)
+ So(err, ShouldBeNil)
+ return err
+}
diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go
index 40d6cf5bcb2..ea8f1216706 100644
--- a/pkg/services/sqlstore/dashboard_folder_test.go
+++ b/pkg/services/sqlstore/dashboard_folder_test.go
@@ -3,7 +3,6 @@ package sqlstore
import (
"testing"
- "github.com/go-xorm/xorm"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
@@ -11,10 +10,8 @@ import (
)
func TestDashboardFolderDataAccess(t *testing.T) {
- var x *xorm.Engine
-
Convey("Testing DB", t, func() {
- x = InitTestDB(t)
+ InitTestDB(t)
Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() {
folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go
index e0a73b9a49a..9124a686236 100644
--- a/pkg/services/sqlstore/dashboard_test.go
+++ b/pkg/services/sqlstore/dashboard_test.go
@@ -5,7 +5,6 @@ import (
"testing"
"time"
- "github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
@@ -15,10 +14,8 @@ import (
)
func TestDashboardDataAccess(t *testing.T) {
- var x *xorm.Engine
-
Convey("Testing DB", t, func() {
- x = InitTestDB(t)
+ InitTestDB(t)
Convey("Given saved dashboard", func() {
savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
diff --git a/pkg/social/generic_oauth.go b/pkg/social/generic_oauth.go
index 2411e0006a2..b92d64ad9fc 100644
--- a/pkg/social/generic_oauth.go
+++ b/pkg/social/generic_oauth.go
@@ -180,6 +180,7 @@ type UserInfoJson struct {
func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
var data UserInfoJson
+ var err error
if s.extractToken(&data, token) != true {
response, err := HttpGet(client, s.apiUrl)
@@ -193,20 +194,17 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
}
}
- name, err := s.extractName(data)
- if err != nil {
- return nil, err
+ name := s.extractName(&data)
+
+ email := s.extractEmail(&data)
+ if email == "" {
+ email, err = s.FetchPrivateEmail(client)
+ if err != nil {
+ return nil, err
+ }
}
- email, err := s.extractEmail(data, client)
- if err != nil {
- return nil, err
- }
-
- login, err := s.extractLogin(data, email)
- if err != nil {
- return nil, err
- }
+ login := s.extractLogin(&data, email)
userInfo := &BasicUserInfo{
Name: name,
@@ -251,49 +249,55 @@ func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Toke
return false
}
+ email := s.extractEmail(data)
+ if email == "" {
+ s.log.Debug("No email found in id_token", "json", string(payload), "data", data)
+ return false
+ }
+
s.log.Debug("Received id_token", "json", string(payload), "data", data)
return true
}
-func (s *SocialGenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (string, error) {
+func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
if data.Email != "" {
- return data.Email, nil
+ return data.Email
}
if data.Attributes["email:primary"] != nil {
- return data.Attributes["email:primary"][0], nil
+ return data.Attributes["email:primary"][0]
}
if data.Upn != "" {
emailAddr, emailErr := mail.ParseAddress(data.Upn)
if emailErr == nil {
- return emailAddr.Address, nil
+ return emailAddr.Address
}
}
- return s.FetchPrivateEmail(client)
+ return ""
}
-func (s *SocialGenericOAuth) extractLogin(data UserInfoJson, email string) (string, error) {
+func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson, email string) string {
if data.Login != "" {
- return data.Login, nil
+ return data.Login
}
if data.Username != "" {
- return data.Username, nil
+ return data.Username
}
- return email, nil
+ return email
}
-func (s *SocialGenericOAuth) extractName(data UserInfoJson) (string, error) {
+func (s *SocialGenericOAuth) extractName(data *UserInfoJson) string {
if data.Name != "" {
- return data.Name, nil
+ return data.Name
}
if data.DisplayName != "" {
- return data.DisplayName, nil
+ return data.DisplayName
}
- return "", nil
+ return ""
}
diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go
index 29daa0c3bb4..692b891eddd 100644
--- a/pkg/tsdb/opentsdb/opentsdb.go
+++ b/pkg/tsdb/opentsdb/opentsdb.go
@@ -22,23 +22,10 @@ import (
)
type OpenTsdbExecutor struct {
- //*models.DataSource
- //httpClient *http.Client
}
func NewOpenTsdbExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
- /*
- httpClient, err := datasource.GetHttpClient()
-
- if err != nil {
- return nil, err
- }
- */
-
- return &OpenTsdbExecutor{
- //DataSource: datasource,
- //httpClient: httpClient,
- }, nil
+ return &OpenTsdbExecutor{}, nil
}
var (
diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts
index 720334d8973..fbf5fd6cd37 100644
--- a/public/app/core/components/scroll/scroll.ts
+++ b/public/app/core/components/scroll/scroll.ts
@@ -8,6 +8,7 @@ export function geminiScrollbar() {
link: function(scope, elem, attrs) {
let scrollbar = new PerfectScrollbar(elem[0], {
wheelPropagation: true,
+ wheelSpeed: 3,
});
let lastPos = 0;
diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html
index e97d34739ba..1b301363e62 100644
--- a/public/app/core/components/sidemenu/sidemenu.html
+++ b/public/app/core/components/sidemenu/sidemenu.html
@@ -1,73 +1,78 @@
-
+
-
+