`,
})
}
-func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+func newDiscordNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
+ content := model.Settings.Get("content").MustString()
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"}
@@ -40,29 +52,38 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return &DiscordNotifier{
NotifierBase: NewNotifierBase(model),
+ Content: content,
WebhookURL: url,
log: log.New("alerting.notifier.discord"),
}, nil
}
+// DiscordNotifier is responsible for sending alert
+// notifications to discord.
type DiscordNotifier struct {
NotifierBase
+ Content string
WebhookURL string
log log.Logger
}
-func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL)
+// Notify send an alert notification to Discord.
+func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
+ dn.log.Info("Sending alert notification to", "webhook_url", dn.WebhookURL)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ dn.log.Error("Failed get rule link", "error", err)
return err
}
bodyJSON := simplejson.New()
bodyJSON.Set("username", "Grafana")
+ if dn.Content != "" {
+ bodyJSON.Set("content", dn.Content)
+ }
+
fields := make([]map[string]interface{}, 0)
for _, evt := range evalContext.EvalMatches {
@@ -85,7 +106,7 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
embed.Set("title", evalContext.GetNotificationTitle())
//Discord takes integer for color
embed.Set("color", color)
- embed.Set("url", ruleUrl)
+ embed.Set("url", ruleURL)
embed.Set("description", evalContext.Rule.Message)
embed.Set("type", "rich")
embed.Set("fields", fields)
@@ -94,9 +115,9 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
var image map[string]interface{}
var embeddedImage = false
- if evalContext.ImagePublicUrl != "" {
+ if evalContext.ImagePublicURL != "" {
image = map[string]interface{}{
- "url": evalContext.ImagePublicUrl,
+ "url": evalContext.ImagePublicURL,
}
embed.Set("image", image)
} else {
@@ -111,8 +132,8 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
json, _ := bodyJSON.MarshalJSON()
- cmd := &m.SendWebhookSync{
- Url: this.WebhookURL,
+ cmd := &models.SendWebhookSync{
+ Url: dn.WebhookURL,
HttpMethod: "POST",
ContentType: "application/json",
}
@@ -120,22 +141,22 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
if !embeddedImage {
cmd.Body = string(json)
} else {
- err := this.embedImage(cmd, evalContext.ImageOnDiskPath, json)
+ err := dn.embedImage(cmd, evalContext.ImageOnDiskPath, json)
if err != nil {
- this.log.Error("failed to embed image", "error", err)
+ dn.log.Error("failed to embed image", "error", err)
return err
}
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send notification to Discord", "error", err)
+ dn.log.Error("Failed to send notification to Discord", "error", err)
return err
}
return nil
}
-func (this *DiscordNotifier) embedImage(cmd *m.SendWebhookSync, imagePath string, existingJSONBody []byte) error {
+func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath string, existingJSONBody []byte) error {
f, err := os.Open(imagePath)
defer f.Close()
if err != nil {
@@ -171,7 +192,7 @@ func (this *DiscordNotifier) embedImage(cmd *m.SendWebhookSync, imagePath string
w.Close()
- cmd.Body = string(b.Bytes())
+ cmd.Body = b.String()
cmd.ContentType = w.FormDataContentType()
return nil
diff --git a/pkg/services/alerting/notifiers/discord_test.go b/pkg/services/alerting/notifiers/discord_test.go
index fe925aab362e..d1cbff6b859a 100644
--- a/pkg/services/alerting/notifiers/discord_test.go
+++ b/pkg/services/alerting/notifiers/discord_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,35 +16,37 @@ func TestDiscordNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "discord_testing",
Type: "discord",
Settings: settingsJSON,
}
- _, err := NewDiscordNotifier(model)
+ _, err := newDiscordNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("settings should trigger incident", func() {
json := `
{
- "url": "https://web.hook/"
+ "content": "@everyone Please check this notification",
+ "url": "https://web.hook/"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "discord_testing",
Type: "discord",
Settings: settingsJSON,
}
- not, err := NewDiscordNotifier(model)
+ not, err := newDiscordNotifier(model)
discordNotifier := not.(*DiscordNotifier)
So(err, ShouldBeNil)
So(discordNotifier.Name, ShouldEqual, "discord_testing")
So(discordNotifier.Type, ShouldEqual, "discord")
+ So(discordNotifier.Content, ShouldEqual, "@everyone Please check this notification")
So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/")
})
})
diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go
index 6c7bf5edb297..5d3422e608b5 100644
--- a/pkg/services/alerting/notifiers/email.go
+++ b/pkg/services/alerting/notifiers/email.go
@@ -5,8 +5,9 @@ import (
"strings"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
+
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/setting"
)
@@ -29,13 +30,17 @@ func init() {
})
}
+// EmailNotifier is responsible for sending
+// alert notifications over email.
type EmailNotifier struct {
NotifierBase
Addresses []string
log log.Logger
}
-func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewEmailNotifier is the constructor function
+// for the EmailNotifier.
+func NewEmailNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
addressesString := model.Settings.Get("addresses").MustString()
if addressesString == "" {
@@ -58,12 +63,13 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
-func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Sending alert notification to", "addresses", this.Addresses)
+// Notify sends the alert notification.
+func (en *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
+ en.log.Info("Sending alert notification to", "addresses", en.Addresses)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ en.log.Error("Failed get rule link", "error", err)
return err
}
@@ -72,8 +78,8 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
error = evalContext.Error.Error()
}
- cmd := &m.SendEmailCommandSync{
- SendEmailCommand: m.SendEmailCommand{
+ cmd := &models.SendEmailCommandSync{
+ SendEmailCommand: models.SendEmailCommand{
Subject: evalContext.GetNotificationTitle(),
Data: map[string]interface{}{
"Title": evalContext.GetNotificationTitle(),
@@ -82,20 +88,20 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
"StateModel": evalContext.GetStateModel(),
"Message": evalContext.Rule.Message,
"Error": error,
- "RuleUrl": ruleUrl,
+ "RuleUrl": ruleURL,
"ImageLink": "",
"EmbeddedImage": "",
"AlertPageUrl": setting.AppUrl + "alerting",
"EvalMatches": evalContext.EvalMatches,
},
- To: this.Addresses,
+ To: en.Addresses,
Template: "alert_notification.html",
EmbededFiles: []string{},
},
}
- if evalContext.ImagePublicUrl != "" {
- cmd.Data["ImageLink"] = evalContext.ImagePublicUrl
+ if evalContext.ImagePublicURL != "" {
+ cmd.Data["ImageLink"] = evalContext.ImagePublicURL
} else {
file, err := os.Stat(evalContext.ImageOnDiskPath)
if err == nil {
@@ -107,9 +113,9 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
err = bus.DispatchCtx(evalContext.Ctx, cmd)
if err != nil {
- this.log.Error("Failed to send alert notification email", "error", err)
+ en.log.Error("Failed to send alert notification email", "error", err)
return err
}
- return nil
+ return nil
}
diff --git a/pkg/services/alerting/notifiers/email_test.go b/pkg/services/alerting/notifiers/email_test.go
index 9750cbc2833c..b0c57f2f254d 100644
--- a/pkg/services/alerting/notifiers/email_test.go
+++ b/pkg/services/alerting/notifiers/email_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestEmailNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
@@ -33,7 +33,7 @@ func TestEmailNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
@@ -57,7 +57,7 @@ func TestEmailNotifier(t *testing.T) {
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
diff --git a/pkg/services/alerting/notifiers/googlechat.go b/pkg/services/alerting/notifiers/googlechat.go
index 1aba15a79282..2d81787fb916 100644
--- a/pkg/services/alerting/notifiers/googlechat.go
+++ b/pkg/services/alerting/notifiers/googlechat.go
@@ -6,8 +6,8 @@ import (
"time"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/setting"
)
@@ -18,7 +18,7 @@ func init() {
Name: "Google Hangouts Chat",
Description: "Sends notifications to Google Hangouts Chat via webhooks based on the official JSON message " +
"format (https://developers.google.com/hangouts/chat/reference/message-formats/).",
- Factory: NewGoogleChatNotifier,
+ Factory: newGoogleChatNotifier,
OptionsTemplate: `
@@ -29,7 +29,7 @@ func init() {
})
}
-func NewGoogleChatNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+func newGoogleChatNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
@@ -37,14 +37,16 @@ func NewGoogleChatNotifier(model *m.AlertNotification) (alerting.Notifier, error
return &GoogleChatNotifier{
NotifierBase: NewNotifierBase(model),
- Url: url,
+ URL: url,
log: log.New("alerting.notifier.googlechat"),
}, nil
}
+// GoogleChatNotifier is responsible for sending
+// alert notifications to Google chat.
type GoogleChatNotifier struct {
NotifierBase
- Url string
+ URL string
log log.Logger
}
@@ -90,7 +92,7 @@ type imageWidget struct {
}
type image struct {
- ImageUrl string `json:"imageUrl"`
+ ImageURL string `json:"imageUrl"`
}
type button struct {
@@ -107,19 +109,20 @@ type onClick struct {
}
type openLink struct {
- Url string `json:"url"`
+ URL string `json:"url"`
}
-func (this *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Executing Google Chat notification")
+// Notify send an alert notification to Google Chat.
+func (gcn *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error {
+ gcn.log.Info("Executing Google Chat notification")
headers := map[string]string{
"Content-Type": "application/json; charset=UTF-8",
}
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("evalContext returned an invalid rule URL")
+ gcn.log.Error("evalContext returned an invalid rule URL")
}
// add a text paragraph widget for the message
@@ -149,14 +152,14 @@ func (this *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error
widgets = append(widgets, fields)
// if an image exists, add it as an image widget
- if evalContext.ImagePublicUrl != "" {
+ if evalContext.ImagePublicURL != "" {
widgets = append(widgets, imageWidget{
Image: image{
- ImageUrl: evalContext.ImagePublicUrl,
+ ImageURL: evalContext.ImagePublicURL,
},
})
} else {
- this.log.Info("Could not retrieve a public image URL.")
+ gcn.log.Info("Could not retrieve a public image URL.")
}
// add a button widget (link to Grafana)
@@ -167,7 +170,7 @@ func (this *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error
Text: "OPEN IN GRAFANA",
OnClick: onClick{
OpenLink: openLink{
- Url: ruleUrl,
+ URL: ruleURL,
},
},
},
@@ -199,15 +202,15 @@ func (this *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error
}
body, _ := json.Marshal(res1D)
- cmd := &m.SendWebhookSync{
- Url: this.Url,
+ cmd := &models.SendWebhookSync{
+ Url: gcn.URL,
HttpMethod: "POST",
HttpHeader: headers,
Body: string(body),
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", this.Name)
+ gcn.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", gcn.Name)
return err
}
diff --git a/pkg/services/alerting/notifiers/googlechat_test.go b/pkg/services/alerting/notifiers/googlechat_test.go
index 1fdce878926f..25dd5053397d 100644
--- a/pkg/services/alerting/notifiers/googlechat_test.go
+++ b/pkg/services/alerting/notifiers/googlechat_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,13 +16,13 @@ func TestGoogleChatNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "googlechat",
Settings: settingsJSON,
}
- _, err := NewGoogleChatNotifier(model)
+ _, err := newGoogleChatNotifier(model)
So(err, ShouldNotBeNil)
})
@@ -33,21 +33,20 @@ func TestGoogleChatNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "googlechat",
Settings: settingsJSON,
}
- not, err := NewGoogleChatNotifier(model)
+ not, err := newGoogleChatNotifier(model)
webhookNotifier := not.(*GoogleChatNotifier)
So(err, ShouldBeNil)
So(webhookNotifier.Name, ShouldEqual, "ops")
So(webhookNotifier.Type, ShouldEqual, "googlechat")
- So(webhookNotifier.Url, ShouldEqual, "http://google.com")
+ So(webhookNotifier.URL, ShouldEqual, "http://google.com")
})
-
})
})
}
diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go
index 388cec795976..2e8be00576bb 100644
--- a/pkg/services/alerting/notifiers/hipchat.go
+++ b/pkg/services/alerting/notifiers/hipchat.go
@@ -8,7 +8,7 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -46,6 +46,8 @@ const (
maxFieldCount int = 4
)
+// NewHipChatNotifier is the constructor functions
+// for the HipChatNotifier
func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if strings.HasSuffix(url, "/") {
@@ -56,31 +58,34 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err
}
apikey := model.Settings.Get("apikey").MustString()
- roomId := model.Settings.Get("roomid").MustString()
+ roomID := model.Settings.Get("roomid").MustString()
return &HipChatNotifier{
NotifierBase: NewNotifierBase(model),
- Url: url,
- ApiKey: apikey,
- RoomId: roomId,
+ URL: url,
+ APIKey: apikey,
+ RoomID: roomID,
log: log.New("alerting.notifier.hipchat"),
}, nil
}
+// HipChatNotifier is responsible for sending
+// alert notifications to Hipchat.
type HipChatNotifier struct {
NotifierBase
- Url string
- ApiKey string
- RoomId string
+ URL string
+ APIKey string
+ RoomID string
log log.Logger
}
-func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+// Notify sends an alert notification to HipChat
+func (hc *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
+ hc.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.ID, "notification", hc.Name)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ hc.log.Error("Failed get rule link", "error", err)
return err
}
@@ -133,7 +138,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
// Add a card with link to the dashboard
card := map[string]interface{}{
"style": "application",
- "url": ruleUrl,
+ "url": ruleURL,
"id": "1",
"title": evalContext.GetNotificationTitle(),
"description": message,
@@ -143,10 +148,10 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
"date": evalContext.EndTime.Unix(),
"attributes": attributes,
}
- if evalContext.ImagePublicUrl != "" {
+ if evalContext.ImagePublicURL != "" {
card["thumbnail"] = map[string]interface{}{
- "url": evalContext.ImagePublicUrl,
- "url@2x": evalContext.ImagePublicUrl,
+ "url": evalContext.ImagePublicURL,
+ "url@2x": evalContext.ImagePublicURL,
"width": 1193,
"height": 564,
}
@@ -160,13 +165,13 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
"card": card,
}
- hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey)
+ hipURL := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", hc.URL, hc.RoomID, hc.APIKey)
data, _ := json.Marshal(&body)
- this.log.Info("Request payload", "json", string(data))
- cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)}
+ hc.log.Info("Request payload", "json", string(data))
+ cmd := &models.SendWebhookSync{Url: hipURL, Body: string(data)}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send hipchat notification", "error", err, "webhook", this.Name)
+ hc.log.Error("Failed to send hipchat notification", "error", err, "webhook", hc.Name)
return err
}
diff --git a/pkg/services/alerting/notifiers/hipchat_test.go b/pkg/services/alerting/notifiers/hipchat_test.go
index 1597be12eb8b..6d9b38f83933 100644
--- a/pkg/services/alerting/notifiers/hipchat_test.go
+++ b/pkg/services/alerting/notifiers/hipchat_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestHipChatNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
@@ -33,7 +33,7 @@ func TestHipChatNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
@@ -45,9 +45,9 @@ func TestHipChatNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(hipchatNotifier.Name, ShouldEqual, "ops")
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
- So(hipchatNotifier.Url, ShouldEqual, "http://google.com")
- So(hipchatNotifier.ApiKey, ShouldEqual, "")
- So(hipchatNotifier.RoomId, ShouldEqual, "")
+ So(hipchatNotifier.URL, ShouldEqual, "http://google.com")
+ So(hipchatNotifier.APIKey, ShouldEqual, "")
+ So(hipchatNotifier.RoomID, ShouldEqual, "")
})
Convey("from settings with Recipient and Mention", func() {
@@ -59,7 +59,7 @@ func TestHipChatNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
@@ -71,11 +71,10 @@ func TestHipChatNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(hipchatNotifier.Name, ShouldEqual, "ops")
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
- So(hipchatNotifier.Url, ShouldEqual, "http://www.hipchat.com")
- So(hipchatNotifier.ApiKey, ShouldEqual, "1234")
- So(hipchatNotifier.RoomId, ShouldEqual, "1234")
+ So(hipchatNotifier.URL, ShouldEqual, "http://www.hipchat.com")
+ So(hipchatNotifier.APIKey, ShouldEqual, "1234")
+ So(hipchatNotifier.RoomID, ShouldEqual, "1234")
})
-
})
})
}
diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go
index a8a424c87a7c..ed795453c42a 100644
--- a/pkg/services/alerting/notifiers/kafka.go
+++ b/pkg/services/alerting/notifiers/kafka.go
@@ -7,8 +7,8 @@ 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"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -32,7 +32,8 @@ func init() {
})
}
-func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewKafkaNotifier is the constructor function for the Kafka notifier.
+func NewKafkaNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
endpoint := model.Settings.Get("kafkaRestProxy").MustString()
if endpoint == "" {
return nil, alerting.ValidationError{Reason: "Could not find kafka rest proxy endpoint property in settings"}
@@ -50,6 +51,8 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
+// KafkaNotifier is responsible for sending
+// alert notifications to Kafka.
type KafkaNotifier struct {
NotifierBase
Endpoint string
@@ -57,8 +60,8 @@ type KafkaNotifier struct {
log log.Logger
}
-func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
-
+// Notify sends the alert notification.
+func (kn *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
state := evalContext.Rule.State
customData := triggMetrString
@@ -66,7 +69,7 @@ func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
}
- this.log.Info("Notifying Kafka", "alert_state", state)
+ kn.log.Info("Notifying Kafka", "alert_state", state)
recordJSON := simplejson.New()
records := make([]interface{}, 1)
@@ -75,20 +78,20 @@ func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
bodyJSON.Set("client", "Grafana")
bodyJSON.Set("details", customData)
- bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
+ bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.ID, 10))
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ kn.log.Error("Failed get rule link", "error", err)
return err
}
- bodyJSON.Set("client_url", ruleUrl)
+ bodyJSON.Set("client_url", ruleURL)
- if evalContext.ImagePublicUrl != "" {
+ if evalContext.ImagePublicURL != "" {
contexts := make([]interface{}, 1)
imageJSON := simplejson.New()
imageJSON.Set("type", "image")
- imageJSON.Set("src", evalContext.ImagePublicUrl)
+ imageJSON.Set("src", evalContext.ImagePublicURL)
contexts[0] = imageJSON
bodyJSON.Set("contexts", contexts)
}
@@ -99,10 +102,10 @@ func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
recordJSON.Set("records", records)
body, _ := recordJSON.MarshalJSON()
- topicUrl := this.Endpoint + "/topics/" + this.Topic
+ topicURL := kn.Endpoint + "/topics/" + kn.Topic
- cmd := &m.SendWebhookSync{
- Url: topicUrl,
+ cmd := &models.SendWebhookSync{
+ Url: topicURL,
Body: string(body),
HttpMethod: "POST",
HttpHeader: map[string]string{
@@ -112,7 +115,7 @@ func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send notification to Kafka", "error", err, "body", string(body))
+ kn.log.Error("Failed to send notification to Kafka", "error", err, "body", string(body))
return err
}
diff --git a/pkg/services/alerting/notifiers/kafka_test.go b/pkg/services/alerting/notifiers/kafka_test.go
index 045976cb14ba..ba3b60ec23e9 100644
--- a/pkg/services/alerting/notifiers/kafka_test.go
+++ b/pkg/services/alerting/notifiers/kafka_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestKafkaNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "kafka_testing",
Type: "kafka",
Settings: settingsJSON,
@@ -34,7 +34,7 @@ func TestKafkaNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "kafka_testing",
Type: "kafka",
Settings: settingsJSON,
@@ -49,7 +49,6 @@ func TestKafkaNotifier(t *testing.T) {
So(kafkaNotifier.Endpoint, ShouldEqual, "http://localhost:8082")
So(kafkaNotifier.Topic, ShouldEqual, "topic1")
})
-
})
})
}
diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go
index 9e3888b8f954..2048495b6465 100644
--- a/pkg/services/alerting/notifiers/line.go
+++ b/pkg/services/alerting/notifiers/line.go
@@ -5,8 +5,8 @@ import (
"net/url"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -29,10 +29,11 @@ func init() {
}
const (
- lineNotifyUrl string = "https://notify-api.line.me/api/notify"
+ lineNotifyURL string = "https://notify-api.line.me/api/notify"
)
-func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewLINENotifier is the constructor for the LINE notifier
+func NewLINENotifier(model *models.AlertNotification) (alerting.Notifier, error) {
token := model.Settings.Get("token").MustString()
if token == "" {
return nil, alerting.ValidationError{Reason: "Could not find token in settings"}
@@ -45,52 +46,55 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
+// LineNotifier is responsible for sending
+// alert notifications to LINE.
type LineNotifier struct {
NotifierBase
Token string
log log.Logger
}
-func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+// Notify send an alert notification to LINE
+func (ln *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
+ ln.log.Info("Executing line notification", "ruleId", evalContext.Rule.ID, "notification", ln.Name)
var err error
switch evalContext.Rule.State {
- case m.AlertStateAlerting:
- err = this.createAlert(evalContext)
+ case models.AlertStateAlerting:
+ err = ln.createAlert(evalContext)
}
return err
}
-func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
- this.log.Info("Creating Line notify", "ruleId", evalContext.Rule.Id, "notification", this.Name)
- ruleUrl, err := evalContext.GetRuleUrl()
+func (ln *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
+ ln.log.Info("Creating Line notify", "ruleId", evalContext.Rule.ID, "notification", ln.Name)
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ ln.log.Error("Failed get rule link", "error", err)
return err
}
form := url.Values{}
- body := fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message)
+ body := fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleURL, evalContext.Rule.Message)
form.Add("message", body)
- if evalContext.ImagePublicUrl != "" {
- form.Add("imageThumbnail", evalContext.ImagePublicUrl)
- form.Add("imageFullsize", evalContext.ImagePublicUrl)
+ if evalContext.ImagePublicURL != "" {
+ form.Add("imageThumbnail", evalContext.ImagePublicURL)
+ form.Add("imageFullsize", evalContext.ImagePublicURL)
}
- cmd := &m.SendWebhookSync{
- Url: lineNotifyUrl,
+ cmd := &models.SendWebhookSync{
+ Url: lineNotifyURL,
HttpMethod: "POST",
HttpHeader: map[string]string{
- "Authorization": fmt.Sprintf("Bearer %s", this.Token),
+ "Authorization": fmt.Sprintf("Bearer %s", ln.Token),
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
Body: form.Encode(),
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send notification to LINE", "error", err, "body", body)
+ ln.log.Error("Failed to send notification to LINE", "error", err, "body", body)
return err
}
diff --git a/pkg/services/alerting/notifiers/line_test.go b/pkg/services/alerting/notifiers/line_test.go
index 6630665e9924..f8f50cc9b958 100644
--- a/pkg/services/alerting/notifiers/line_test.go
+++ b/pkg/services/alerting/notifiers/line_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -14,7 +14,7 @@ func TestLineNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "line_testing",
Type: "line",
Settings: settingsJSON,
@@ -30,7 +30,7 @@ func TestLineNotifier(t *testing.T) {
"token": "abcdefgh0123456789"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "line_testing",
Type: "line",
Settings: settingsJSON,
@@ -44,6 +44,5 @@ func TestLineNotifier(t *testing.T) {
So(lineNotifier.Type, ShouldEqual, "line")
So(lineNotifier.Token, ShouldEqual, "abcdefgh0123456789")
})
-
})
}
diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go
index 629968b51023..833927dee9f5 100644
--- a/pkg/services/alerting/notifiers/opsgenie.go
+++ b/pkg/services/alerting/notifiers/opsgenie.go
@@ -6,8 +6,8 @@ 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"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -44,54 +44,57 @@ var (
opsgenieAlertURL = "https://api.opsgenie.com/v2/alerts"
)
-func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewOpsGenieNotifier is the constructor for OpsGenie.
+func NewOpsGenieNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
autoClose := model.Settings.Get("autoClose").MustBool(true)
apiKey := model.Settings.Get("apiKey").MustString()
- apiUrl := model.Settings.Get("apiUrl").MustString()
+ apiURL := model.Settings.Get("apiUrl").MustString()
if apiKey == "" {
return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"}
}
- if apiUrl == "" {
- apiUrl = opsgenieAlertURL
+ if apiURL == "" {
+ apiURL = opsgenieAlertURL
}
return &OpsGenieNotifier{
NotifierBase: NewNotifierBase(model),
- ApiKey: apiKey,
- ApiUrl: apiUrl,
+ APIKey: apiKey,
+ APIUrl: apiURL,
AutoClose: autoClose,
log: log.New("alerting.notifier.opsgenie"),
}, nil
}
+// OpsGenieNotifier is responsible for sending
+// alert notifications to OpsGenie
type OpsGenieNotifier struct {
NotifierBase
- ApiKey string
- ApiUrl string
+ APIKey string
+ APIUrl string
AutoClose bool
log log.Logger
}
-func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
-
+// Notify sends an alert notification to OpsGenie.
+func (on *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
var err error
switch evalContext.Rule.State {
- case m.AlertStateOK:
- if this.AutoClose {
- err = this.closeAlert(evalContext)
+ case models.AlertStateOK:
+ if on.AutoClose {
+ err = on.closeAlert(evalContext)
}
- case m.AlertStateAlerting:
- err = this.createAlert(evalContext)
+ case models.AlertStateAlerting:
+ err = on.createAlert(evalContext)
}
return err
}
-func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error {
- this.log.Info("Creating OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+func (on *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error {
+ on.log.Info("Creating OpsGenie alert", "ruleId", evalContext.Rule.ID, "notification", on.Name)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ on.log.Error("Failed get rule link", "error", err)
return err
}
@@ -103,54 +106,54 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
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\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message, customData))
+ bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.ID, 10))
+ 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)
- if evalContext.ImagePublicUrl != "" {
- details.Set("image", evalContext.ImagePublicUrl)
+ details.Set("url", ruleURL)
+ if evalContext.ImagePublicURL != "" {
+ details.Set("image", evalContext.ImagePublicURL)
}
bodyJSON.Set("details", details)
body, _ := bodyJSON.MarshalJSON()
- cmd := &m.SendWebhookSync{
- Url: this.ApiUrl,
+ cmd := &models.SendWebhookSync{
+ Url: on.APIUrl,
Body: string(body),
HttpMethod: "POST",
HttpHeader: map[string]string{
"Content-Type": "application/json",
- "Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey),
+ "Authorization": fmt.Sprintf("GenieKey %s", on.APIKey),
},
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
+ on.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
}
return nil
}
-func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error {
- this.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+func (on *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error {
+ on.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.ID, "notification", on.Name)
bodyJSON := simplejson.New()
bodyJSON.Set("source", "Grafana")
body, _ := bodyJSON.MarshalJSON()
- cmd := &m.SendWebhookSync{
- Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", this.ApiUrl, evalContext.Rule.Id),
+ cmd := &models.SendWebhookSync{
+ Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", on.APIUrl, evalContext.Rule.ID),
Body: string(body),
HttpMethod: "POST",
HttpHeader: map[string]string{
"Content-Type": "application/json",
- "Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey),
+ "Authorization": fmt.Sprintf("GenieKey %s", on.APIKey),
},
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
+ on.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
return err
}
diff --git a/pkg/services/alerting/notifiers/opsgenie_test.go b/pkg/services/alerting/notifiers/opsgenie_test.go
index 9dcb9f3c600e..1df57676b38b 100644
--- a/pkg/services/alerting/notifiers/opsgenie_test.go
+++ b/pkg/services/alerting/notifiers/opsgenie_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestOpsGenieNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
@@ -33,7 +33,7 @@ func TestOpsGenieNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
@@ -45,7 +45,7 @@ func TestOpsGenieNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(opsgenieNotifier.Name, ShouldEqual, "opsgenie_testing")
So(opsgenieNotifier.Type, ShouldEqual, "opsgenie")
- So(opsgenieNotifier.ApiKey, ShouldEqual, "abcdefgh0123456789")
+ So(opsgenieNotifier.APIKey, ShouldEqual, "abcdefgh0123456789")
})
})
})
diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go
index 9f6ce3c2dc89..d771bfd1ad68 100644
--- a/pkg/services/alerting/notifiers/pagerduty.go
+++ b/pkg/services/alerting/notifiers/pagerduty.go
@@ -9,8 +9,8 @@ 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"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -40,10 +40,11 @@ func init() {
}
var (
- pagerdutyEventApiUrl = "https://events.pagerduty.com/v2/enqueue"
+ pagerdutyEventAPIURL = "https://events.pagerduty.com/v2/enqueue"
)
-func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewPagerdutyNotifier is the constructor for the PagerDuty notifier
+func NewPagerdutyNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
autoResolve := model.Settings.Get("autoResolve").MustBool(false)
key := model.Settings.Get("integrationKey").MustString()
if key == "" {
@@ -58,6 +59,8 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}, nil
}
+// PagerdutyNotifier is responsible for sending
+// alert notifications to pagerduty
type PagerdutyNotifier struct {
NotifierBase
Key string
@@ -65,15 +68,16 @@ type PagerdutyNotifier struct {
log log.Logger
}
-func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
+// Notify sends an alert notification to PagerDuty
+func (pn *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
- if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve {
- this.log.Info("Not sending a trigger to Pagerduty", "state", evalContext.Rule.State, "auto resolve", this.AutoResolve)
+ if evalContext.Rule.State == models.AlertStateOK && !pn.AutoResolve {
+ pn.log.Info("Not sending a trigger to Pagerduty", "state", evalContext.Rule.State, "auto resolve", pn.AutoResolve)
return nil
}
eventType := "trigger"
- if evalContext.Rule.State == m.AlertStateOK {
+ if evalContext.Rule.State == models.AlertStateOK {
eventType = "resolve"
}
customData := triggMetrString
@@ -81,7 +85,7 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
}
- this.log.Info("Notifying Pagerduty", "event_type", eventType)
+ pn.log.Info("Notifying Pagerduty", "event_type", eventType)
payloadJSON := simplejson.New()
payloadJSON.Set("summary", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
@@ -94,36 +98,36 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
payloadJSON.Set("custom_details", customData)
bodyJSON := simplejson.New()
- bodyJSON.Set("routing_key", this.Key)
+ bodyJSON.Set("routing_key", pn.Key)
bodyJSON.Set("event_action", eventType)
- bodyJSON.Set("dedup_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
+ bodyJSON.Set("dedup_key", "alertId-"+strconv.FormatInt(evalContext.Rule.ID, 10))
bodyJSON.Set("payload", payloadJSON)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ pn.log.Error("Failed get rule link", "error", err)
return err
}
links := make([]interface{}, 1)
linkJSON := simplejson.New()
- linkJSON.Set("href", ruleUrl)
- bodyJSON.Set("client_url", ruleUrl)
+ linkJSON.Set("href", ruleURL)
+ bodyJSON.Set("client_url", ruleURL)
bodyJSON.Set("client", "Grafana")
links[0] = linkJSON
bodyJSON.Set("links", links)
- if evalContext.ImagePublicUrl != "" {
+ if evalContext.ImagePublicURL != "" {
contexts := make([]interface{}, 1)
imageJSON := simplejson.New()
- imageJSON.Set("src", evalContext.ImagePublicUrl)
+ imageJSON.Set("src", evalContext.ImagePublicURL)
contexts[0] = imageJSON
bodyJSON.Set("images", contexts)
}
body, _ := bodyJSON.MarshalJSON()
- cmd := &m.SendWebhookSync{
- Url: pagerdutyEventApiUrl,
+ cmd := &models.SendWebhookSync{
+ Url: pagerdutyEventAPIURL,
Body: string(body),
HttpMethod: "POST",
HttpHeader: map[string]string{
@@ -132,7 +136,7 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send notification to Pagerduty", "error", err, "body", string(body))
+ pn.log.Error("Failed to send notification to Pagerduty", "error", err, "body", string(body))
return err
}
diff --git a/pkg/services/alerting/notifiers/pagerduty_test.go b/pkg/services/alerting/notifiers/pagerduty_test.go
index 1d2eeec4a522..1698ec7605ac 100644
--- a/pkg/services/alerting/notifiers/pagerduty_test.go
+++ b/pkg/services/alerting/notifiers/pagerduty_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -15,7 +15,7 @@ func TestPagerdutyNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "pageduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
@@ -29,7 +29,7 @@ func TestPagerdutyNotifier(t *testing.T) {
json := `{ "integrationKey": "abcdefgh0123456789" }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
@@ -53,7 +53,7 @@ func TestPagerdutyNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go
index 8e1903f68ac0..5da1a457e679 100644
--- a/pkg/services/alerting/notifiers/pushover.go
+++ b/pkg/services/alerting/notifiers/pushover.go
@@ -9,12 +9,12 @@ import (
"strconv"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
-const PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json"
+const pushoverEndpoint = "https://api.pushover.net/1/messages.json"
func init() {
sounds := `
@@ -95,9 +95,10 @@ func init() {
})
}
-func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewPushoverNotifier is the constructor for the Pushover Notifier
+func NewPushoverNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
userKey := model.Settings.Get("userKey").MustString()
- apiToken := model.Settings.Get("apiToken").MustString()
+ APIToken := model.Settings.Get("apiToken").MustString()
device := model.Settings.Get("device").MustString()
priority, _ := strconv.Atoi(model.Settings.Get("priority").MustString())
retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
@@ -109,13 +110,13 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error)
if userKey == "" {
return nil, alerting.ValidationError{Reason: "User key not given"}
}
- if apiToken == "" {
+ if APIToken == "" {
return nil, alerting.ValidationError{Reason: "API token not given"}
}
return &PushoverNotifier{
NotifierBase: NewNotifierBase(model),
UserKey: userKey,
- ApiToken: apiToken,
+ APIToken: APIToken,
Priority: priority,
Retry: retry,
Expire: expire,
@@ -127,10 +128,12 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}, nil
}
+// PushoverNotifier is responsible for sending
+// alert notifications to Pushover
type PushoverNotifier struct {
NotifierBase
UserKey string
- ApiToken string
+ APIToken string
Priority int
Retry int
Expire int
@@ -141,10 +144,11 @@ type PushoverNotifier struct {
log log.Logger
}
-func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
- ruleUrl, err := evalContext.GetRuleUrl()
+// Notify sends a alert notification to Pushover
+func (pn *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ pn.log.Error("Failed get rule link", "error", err)
return err
}
@@ -163,34 +167,34 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
message = "Notification message missing (Set a notification message to replace this text.)"
}
- headers, uploadBody, err := this.genPushoverBody(evalContext, message, ruleUrl)
+ headers, uploadBody, err := pn.genPushoverBody(evalContext, message, ruleURL)
if err != nil {
- this.log.Error("Failed to generate body for pushover", "error", err)
+ pn.log.Error("Failed to generate body for pushover", "error", err)
return err
}
- cmd := &m.SendWebhookSync{
- Url: PUSHOVER_ENDPOINT,
+ cmd := &models.SendWebhookSync{
+ Url: pushoverEndpoint,
HttpMethod: "POST",
HttpHeader: headers,
Body: uploadBody.String(),
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send pushover notification", "error", err, "webhook", this.Name)
+ pn.log.Error("Failed to send pushover notification", "error", err, "webhook", pn.Name)
return err
}
return nil
}
-func (this *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, message string, ruleUrl string) (map[string]string, bytes.Buffer, error) {
+func (pn *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, message string, ruleURL string) (map[string]string, bytes.Buffer, error) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&b)
// Add image only if requested and available
- if this.Upload && evalContext.ImageOnDiskPath != "" {
+ if pn.Upload && evalContext.ImageOnDiskPath != "" {
f, err := os.Open(evalContext.ImageOnDiskPath)
if err != nil {
return nil, b, err
@@ -209,47 +213,47 @@ func (this *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext,
}
// Add the user token
- err = w.WriteField("user", this.UserKey)
+ err = w.WriteField("user", pn.UserKey)
if err != nil {
return nil, b, err
}
// Add the api token
- err = w.WriteField("token", this.ApiToken)
+ err = w.WriteField("token", pn.APIToken)
if err != nil {
return nil, b, err
}
// Add priority
- err = w.WriteField("priority", strconv.Itoa(this.Priority))
+ err = w.WriteField("priority", strconv.Itoa(pn.Priority))
if err != nil {
return nil, b, err
}
- if this.Priority == 2 {
- err = w.WriteField("retry", strconv.Itoa(this.Retry))
+ if pn.Priority == 2 {
+ err = w.WriteField("retry", strconv.Itoa(pn.Retry))
if err != nil {
return nil, b, err
}
- err = w.WriteField("expire", strconv.Itoa(this.Expire))
+ err = w.WriteField("expire", strconv.Itoa(pn.Expire))
if err != nil {
return nil, b, err
}
}
// Add device
- if this.Device != "" {
- err = w.WriteField("device", this.Device)
+ if pn.Device != "" {
+ err = w.WriteField("device", pn.Device)
if err != nil {
return nil, b, err
}
}
// Add sound
- sound := this.AlertingSound
- if evalContext.Rule.State == m.AlertStateOK {
- sound = this.OkSound
+ sound := pn.AlertingSound
+ if evalContext.Rule.State == models.AlertStateOK {
+ sound = pn.OkSound
}
if sound != "default" {
err = w.WriteField("sound", sound)
@@ -265,7 +269,7 @@ func (this *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext,
}
// Add URL
- err = w.WriteField("url", ruleUrl)
+ err = w.WriteField("url", ruleURL)
if err != nil {
return nil, b, err
}
diff --git a/pkg/services/alerting/notifiers/pushover_test.go b/pkg/services/alerting/notifiers/pushover_test.go
index 5228491cccd5..7b2d51a176f3 100644
--- a/pkg/services/alerting/notifiers/pushover_test.go
+++ b/pkg/services/alerting/notifiers/pushover_test.go
@@ -2,12 +2,13 @@ package notifiers
import (
"context"
- "github.com/grafana/grafana/pkg/services/alerting"
"strings"
"testing"
+ "github.com/grafana/grafana/pkg/services/alerting"
+
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -19,7 +20,7 @@ func TestPushoverNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "Pushover",
Type: "pushover",
Settings: settingsJSON,
@@ -40,7 +41,7 @@ func TestPushoverNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "Pushover",
Type: "pushover",
Settings: settingsJSON,
@@ -52,7 +53,7 @@ func TestPushoverNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(pushoverNotifier.Name, ShouldEqual, "Pushover")
So(pushoverNotifier.Type, ShouldEqual, "pushover")
- So(pushoverNotifier.ApiToken, ShouldEqual, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve")
+ So(pushoverNotifier.APIToken, ShouldEqual, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve")
So(pushoverNotifier.UserKey, ShouldEqual, "tzNZYf36y0ohWwXo4XoUrB61rz1A4o")
So(pushoverNotifier.Priority, ShouldEqual, 1)
So(pushoverNotifier.AlertingSound, ShouldEqual, "pushover")
@@ -73,7 +74,7 @@ func TestGenPushoverBody(t *testing.T) {
Convey("When alert is firing - should use siren sound", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
- State: m.AlertStateAlerting,
+ State: models.AlertStateAlerting,
})
_, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "")
@@ -84,7 +85,7 @@ func TestGenPushoverBody(t *testing.T) {
Convey("When alert is ok - should use success sound", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
- State: m.AlertStateOK,
+ State: models.AlertStateOK,
})
_, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "")
diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go
index 21d5d3d9d9e5..7f60178d10f5 100644
--- a/pkg/services/alerting/notifiers/sensu.go
+++ b/pkg/services/alerting/notifiers/sensu.go
@@ -6,8 +6,8 @@ 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"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -44,7 +44,8 @@ func init() {
}
-func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewSensuNotifier is the constructor for the Sensu Notifier.
+func NewSensuNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
@@ -52,7 +53,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return &SensuNotifier{
NotifierBase: NewNotifierBase(model),
- Url: url,
+ URL: url,
User: model.Settings.Get("username").MustString(),
Source: model.Settings.Get("source").MustString(),
Password: model.Settings.Get("password").MustString(),
@@ -61,9 +62,11 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
+// SensuNotifier is responsible for sending
+// alert notifications to Sensu.
type SensuNotifier struct {
NotifierBase
- Url string
+ URL string
Source string
User string
Password string
@@ -71,19 +74,20 @@ type SensuNotifier struct {
log log.Logger
}
-func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Sending sensu result")
+// Notify send alert notification to Sensu
+func (sn *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
+ sn.log.Info("Sending sensu result")
bodyJSON := simplejson.New()
- bodyJSON.Set("ruleId", evalContext.Rule.Id)
+ bodyJSON.Set("ruleId", evalContext.Rule.ID)
// Sensu alerts cannot have spaces in them
bodyJSON.Set("name", strings.Replace(evalContext.Rule.Name, " ", "_", -1))
// Sensu alerts require a source. We set it to the user-specified value (optional),
// else we fallback and use the grafana ruleID.
- if this.Source != "" {
- bodyJSON.Set("source", this.Source)
+ if sn.Source != "" {
+ bodyJSON.Set("source", sn.Source)
} else {
- bodyJSON.Set("source", "grafana_rule_"+strconv.FormatInt(evalContext.Rule.Id, 10))
+ bodyJSON.Set("source", "grafana_rule_"+strconv.FormatInt(evalContext.Rule.ID, 10))
}
// Finally, sensu expects an output
// We set it to a default output
@@ -98,17 +102,17 @@ func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
bodyJSON.Set("status", 0)
}
- if this.Handler != "" {
- bodyJSON.Set("handler", this.Handler)
+ if sn.Handler != "" {
+ bodyJSON.Set("handler", sn.Handler)
}
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err == nil {
- bodyJSON.Set("ruleUrl", ruleUrl)
+ bodyJSON.Set("ruleUrl", ruleURL)
}
- if evalContext.ImagePublicUrl != "" {
- bodyJSON.Set("imageUrl", evalContext.ImagePublicUrl)
+ if evalContext.ImagePublicURL != "" {
+ bodyJSON.Set("imageUrl", evalContext.ImagePublicURL)
}
if evalContext.Rule.Message != "" {
@@ -117,16 +121,16 @@ func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
body, _ := bodyJSON.MarshalJSON()
- cmd := &m.SendWebhookSync{
- Url: this.Url,
- User: this.User,
- Password: this.Password,
+ cmd := &models.SendWebhookSync{
+ Url: sn.URL,
+ User: sn.User,
+ Password: sn.Password,
Body: string(body),
HttpMethod: "POST",
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send sensu event", "error", err, "sensu", this.Name)
+ sn.log.Error("Failed to send sensu event", "error", err, "sensu", sn.Name)
return err
}
diff --git a/pkg/services/alerting/notifiers/sensu_test.go b/pkg/services/alerting/notifiers/sensu_test.go
index 40e3b1e1cc31..0c73a493ee8b 100644
--- a/pkg/services/alerting/notifiers/sensu_test.go
+++ b/pkg/services/alerting/notifiers/sensu_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestSensuNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "sensu",
Type: "sensu",
Settings: settingsJSON,
@@ -35,7 +35,7 @@ func TestSensuNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "sensu",
Type: "sensu",
Settings: settingsJSON,
@@ -47,7 +47,7 @@ func TestSensuNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(sensuNotifier.Name, ShouldEqual, "sensu")
So(sensuNotifier.Type, ShouldEqual, "sensu")
- So(sensuNotifier.Url, ShouldEqual, "http://sensu-api.example.com:4567/results")
+ So(sensuNotifier.URL, ShouldEqual, "http://sensu-api.example.com:4567/results")
So(sensuNotifier.Source, ShouldEqual, "grafana_instance_01")
So(sensuNotifier.Handler, ShouldEqual, "myhandler")
})
diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go
index ca5f47a322f5..f86314d07263 100644
--- a/pkg/services/alerting/notifiers/slack.go
+++ b/pkg/services/alerting/notifiers/slack.go
@@ -10,8 +10,8 @@ import (
"time"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/setting"
)
@@ -99,7 +99,8 @@ func init() {
}
-func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewSlackNotifier is the constructor for the Slack notifier
+func NewSlackNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
@@ -108,18 +109,18 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
recipient := model.Settings.Get("recipient").MustString()
username := model.Settings.Get("username").MustString()
iconEmoji := model.Settings.Get("icon_emoji").MustString()
- iconUrl := model.Settings.Get("icon_url").MustString()
+ iconURL := model.Settings.Get("icon_url").MustString()
mention := model.Settings.Get("mention").MustString()
token := model.Settings.Get("token").MustString()
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
return &SlackNotifier{
NotifierBase: NewNotifierBase(model),
- Url: url,
+ URL: url,
Recipient: recipient,
Username: username,
IconEmoji: iconEmoji,
- IconUrl: iconUrl,
+ IconURL: iconURL,
Mention: mention,
Token: token,
Upload: uploadImage,
@@ -127,25 +128,28 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
+// SlackNotifier is responsible for sending
+// alert notification to Slack.
type SlackNotifier struct {
NotifierBase
- Url string
+ URL string
Recipient string
Username string
IconEmoji string
- IconUrl string
+ IconURL string
Mention string
Token string
Upload bool
log log.Logger
}
-func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+// Notify send alert notification to Slack.
+func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
+ sn.log.Info("Executing slack notification", "ruleId", evalContext.Rule.ID, "notification", sn.Name)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ sn.log.Error("Failed get rule link", "error", err)
return err
}
@@ -170,14 +174,14 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
})
}
- message := this.Mention
- if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
+ message := sn.Mention
+ if evalContext.Rule.State != models.AlertStateOK { //don't add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
}
- image_url := ""
+ imageURL := ""
// default to file.upload API method if a token is provided
- if this.Token == "" {
- image_url = evalContext.ImagePublicUrl
+ if sn.Token == "" {
+ imageURL = evalContext.ImagePublicURL
}
body := map[string]interface{}{
@@ -186,10 +190,10 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
"fallback": evalContext.GetNotificationTitle(),
"color": evalContext.GetStateModel().Color,
"title": evalContext.GetNotificationTitle(),
- "title_link": ruleUrl,
+ "title_link": ruleURL,
"text": message,
"fields": fields,
- "image_url": image_url,
+ "image_url": imageURL,
"footer": "Grafana v" + setting.BuildVersion,
"footer_icon": "https://grafana.com/assets/img/fav32.png",
"ts": time.Now().Unix(),
@@ -199,26 +203,26 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
}
//recipient override
- if this.Recipient != "" {
- body["channel"] = this.Recipient
+ if sn.Recipient != "" {
+ body["channel"] = sn.Recipient
}
- if this.Username != "" {
- body["username"] = this.Username
+ if sn.Username != "" {
+ body["username"] = sn.Username
}
- if this.IconEmoji != "" {
- body["icon_emoji"] = this.IconEmoji
+ if sn.IconEmoji != "" {
+ body["icon_emoji"] = sn.IconEmoji
}
- if this.IconUrl != "" {
- body["icon_url"] = this.IconUrl
+ if sn.IconURL != "" {
+ body["icon_url"] = sn.IconURL
}
data, _ := json.Marshal(&body)
- cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)}
+ cmd := &models.SendWebhookSync{Url: sn.URL, Body: string(data)}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name)
+ sn.log.Error("Failed to send slack notification", "error", err, "webhook", sn.Name)
return err
}
- if this.Token != "" && this.UploadImage {
- err = SlackFileUpload(evalContext, this.log, "https://slack.com/api/files.upload", this.Recipient, this.Token)
+ if sn.Token != "" && sn.UploadImage {
+ err = slackFileUpload(evalContext, sn.log, "https://slack.com/api/files.upload", sn.Recipient, sn.Token)
if err != nil {
return err
}
@@ -226,27 +230,24 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
-func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
+func slackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
if evalContext.ImageOnDiskPath == "" {
evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png")
}
log.Info("Uploading to slack via file.upload API")
- headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
+ headers, uploadBody, err := generateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
if err != nil {
return err
}
- cmd := &m.SendWebhookSync{Url: url, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"}
+ cmd := &models.SendWebhookSync{Url: url, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload")
return err
}
- if err != nil {
- return err
- }
return nil
}
-func GenerateSlackBody(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) {
+func generateSlackBody(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) {
// Slack requires all POSTs to files.upload to present
// an "application/x-www-form-urlencoded" encoded querystring
// See https://api.slack.com/methods/files.upload
diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go
index 17362bc850b6..c8c5b1220cf6 100644
--- a/pkg/services/alerting/notifiers/slack_test.go
+++ b/pkg/services/alerting/notifiers/slack_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestSlackNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "slack",
Settings: settingsJSON,
@@ -33,7 +33,7 @@ func TestSlackNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "slack",
Settings: settingsJSON,
@@ -45,11 +45,11 @@ func TestSlackNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(slackNotifier.Name, ShouldEqual, "ops")
So(slackNotifier.Type, ShouldEqual, "slack")
- So(slackNotifier.Url, ShouldEqual, "http://google.com")
+ So(slackNotifier.URL, ShouldEqual, "http://google.com")
So(slackNotifier.Recipient, ShouldEqual, "")
So(slackNotifier.Username, ShouldEqual, "")
So(slackNotifier.IconEmoji, ShouldEqual, "")
- So(slackNotifier.IconUrl, ShouldEqual, "")
+ So(slackNotifier.IconURL, ShouldEqual, "")
So(slackNotifier.Mention, ShouldEqual, "")
So(slackNotifier.Token, ShouldEqual, "")
})
@@ -67,7 +67,7 @@ func TestSlackNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "slack",
Settings: settingsJSON,
@@ -79,11 +79,11 @@ func TestSlackNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(slackNotifier.Name, ShouldEqual, "ops")
So(slackNotifier.Type, ShouldEqual, "slack")
- So(slackNotifier.Url, ShouldEqual, "http://google.com")
+ So(slackNotifier.URL, ShouldEqual, "http://google.com")
So(slackNotifier.Recipient, ShouldEqual, "#ds-opentsdb")
So(slackNotifier.Username, ShouldEqual, "Grafana Alerts")
So(slackNotifier.IconEmoji, ShouldEqual, ":smile:")
- So(slackNotifier.IconUrl, ShouldEqual, "https://grafana.com/img/fav32.png")
+ So(slackNotifier.IconURL, ShouldEqual, "https://grafana.com/img/fav32.png")
So(slackNotifier.Mention, ShouldEqual, "@carl")
So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
})
diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go
index e19357d7b7e2..4d0c47ddad23 100644
--- a/pkg/services/alerting/notifiers/teams.go
+++ b/pkg/services/alerting/notifiers/teams.go
@@ -4,8 +4,8 @@ import (
"encoding/json"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -26,7 +26,8 @@ func init() {
}
-func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewTeamsNotifier is the constructor for Teams notifier.
+func NewTeamsNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
@@ -34,23 +35,26 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return &TeamsNotifier{
NotifierBase: NewNotifierBase(model),
- Url: url,
+ URL: url,
log: log.New("alerting.notifier.teams"),
}, nil
}
+// TeamsNotifier is responsible for sending
+// alert notifications to Microsoft teams.
type TeamsNotifier struct {
NotifierBase
- Url string
+ URL string
log log.Logger
}
-func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+// Notify send an alert notification to Microsoft teams.
+func (tn *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
+ tn.log.Info("Executing teams notification", "ruleId", evalContext.Rule.ID, "notification", tn.Name)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ tn.log.Error("Failed get rule link", "error", err)
return err
}
@@ -74,14 +78,14 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
}
message := ""
- if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
+ if evalContext.Rule.State != models.AlertStateOK { //don't add message when going back to alert state ok.
message = evalContext.Rule.Message
}
images := make([]map[string]interface{}, 0)
- if evalContext.ImagePublicUrl != "" {
+ if evalContext.ImagePublicURL != "" {
images = append(images, map[string]interface{}{
- "image": evalContext.ImagePublicUrl,
+ "image": evalContext.ImagePublicURL,
})
}
@@ -108,7 +112,7 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
"name": "View Rule",
"targets": []map[string]interface{}{
{
- "os": "default", "uri": ruleUrl,
+ "os": "default", "uri": ruleURL,
},
},
},
@@ -118,7 +122,7 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
"name": "View Graph",
"targets": []map[string]interface{}{
{
- "os": "default", "uri": evalContext.ImagePublicUrl,
+ "os": "default", "uri": evalContext.ImagePublicURL,
},
},
},
@@ -126,10 +130,10 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
}
data, _ := json.Marshal(&body)
- cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)}
+ cmd := &models.SendWebhookSync{Url: tn.URL, Body: string(data)}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send teams notification", "error", err, "webhook", this.Name)
+ tn.log.Error("Failed to send teams notification", "error", err, "webhook", tn.Name)
return err
}
diff --git a/pkg/services/alerting/notifiers/teams_test.go b/pkg/services/alerting/notifiers/teams_test.go
index a96473507363..10ec7edca285 100644
--- a/pkg/services/alerting/notifiers/teams_test.go
+++ b/pkg/services/alerting/notifiers/teams_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestTeamsNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
@@ -33,7 +33,7 @@ func TestTeamsNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
@@ -45,7 +45,7 @@ func TestTeamsNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(teamsNotifier.Name, ShouldEqual, "ops")
So(teamsNotifier.Type, ShouldEqual, "teams")
- So(teamsNotifier.Url, ShouldEqual, "http://google.com")
+ So(teamsNotifier.URL, ShouldEqual, "http://google.com")
})
Convey("from settings with Recipient and Mention", func() {
@@ -55,7 +55,7 @@ func TestTeamsNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
@@ -67,9 +67,8 @@ func TestTeamsNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(teamsNotifier.Name, ShouldEqual, "ops")
So(teamsNotifier.Type, ShouldEqual, "teams")
- So(teamsNotifier.Url, ShouldEqual, "http://google.com")
+ So(teamsNotifier.URL, ShouldEqual, "http://google.com")
})
-
})
})
}
diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go
index ab43f3bce356..be354bc2733e 100644
--- a/pkg/services/alerting/notifiers/telegram.go
+++ b/pkg/services/alerting/notifiers/telegram.go
@@ -8,8 +8,8 @@ import (
"os"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -18,7 +18,7 @@ const (
)
var (
- telegramApiUrl = "https://api.telegram.org/bot%s/%s"
+ telegramAPIURL = "https://api.telegram.org/bot%s/%s"
)
func init() {
@@ -52,6 +52,8 @@ func init() {
}
+// TelegramNotifier is responsible for sending
+// alert notifications to Telegram.
type TelegramNotifier struct {
NotifierBase
BotToken string
@@ -60,54 +62,55 @@ type TelegramNotifier struct {
log log.Logger
}
-func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewTelegramNotifier is the constructor for the Telegram notifier
+func NewTelegramNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
if model.Settings == nil {
return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
}
botToken := model.Settings.Get("bottoken").MustString()
- chatId := model.Settings.Get("chatid").MustString()
+ chatID := model.Settings.Get("chatid").MustString()
uploadImage := model.Settings.Get("uploadImage").MustBool()
if botToken == "" {
return nil, alerting.ValidationError{Reason: "Could not find Bot Token in settings"}
}
- if chatId == "" {
+ if chatID == "" {
return nil, alerting.ValidationError{Reason: "Could not find Chat Id in settings"}
}
return &TelegramNotifier{
NotifierBase: NewNotifierBase(model),
BotToken: botToken,
- ChatID: chatId,
+ ChatID: chatID,
UploadImage: uploadImage,
log: log.New("alerting.notifier.telegram"),
}, nil
}
-func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync {
+func (tn *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *models.SendWebhookSync {
if sendImageInline {
- cmd, err := this.buildMessageInlineImage(evalContext)
+ cmd, err := tn.buildMessageInlineImage(evalContext)
if err == nil {
return cmd
}
- this.log.Error("Could not generate Telegram message with inline image.", "err", err)
+ tn.log.Error("Could not generate Telegram message with inline image.", "err", err)
}
- return this.buildMessageLinkedImage(evalContext)
+ return tn.buildMessageLinkedImage(evalContext)
}
-func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *m.SendWebhookSync {
+func (tn *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *models.SendWebhookSync {
message := fmt.Sprintf("
%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err == nil {
- message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
+ message = message + fmt.Sprintf("URL: %s\n", ruleURL)
}
- if evalContext.ImagePublicUrl != "" {
- message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
+ if evalContext.ImagePublicURL != "" {
+ message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicURL)
}
metrics := generateMetricsMessage(evalContext)
@@ -115,14 +118,14 @@ func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.Eval
message = message + fmt.Sprintf("\n
Metrics:%s", metrics)
}
- cmd := this.generateTelegramCmd(message, "text", "sendMessage", func(w *multipart.Writer) {
+ cmd := tn.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) {
+func (tn *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*models.SendWebhookSync, error) {
var imageFile *os.File
var err error
@@ -130,7 +133,7 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
defer func() {
err := imageFile.Close()
if err != nil {
- this.log.Error("Could not close Telegram inline image.", "err", err)
+ tn.log.Error("Could not close Telegram inline image.", "err", err)
}
}()
@@ -138,27 +141,27 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
return nil, err
}
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
return nil, err
}
metrics := generateMetricsMessage(evalContext)
- message := generateImageCaption(evalContext, ruleUrl, metrics)
+ message := generateImageCaption(evalContext, ruleURL, metrics)
- cmd := this.generateTelegramCmd(message, "caption", "sendPhoto", func(w *multipart.Writer) {
+ cmd := tn.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 {
+func (tn *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) *models.SendWebhookSync {
var body bytes.Buffer
w := multipart.NewWriter(&body)
fw, _ := w.CreateFormField("chat_id")
- fw.Write([]byte(this.ChatID))
+ fw.Write([]byte(tn.ChatID))
fw, _ = w.CreateFormField(messageField)
fw.Write([]byte(message))
@@ -167,10 +170,10 @@ func (this *TelegramNotifier) generateTelegramCmd(message string, messageField s
w.Close()
- this.log.Info("Sending telegram notification", "chat_id", this.ChatID, "bot_token", this.BotToken, "apiAction", apiAction)
- url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiAction)
+ tn.log.Info("Sending telegram notification", "chat_id", tn.ChatID, "bot_token", tn.BotToken, "apiAction", apiAction)
+ url := fmt.Sprintf(telegramAPIURL, tn.BotToken, apiAction)
- cmd := &m.SendWebhookSync{
+ cmd := &models.SendWebhookSync{
Url: url,
Body: body.String(),
HttpMethod: "POST",
@@ -193,7 +196,7 @@ func generateMetricsMessage(evalContext *alerting.EvalContext) string {
return metrics
}
-func generateImageCaption(evalContext *alerting.EvalContext, ruleUrl string, metrics string) string {
+func generateImageCaption(evalContext *alerting.EvalContext, ruleURL string, metrics string) string {
message := evalContext.GetNotificationTitle()
if len(evalContext.Rule.Message) > 0 {
@@ -205,8 +208,8 @@ func generateImageCaption(evalContext *alerting.EvalContext, ruleUrl string, met
}
- if len(ruleUrl) > 0 {
- urlLine := fmt.Sprintf("\nURL: %s", ruleUrl)
+ if len(ruleURL) > 0 {
+ urlLine := fmt.Sprintf("\nURL: %s", ruleURL)
message = appendIfPossible(message, urlLine, captionLengthLimit)
}
@@ -226,16 +229,17 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
return message
}
-func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
- var cmd *m.SendWebhookSync
- if evalContext.ImagePublicUrl == "" && this.UploadImage {
- cmd = this.buildMessage(evalContext, true)
+// Notify send an alert notification to Telegram.
+func (tn *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
+ var cmd *models.SendWebhookSync
+ if evalContext.ImagePublicURL == "" && tn.UploadImage {
+ cmd = tn.buildMessage(evalContext, true)
} else {
- cmd = this.buildMessage(evalContext, false)
+ cmd = tn.buildMessage(evalContext, false)
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
+ tn.log.Error("Failed to send webhook", "error", err, "webhook", tn.Name)
return err
}
diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go
index 9906a2ffd957..3559555439e5 100644
--- a/pkg/services/alerting/notifiers/telegram_test.go
+++ b/pkg/services/alerting/notifiers/telegram_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
@@ -18,7 +18,7 @@ func TestTelegramNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "telegram_testing",
Type: "telegram",
Settings: settingsJSON,
@@ -36,7 +36,7 @@ func TestTelegramNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "telegram_testing",
Type: "telegram",
Settings: settingsJSON,
@@ -57,7 +57,7 @@ func TestTelegramNotifier(t *testing.T) {
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message.",
- State: m.AlertStateOK,
+ State: models.AlertStateOK,
})
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
@@ -74,7 +74,7 @@ func TestTelegramNotifier(t *testing.T) {
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message.",
- State: m.AlertStateOK,
+ State: models.AlertStateOK,
})
caption := generateImageCaption(evalContext,
@@ -92,7 +92,7 @@ func TestTelegramNotifier(t *testing.T) {
&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. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis scelerisque. Nulla ipsum ex, iaculis vitae vehicula sit amet, fermentum eu eros.",
- State: m.AlertStateOK,
+ State: models.AlertStateOK,
})
caption := generateImageCaption(evalContext,
@@ -109,7 +109,7 @@ func TestTelegramNotifier(t *testing.T) {
&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. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri",
- State: m.AlertStateOK,
+ State: models.AlertStateOK,
})
caption := generateImageCaption(evalContext,
diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go
index 28a62fade177..560e8c12e80b 100644
--- a/pkg/services/alerting/notifiers/threema.go
+++ b/pkg/services/alerting/notifiers/threema.go
@@ -6,8 +6,8 @@ import (
"strings"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -68,6 +68,8 @@ func init() {
}
+// ThreemaNotifier is responsible for sending
+// alert notifications to Threema.
type ThreemaNotifier struct {
NotifierBase
GatewayID string
@@ -76,7 +78,8 @@ type ThreemaNotifier struct {
log log.Logger
}
-func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewThreemaNotifier is the constructor for the Threema notifer
+func NewThreemaNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
if model.Settings == nil {
return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
}
@@ -114,6 +117,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
+// Notify send an alert notification to Threema
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)
@@ -127,11 +131,11 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error
// Determine emoji
stateEmoji := ""
switch evalContext.Rule.State {
- case m.AlertStateOK:
+ case models.AlertStateOK:
stateEmoji = "\u2705 " // White Heavy Check Mark
- case m.AlertStateNoData:
+ case models.AlertStateNoData:
stateEmoji = "\u2753 " // Black Question Mark Ornament
- case m.AlertStateAlerting:
+ case models.AlertStateAlerting:
stateEmoji = "\u26A0 " // Warning sign
}
@@ -139,12 +143,12 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error
message := fmt.Sprintf("%s%s\n\n*State:* %s\n*Message:* %s\n",
stateEmoji, evalContext.GetNotificationTitle(),
evalContext.Rule.Name, evalContext.Rule.Message)
- ruleURL, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err == nil {
message = message + fmt.Sprintf("*URL:* %s\n", ruleURL)
}
- if evalContext.ImagePublicUrl != "" {
- message = message + fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicUrl)
+ if evalContext.ImagePublicURL != "" {
+ message = message + fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicURL)
}
data.Set("text", message)
@@ -154,7 +158,7 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error
headers := map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
}
- cmd := &m.SendWebhookSync{
+ cmd := &models.SendWebhookSync{
Url: url,
Body: body,
HttpMethod: "POST",
diff --git a/pkg/services/alerting/notifiers/threema_test.go b/pkg/services/alerting/notifiers/threema_test.go
index 3f23730a249a..8ec77db42129 100644
--- a/pkg/services/alerting/notifiers/threema_test.go
+++ b/pkg/services/alerting/notifiers/threema_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
@@ -17,7 +17,7 @@ func TestThreemaNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
@@ -36,7 +36,7 @@ func TestThreemaNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
@@ -63,7 +63,7 @@ func TestThreemaNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
@@ -83,7 +83,7 @@ func TestThreemaNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
@@ -103,7 +103,7 @@ func TestThreemaNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
@@ -113,7 +113,6 @@ func TestThreemaNotifier(t *testing.T) {
So(not, ShouldBeNil)
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Recipient ID: Must be 8 characters long")
})
-
})
})
}
diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go
index f33b3f4019e0..d19ea356547b 100644
--- a/pkg/services/alerting/notifiers/victorops.go
+++ b/pkg/services/alerting/notifiers/victorops.go
@@ -5,7 +5,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/setting"
@@ -14,7 +14,7 @@ import (
// AlertStateCritical - Victorops uses "CRITICAL" string to indicate "Alerting" state
const AlertStateCritical = "CRITICAL"
-const AlertStateRecovery = "RECOVERY"
+const alertStateRecovery = "RECOVERY"
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
@@ -69,17 +69,17 @@ type VictoropsNotifier struct {
}
// Notify sends notification to Victorops via POST to URL endpoint
-func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+func (vn *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
+ vn.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.ID, "notification", vn.Name)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err != nil {
- this.log.Error("Failed get rule link", "error", err)
+ vn.log.Error("Failed get rule link", "error", err)
return err
}
- if evalContext.Rule.State == models.AlertStateOK && !this.AutoResolve {
- this.log.Info("Not alerting VictorOps", "state", evalContext.Rule.State, "auto resolve", this.AutoResolve)
+ if evalContext.Rule.State == models.AlertStateOK && !vn.AutoResolve {
+ vn.log.Info("Not alerting VictorOps", "state", evalContext.Rule.State, "auto resolve", vn.AutoResolve)
return nil
}
@@ -89,10 +89,10 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
}
if evalContext.Rule.State == models.AlertStateOK {
- messageType = AlertStateRecovery
+ messageType = alertStateRecovery
}
- fields := make(map[string]interface{}, 0)
+ fields := make(map[string]interface{})
fieldLimitCount := 4
for index, evt := range evalContext.EvalMatches {
fields[evt.Metric] = evt.Value
@@ -109,22 +109,22 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
bodyJSON.Set("state_start_time", evalContext.StartTime.Unix())
bodyJSON.Set("state_message", evalContext.Rule.Message)
bodyJSON.Set("monitoring_tool", "Grafana v"+setting.BuildVersion)
- bodyJSON.Set("alert_url", ruleUrl)
+ bodyJSON.Set("alert_url", ruleURL)
bodyJSON.Set("metrics", fields)
if evalContext.Error != nil {
bodyJSON.Set("error_message", evalContext.Error.Error())
}
- if evalContext.ImagePublicUrl != "" {
- bodyJSON.Set("image_url", evalContext.ImagePublicUrl)
+ if evalContext.ImagePublicURL != "" {
+ bodyJSON.Set("image_url", evalContext.ImagePublicURL)
}
data, _ := bodyJSON.MarshalJSON()
- cmd := &models.SendWebhookSync{Url: this.URL, Body: string(data)}
+ cmd := &models.SendWebhookSync{Url: vn.URL, Body: string(data)}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send Victorops notification", "error", err, "webhook", this.Name)
+ vn.log.Error("Failed to send Victorops notification", "error", err, "webhook", vn.Name)
return err
}
diff --git a/pkg/services/alerting/notifiers/victorops_test.go b/pkg/services/alerting/notifiers/victorops_test.go
index 6ac806a82cc5..258d2900a22b 100644
--- a/pkg/services/alerting/notifiers/victorops_test.go
+++ b/pkg/services/alerting/notifiers/victorops_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestVictoropsNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
@@ -33,7 +33,7 @@ func TestVictoropsNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go
index 4045e496af99..f5ee99245127 100644
--- a/pkg/services/alerting/notifiers/webhook.go
+++ b/pkg/services/alerting/notifiers/webhook.go
@@ -3,8 +3,8 @@ package notifiers
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"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
@@ -40,7 +40,9 @@ func init() {
}
-func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+// NewWebHookNotifier is the constructor for
+// the WebHook notifier.
+func NewWebHookNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
@@ -48,40 +50,44 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return &WebhookNotifier{
NotifierBase: NewNotifierBase(model),
- Url: url,
+ URL: url,
User: model.Settings.Get("username").MustString(),
Password: model.Settings.Get("password").MustString(),
- HttpMethod: model.Settings.Get("httpMethod").MustString("POST"),
+ HTTPMethod: model.Settings.Get("httpMethod").MustString("POST"),
log: log.New("alerting.notifier.webhook"),
}, nil
}
+// WebhookNotifier is responsible for sending
+// alert notifications as webhooks.
type WebhookNotifier struct {
NotifierBase
- Url string
+ URL string
User string
Password string
- HttpMethod string
+ HTTPMethod string
log log.Logger
}
-func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
- this.log.Info("Sending webhook")
+// Notify send alert notifications as
+// webhook as http requests.
+func (wn *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
+ wn.log.Info("Sending webhook")
bodyJSON := simplejson.New()
bodyJSON.Set("title", evalContext.GetNotificationTitle())
- bodyJSON.Set("ruleId", evalContext.Rule.Id)
+ bodyJSON.Set("ruleId", evalContext.Rule.ID)
bodyJSON.Set("ruleName", evalContext.Rule.Name)
bodyJSON.Set("state", evalContext.Rule.State)
bodyJSON.Set("evalMatches", evalContext.EvalMatches)
- ruleUrl, err := evalContext.GetRuleUrl()
+ ruleURL, err := evalContext.GetRuleURL()
if err == nil {
- bodyJSON.Set("ruleUrl", ruleUrl)
+ bodyJSON.Set("ruleUrl", ruleURL)
}
- if evalContext.ImagePublicUrl != "" {
- bodyJSON.Set("imageUrl", evalContext.ImagePublicUrl)
+ if evalContext.ImagePublicURL != "" {
+ bodyJSON.Set("imageUrl", evalContext.ImagePublicURL)
}
if evalContext.Rule.Message != "" {
@@ -90,16 +96,16 @@ func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
body, _ := bodyJSON.MarshalJSON()
- cmd := &m.SendWebhookSync{
- Url: this.Url,
- User: this.User,
- Password: this.Password,
+ cmd := &models.SendWebhookSync{
+ Url: wn.URL,
+ User: wn.User,
+ Password: wn.Password,
Body: string(body),
- HttpMethod: this.HttpMethod,
+ HttpMethod: wn.HTTPMethod,
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
+ wn.log.Error("Failed to send webhook", "error", err, "webhook", wn.Name)
return err
}
diff --git a/pkg/services/alerting/notifiers/webhook_test.go b/pkg/services/alerting/notifiers/webhook_test.go
index b2d944eb6e94..b8c001b6e60b 100644
--- a/pkg/services/alerting/notifiers/webhook_test.go
+++ b/pkg/services/alerting/notifiers/webhook_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -16,7 +16,7 @@ func TestWebhookNotifier(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "webhook",
Settings: settingsJSON,
@@ -33,7 +33,7 @@ func TestWebhookNotifier(t *testing.T) {
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: "ops",
Type: "webhook",
Settings: settingsJSON,
@@ -45,7 +45,7 @@ func TestWebhookNotifier(t *testing.T) {
So(err, ShouldBeNil)
So(webhookNotifier.Name, ShouldEqual, "ops")
So(webhookNotifier.Type, ShouldEqual, "webhook")
- So(webhookNotifier.Url, ShouldEqual, "http://google.com")
+ So(webhookNotifier.URL, ShouldEqual, "http://google.com")
})
})
})
diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go
index d9f20a21c8e6..c8020510ef63 100644
--- a/pkg/services/alerting/reader.go
+++ b/pkg/services/alerting/reader.go
@@ -2,45 +2,32 @@ package alerting
import (
"sync"
- "time"
"github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
)
-type RuleReader interface {
- Fetch() []*Rule
+type ruleReader interface {
+ fetch() []*Rule
}
-type DefaultRuleReader struct {
+type defaultRuleReader struct {
sync.RWMutex
- //serverID string
- serverPosition int
- clusterSize int
- log log.Logger
+ log log.Logger
}
-func NewRuleReader() *DefaultRuleReader {
- ruleReader := &DefaultRuleReader{
+func newRuleReader() *defaultRuleReader {
+ ruleReader := &defaultRuleReader{
log: log.New("alerting.ruleReader"),
}
- go ruleReader.initReader()
return ruleReader
}
-func (arr *DefaultRuleReader) initReader() {
- heartbeat := time.NewTicker(time.Second * 10)
-
- for range heartbeat.C {
- arr.heartbeat()
- }
-}
-
-func (arr *DefaultRuleReader) Fetch() []*Rule {
- cmd := &m.GetAllAlertsQuery{}
+func (arr *defaultRuleReader) fetch() []*Rule {
+ cmd := &models.GetAllAlertsQuery{}
if err := bus.Dispatch(cmd); err != nil {
arr.log.Error("Could not load alerts", "error", err)
@@ -59,8 +46,3 @@ func (arr *DefaultRuleReader) Fetch() []*Rule {
metrics.M_Alerting_Active_Alerts.Set(float64(len(res)))
return res
}
-
-func (arr *DefaultRuleReader) heartbeat() {
- arr.clusterSize = 1
- arr.serverPosition = 1
-}
diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go
index c8e9e2dc25c7..814a3f8a21e2 100644
--- a/pkg/services/alerting/result_handler.go
+++ b/pkg/services/alerting/result_handler.go
@@ -5,30 +5,31 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
+
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/rendering"
)
-type ResultHandler interface {
- Handle(evalContext *EvalContext) error
+type resultHandler interface {
+ handle(evalContext *EvalContext) error
}
-type DefaultResultHandler struct {
- notifier NotificationService
+type defaultResultHandler struct {
+ notifier *notificationService
log log.Logger
}
-func NewResultHandler(renderService rendering.Service) *DefaultResultHandler {
- return &DefaultResultHandler{
+func newResultHandler(renderService rendering.Service) *defaultResultHandler {
+ return &defaultResultHandler{
log: log.New("alerting.resultHandler"),
- notifier: NewNotificationService(renderService),
+ notifier: newNotificationService(renderService),
}
}
-func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
+func (handler *defaultResultHandler) handle(evalContext *EvalContext) error {
executionError := ""
annotationData := simplejson.New()
@@ -44,24 +45,24 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
}
metrics.M_Alerting_Result_State.WithLabelValues(string(evalContext.Rule.State)).Inc()
- if evalContext.ShouldUpdateAlertState() {
- handler.log.Info("New state change", "alertId", evalContext.Rule.Id, "newState", evalContext.Rule.State, "prev state", evalContext.PrevAlertState)
+ if evalContext.shouldUpdateAlertState() {
+ handler.log.Info("New state change", "alertId", evalContext.Rule.ID, "newState", evalContext.Rule.State, "prev state", evalContext.PrevAlertState)
- cmd := &m.SetAlertStateCommand{
- AlertId: evalContext.Rule.Id,
- OrgId: evalContext.Rule.OrgId,
+ cmd := &models.SetAlertStateCommand{
+ AlertId: evalContext.Rule.ID,
+ OrgId: evalContext.Rule.OrgID,
State: evalContext.Rule.State,
Error: executionError,
EvalData: annotationData,
}
if err := bus.Dispatch(cmd); err != nil {
- if err == m.ErrCannotChangeStateOnPausedAlert {
+ if err == models.ErrCannotChangeStateOnPausedAlert {
handler.log.Error("Cannot change state on alert that's paused", "error", err)
return err
}
- if err == m.ErrRequiresNewState {
+ if err == models.ErrRequiresNewState {
handler.log.Info("Alert already updated")
return nil
}
@@ -80,10 +81,10 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
// save annotation
item := annotations.Item{
- OrgId: evalContext.Rule.OrgId,
- DashboardId: evalContext.Rule.DashboardId,
- PanelId: evalContext.Rule.PanelId,
- AlertId: evalContext.Rule.Id,
+ OrgId: evalContext.Rule.OrgID,
+ DashboardId: evalContext.Rule.DashboardID,
+ PanelId: evalContext.Rule.PanelID,
+ AlertId: evalContext.Rule.ID,
Text: "",
NewState: string(evalContext.Rule.State),
PrevState: string(evalContext.PrevAlertState),
diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go
index f62690a2d960..9a4065e279da 100644
--- a/pkg/services/alerting/rule.go
+++ b/pkg/services/alerting/rule.go
@@ -8,53 +8,59 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
)
var (
+ // ErrFrequencyCannotBeZeroOrLess frequency cannot be below zero
ErrFrequencyCannotBeZeroOrLess = errors.New(`"evaluate every" cannot be zero or below`)
- ErrFrequencyCouldNotBeParsed = errors.New(`"evaluate every" field could not be parsed`)
+
+ // ErrFrequencyCouldNotBeParsed frequency cannot be parsed
+ ErrFrequencyCouldNotBeParsed = errors.New(`"evaluate every" field could not be parsed`)
)
+// Rule is the in-memory version of an alert rule.
type Rule struct {
- Id int64
- OrgId int64
- DashboardId int64
- PanelId int64
+ ID int64
+ OrgID int64
+ DashboardID int64
+ PanelID int64
Frequency int64
Name string
Message string
LastStateChange time.Time
For time.Duration
- NoDataState m.NoDataOption
- ExecutionErrorState m.ExecutionErrorOption
- State m.AlertStateType
+ NoDataState models.NoDataOption
+ ExecutionErrorState models.ExecutionErrorOption
+ State models.AlertStateType
Conditions []Condition
Notifications []string
StateChanges int64
}
+// ValidationError is a typed error with meta data
+// about the validation error.
type ValidationError struct {
Reason string
Err error
- Alertid int64
- DashboardId int64
- PanelId int64
+ AlertID int64
+ DashboardID int64
+ PanelID int64
}
func (e ValidationError) Error() string {
extraInfo := e.Reason
- if e.Alertid != 0 {
- extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid)
+ if e.AlertID != 0 {
+ extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.AlertID)
}
- if e.PanelId != 0 {
- extraInfo = fmt.Sprintf("%s PanelId: %v", extraInfo, e.PanelId)
+ if e.PanelID != 0 {
+ extraInfo = fmt.Sprintf("%s PanelId: %v", extraInfo, e.PanelID)
}
- if e.DashboardId != 0 {
- extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardId)
+ if e.DashboardID != 0 {
+ extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardID)
}
if e.Err != nil {
@@ -65,8 +71,8 @@ func (e ValidationError) Error() string {
}
var (
- ValueFormatRegex = regexp.MustCompile(`^\d+`)
- UnitFormatRegex = regexp.MustCompile(`\w{1}$`)
+ valueFormatRegex = regexp.MustCompile(`^\d+`)
+ unitFormatRegex = regexp.MustCompile(`\w{1}$`)
)
var unitMultiplier = map[string]int{
@@ -79,7 +85,7 @@ var unitMultiplier = map[string]int{
func getTimeDurationStringToSeconds(str string) (int64, error) {
multiplier := 1
- matches := ValueFormatRegex.FindAllString(str, 1)
+ matches := valueFormatRegex.FindAllString(str, 1)
if len(matches) <= 0 {
return 0, ErrFrequencyCouldNotBeParsed
@@ -94,7 +100,7 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
return 0, ErrFrequencyCannotBeZeroOrLess
}
- unit := UnitFormatRegex.FindAllString(str, 1)[0]
+ unit := unitFormatRegex.FindAllString(str, 1)[0]
if val, ok := unitMultiplier[unit]; ok {
multiplier = val
@@ -103,19 +109,21 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
return int64(value * multiplier), nil
}
-func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
+// NewRuleFromDBAlert mappes an db version of
+// alert to an in-memory version.
+func NewRuleFromDBAlert(ruleDef *models.Alert) (*Rule, error) {
model := &Rule{}
- model.Id = ruleDef.Id
- model.OrgId = ruleDef.OrgId
- model.DashboardId = ruleDef.DashboardId
- model.PanelId = ruleDef.PanelId
+ model.ID = ruleDef.Id
+ model.OrgID = ruleDef.OrgId
+ model.DashboardID = ruleDef.DashboardId
+ model.PanelID = ruleDef.PanelId
model.Name = ruleDef.Name
model.Message = ruleDef.Message
model.State = ruleDef.State
model.LastStateChange = ruleDef.NewStateDate
model.For = ruleDef.For
- model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
- model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
+ model.NoDataState = models.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
+ model.ExecutionErrorState = models.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
model.StateChanges = ruleDef.StateChanges
model.Frequency = ruleDef.Frequency
@@ -132,7 +140,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
} else {
uid, err := jsonModel.Get("uid").String()
if err != nil {
- return nil, ValidationError{Reason: "Neither id nor uid is specified, " + err.Error(), DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+ return nil, ValidationError{Reason: "Neither id nor uid is specified, " + err.Error(), DashboardID: model.DashboardID, AlertID: model.ID, PanelID: model.PanelID}
}
model.Notifications = append(model.Notifications, uid)
}
@@ -143,11 +151,11 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
conditionType := conditionModel.Get("type").MustString()
factory, exist := conditionFactories[conditionType]
if !exist {
- return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+ return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardID: model.DashboardID, AlertID: model.ID, PanelID: model.PanelID}
}
queryCondition, err := factory(conditionModel, index)
if err != nil {
- return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+ return nil, ValidationError{Err: err, DashboardID: model.DashboardID, AlertID: model.ID, PanelID: model.PanelID}
}
model.Conditions = append(model.Conditions, queryCondition)
}
@@ -159,10 +167,12 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
return model, nil
}
+// ConditionFactory is the function signature for creating `Conditions`.
type ConditionFactory func(model *simplejson.Json, index int) (Condition, error)
var conditionFactories = make(map[string]ConditionFactory)
+// RegisterCondition adds support for alerting conditions.
func RegisterCondition(typeName string, factory ConditionFactory) {
conditionFactories[typeName] = factory
}
diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go
index ca533c36210f..853fb5d17b5c 100644
--- a/pkg/services/alerting/rule_test.go
+++ b/pkg/services/alerting/rule_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
. "github.com/smartystreets/goconvey/convey"
)
@@ -60,10 +60,10 @@ func TestAlertRuleModel(t *testing.T) {
})
Convey("can construct alert rule model", func() {
- firstNotification := m.CreateAlertNotificationCommand{OrgId: 1, Name: "1"}
+ firstNotification := models.CreateAlertNotificationCommand{OrgId: 1, Name: "1"}
err := sqlstore.CreateAlertNotificationCommand(&firstNotification)
So(err, ShouldBeNil)
- secondNotification := m.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
+ secondNotification := models.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
So(err, ShouldBeNil)
@@ -92,7 +92,7 @@ func TestAlertRuleModel(t *testing.T) {
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil)
- alert := &m.Alert{
+ alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
@@ -129,7 +129,7 @@ func TestAlertRuleModel(t *testing.T) {
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil)
- alert := &m.Alert{
+ alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
@@ -167,7 +167,7 @@ func TestAlertRuleModel(t *testing.T) {
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil)
- alert := &m.Alert{
+ alert := &models.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go
index b7555ae8d898..b01618f40955 100644
--- a/pkg/services/alerting/scheduler.go
+++ b/pkg/services/alerting/scheduler.go
@@ -4,31 +4,31 @@ import (
"math"
"time"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
-type SchedulerImpl struct {
+type schedulerImpl struct {
jobs map[int64]*Job
log log.Logger
}
-func NewScheduler() Scheduler {
- return &SchedulerImpl{
+func newScheduler() scheduler {
+ return &schedulerImpl{
jobs: make(map[int64]*Job),
log: log.New("alerting.scheduler"),
}
}
-func (s *SchedulerImpl) Update(rules []*Rule) {
+func (s *schedulerImpl) Update(rules []*Rule) {
s.log.Debug("Scheduling update", "ruleCount", len(rules))
jobs := make(map[int64]*Job)
for i, rule := range rules {
var job *Job
- if s.jobs[rule.Id] != nil {
- job = s.jobs[rule.Id]
+ if s.jobs[rule.ID] != nil {
+ job = s.jobs[rule.ID]
} else {
job = &Job{
Running: false,
@@ -42,13 +42,13 @@ func (s *SchedulerImpl) Update(rules []*Rule) {
if job.Offset == 0 { //zero offset causes division with 0 panics.
job.Offset = 1
}
- jobs[rule.Id] = job
+ jobs[rule.ID] = job
}
s.jobs = jobs
}
-func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
+func (s *schedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
now := tickTime.Unix()
for _, job := range s.jobs {
@@ -72,7 +72,7 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
}
}
-func (s *SchedulerImpl) enqueue(job *Job, execQueue chan *Job) {
- s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.Id)
+func (s *schedulerImpl) enqueue(job *Job, execQueue chan *Job) {
+ s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.ID)
execQueue <- job
}
diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go
index 22f6a2118b7d..311109ed6078 100644
--- a/pkg/services/alerting/test_notification.go
+++ b/pkg/services/alerting/test_notification.go
@@ -7,12 +7,14 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
)
+// NotificationTestCommand initiates an test
+// execution of an alert notification.
type NotificationTestCommand struct {
- State m.AlertStateType
+ State models.AlertStateType
Name string
Type string
Settings *simplejson.Json
@@ -27,9 +29,9 @@ func init() {
}
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
- notifier := NewNotificationService(nil).(*notificationService)
+ notifier := newNotificationService(nil)
- model := &m.AlertNotification{
+ model := &models.AlertNotification{
Name: cmd.Name,
Type: cmd.Type,
Settings: cmd.Settings,
@@ -47,16 +49,16 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
testRule := &Rule{
- DashboardId: 1,
- PanelId: 1,
+ DashboardID: 1,
+ PanelID: 1,
Name: "Test notification",
Message: "Someone is testing the alert notification within grafana.",
- State: m.AlertStateAlerting,
+ State: models.AlertStateAlerting,
}
ctx := NewEvalContext(context.Background(), testRule)
if cmd.Settings.Get("uploadImage").MustBool(true) {
- ctx.ImagePublicUrl = "https://grafana.com/assets/img/blog/mixed_styles.png"
+ ctx.ImagePublicURL = "https://grafana.com/assets/img/blog/mixed_styles.png"
}
ctx.IsTestRun = true
ctx.Firing = true
diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go
index 360ee065de0f..1575490ea324 100644
--- a/pkg/services/alerting/test_rule.go
+++ b/pkg/services/alerting/test_rule.go
@@ -6,14 +6,16 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
)
+// AlertTestCommand initiates an test evaluation
+// of an alert rule.
type AlertTestCommand struct {
Dashboard *simplejson.Json
- PanelId int64
- OrgId int64
- User *m.SignedInUser
+ PanelID int64
+ OrgID int64
+ User *models.SignedInUser
Result *EvalContext
}
@@ -24,16 +26,16 @@ func init() {
func handleAlertTestCommand(cmd *AlertTestCommand) error {
- dash := m.NewDashboardFromJson(cmd.Dashboard)
+ dash := models.NewDashboardFromJson(cmd.Dashboard)
- extractor := NewDashAlertExtractor(dash, cmd.OrgId, cmd.User)
+ extractor := NewDashAlertExtractor(dash, cmd.OrgID, cmd.User)
alerts, err := extractor.GetAlerts()
if err != nil {
return err
}
for _, alert := range alerts {
- if alert.PanelId == cmd.PanelId {
+ if alert.PanelId == cmd.PanelID {
rule, err := NewRuleFromDBAlert(alert)
if err != nil {
return err
@@ -44,7 +46,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
}
}
- return fmt.Errorf("Could not find alert with panel id %d", cmd.PanelId)
+ return fmt.Errorf("Could not find alert with panel id %d", cmd.PanelID)
}
func testAlertRule(rule *Rule) *EvalContext {
diff --git a/pkg/services/alerting/ticker.go b/pkg/services/alerting/ticker.go
index 8cee2653ee9d..9702a2cda63a 100644
--- a/pkg/services/alerting/ticker.go
+++ b/pkg/services/alerting/ticker.go
@@ -6,7 +6,7 @@ import (
"github.com/benbjohnson/clock"
)
-// ticker is a ticker to power the alerting scheduler. it's like a time.Ticker, except:
+// Ticker is a ticker to power the alerting scheduler. it's like a time.Ticker, except:
// * it doesn't drop ticks for slow receivers, rather, it queues up. so that callers are in control to instrument what's going on.
// * it automatically ticks every second, which is the right thing in our current design
// * it ticks on second marks or very shortly after. this provides a predictable load pattern
diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go
index dc9936f2f3ff..af23d773f65c 100644
--- a/pkg/services/auth/auth_token.go
+++ b/pkg/services/auth/auth_token.go
@@ -4,11 +4,12 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
+ "strings"
"time"
"github.com/grafana/grafana/pkg/infra/serverlock"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -305,6 +306,36 @@ func (s *UserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId i
})
}
+func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, userIds []int64) error {
+ return s.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
+ if len(userIds) == 0 {
+ return nil
+ }
+
+ user_id_params := strings.Repeat(",?", len(userIds)-1)
+ sql := "DELETE from user_auth_token WHERE user_id IN (?" + user_id_params + ")"
+
+ params := []interface{}{sql}
+ for _, v := range userIds {
+ params = append(params, v)
+ }
+
+ res, err := dbSession.Exec(params...)
+ if err != nil {
+ return err
+ }
+
+ affected, err := res.RowsAffected()
+ if err != nil {
+ return err
+ }
+
+ s.log.Debug("all user tokens for given users revoked", "usersCount", len(userIds), "count", affected)
+
+ return err
+ })
+}
+
func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) {
var result models.UserToken
diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go
index b1398834bdc9..bf12d914e970 100644
--- a/pkg/services/auth/auth_token_test.go
+++ b/pkg/services/auth/auth_token_test.go
@@ -9,7 +9,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/setting"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
. "github.com/smartystreets/goconvey/convey"
@@ -117,6 +117,26 @@ func TestUserAuthToken(t *testing.T) {
So(model2, ShouldBeNil)
})
})
+
+ Convey("When revoking users tokens in a batch", func() {
+ Convey("Can revoke all users tokens", func() {
+ userIds := []int64{}
+ for i := 0; i < 3; i++ {
+ userId := userID + int64(i+1)
+ userIds = append(userIds, userId)
+ userAuthTokenService.CreateToken(context.Background(), userId, "192.168.10.11:1234", "some user agent")
+ }
+
+ err := userAuthTokenService.BatchRevokeAllUserTokens(context.Background(), userIds)
+ So(err, ShouldBeNil)
+
+ for _, v := range userIds {
+ tokens, err := userAuthTokenService.GetUserTokens(context.Background(), v)
+ So(err, ShouldBeNil)
+ So(len(tokens), ShouldEqual, 0)
+ }
+ })
+ })
})
Convey("expires correctly", func() {
diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go
index 63d829bccec1..6e07a76f8002 100644
--- a/pkg/services/cleanup/cleanup.go
+++ b/pkg/services/cleanup/cleanup.go
@@ -8,8 +8,8 @@ import (
"time"
"github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
- "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go
index 884d993f43c6..bec718700a78 100644
--- a/pkg/services/dashboards/dashboard_service.go
+++ b/pkg/services/dashboards/dashboard_service.go
@@ -5,7 +5,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go
index 366bc90fc377..4d242722d6b1 100644
--- a/pkg/services/guardian/guardian.go
+++ b/pkg/services/guardian/guardian.go
@@ -4,7 +4,7 @@ import (
"errors"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
diff --git a/pkg/services/ldap/hooks.go b/pkg/services/ldap/hooks.go
deleted file mode 100644
index ece98e5e73b0..000000000000
--- a/pkg/services/ldap/hooks.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package ldap
-
-var (
- hookDial func(*Auth) error
-)
diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go
index 5ff74bd7a3e9..418673be4463 100644
--- a/pkg/services/ldap/ldap.go
+++ b/pkg/services/ldap/ldap.go
@@ -8,39 +8,37 @@ import (
"io/ioutil"
"strings"
- "github.com/davecgh/go-spew/spew"
- LDAP "gopkg.in/ldap.v3"
+ "gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- models "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/setting"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
)
// IConnection is interface for LDAP connection manipulation
type IConnection interface {
Bind(username, password string) error
UnauthenticatedBind(username string) error
- Search(*LDAP.SearchRequest) (*LDAP.SearchResult, error)
+ Add(*ldap.AddRequest) error
+ Del(*ldap.DelRequest) error
+ Search(*ldap.SearchRequest) (*ldap.SearchResult, error)
StartTLS(*tls.Config) error
Close()
}
-// IAuth is interface for LDAP authorization
-type IAuth interface {
- Login(query *models.LoginUserQuery) error
- SyncUser(query *models.LoginUserQuery) error
- GetGrafanaUserFor(
- ctx *models.ReqContext,
- user *UserInfo,
- ) (*models.User, error)
- Users() ([]*UserInfo, error)
+// IServer is interface for LDAP authorization
+type IServer interface {
+ Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error)
+ Users([]string) ([]*models.ExternalUserInfo, error)
+ InitialBind(string, string) error
+ Dial() error
+ Close()
}
-// Auth is basic struct of LDAP authorization
-type Auth struct {
- server *ServerConfig
- conn IConnection
+// Server is basic struct of LDAP authorization
+type Server struct {
+ Config *ServerConfig
+ Connection IConnection
requireSecondBind bool
log log.Logger
}
@@ -52,28 +50,24 @@ var (
)
var dial = func(network, addr string) (IConnection, error) {
- return LDAP.Dial(network, addr)
+ return ldap.Dial(network, addr)
}
// New creates the new LDAP auth
-func New(server *ServerConfig) IAuth {
- return &Auth{
- server: server,
+func New(config *ServerConfig) IServer {
+ return &Server{
+ Config: config,
log: log.New("ldap"),
}
}
// Dial dials in the LDAP
-func (auth *Auth) Dial() error {
- if hookDial != nil {
- return hookDial(auth)
- }
-
+func (server *Server) Dial() error {
var err error
var certPool *x509.CertPool
- if auth.server.RootCACert != "" {
+ if server.Config.RootCACert != "" {
certPool = x509.NewCertPool()
- for _, caCertFile := range strings.Split(auth.server.RootCACert, " ") {
+ for _, caCertFile := range strings.Split(server.Config.RootCACert, " ") {
pem, err := ioutil.ReadFile(caCertFile)
if err != nil {
return err
@@ -84,35 +78,35 @@ func (auth *Auth) Dial() error {
}
}
var clientCert tls.Certificate
- if auth.server.ClientCert != "" && auth.server.ClientKey != "" {
- clientCert, err = tls.LoadX509KeyPair(auth.server.ClientCert, auth.server.ClientKey)
+ if server.Config.ClientCert != "" && server.Config.ClientKey != "" {
+ clientCert, err = tls.LoadX509KeyPair(server.Config.ClientCert, server.Config.ClientKey)
if err != nil {
return err
}
}
- for _, host := range strings.Split(auth.server.Host, " ") {
- address := fmt.Sprintf("%s:%d", host, auth.server.Port)
- if auth.server.UseSSL {
+ for _, host := range strings.Split(server.Config.Host, " ") {
+ address := fmt.Sprintf("%s:%d", host, server.Config.Port)
+ if server.Config.UseSSL {
tlsCfg := &tls.Config{
- InsecureSkipVerify: auth.server.SkipVerifySSL,
+ InsecureSkipVerify: server.Config.SkipVerifySSL,
ServerName: host,
RootCAs: certPool,
}
if len(clientCert.Certificate) > 0 {
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
}
- if auth.server.StartTLS {
- auth.conn, err = dial("tcp", address)
+ if server.Config.StartTLS {
+ server.Connection, err = dial("tcp", address)
if err == nil {
- if err = auth.conn.StartTLS(tlsCfg); err == nil {
+ if err = server.Connection.StartTLS(tlsCfg); err == nil {
return nil
}
}
} else {
- auth.conn, err = LDAP.DialTLS("tcp", address, tlsCfg)
+ server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg)
}
} else {
- auth.conn, err = dial("tcp", address)
+ server.Connection, err = dial("tcp", address)
}
if err == nil {
@@ -122,91 +116,180 @@ func (auth *Auth) Dial() error {
return err
}
-// Login logs in the user
-func (auth *Auth) Login(query *models.LoginUserQuery) error {
- // connect to ldap server
- if err := auth.Dial(); err != nil {
- return err
- }
- defer auth.conn.Close()
+// Close closes the LDAP connection
+func (server *Server) Close() {
+ server.Connection.Close()
+}
- // perform initial authentication
- if err := auth.initialBind(query.Username, query.Password); err != nil {
- return err
+// Login user by searching and serializing it
+func (server *Server) Login(query *models.LoginUserQuery) (
+ *models.ExternalUserInfo, error,
+) {
+
+ // Perform initial authentication
+ err := server.InitialBind(query.Username, query.Password)
+ if err != nil {
+ return nil, err
}
- // find user entry & attributes
- user, err := auth.searchForUser(query.Username)
+ // Find user entry & attributes
+ users, err := server.Users([]string{query.Username})
if err != nil {
- return err
+ return nil, err
+ }
+
+ // If we couldn't find the user -
+ // we should show incorrect credentials err
+ if len(users) == 0 {
+ server.disableExternalUser(query.Username)
+ return nil, ErrInvalidCredentials
}
- auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
+ // Check if a second user bind is needed
+ user := users[0]
- // check if a second user bind is needed
- if auth.requireSecondBind {
- err = auth.secondBind(user, query.Password)
+ if err := server.validateGrafanaUser(user); err != nil {
+ return nil, err
+ }
+
+ if server.requireSecondBind {
+ err = server.secondBind(user, query.Password)
if err != nil {
- return err
+ return nil, err
+ }
+ }
+
+ return user, nil
+}
+
+// Users gets LDAP users
+func (server *Server) Users(logins []string) (
+ []*models.ExternalUserInfo,
+ error,
+) {
+ var result *ldap.SearchResult
+ var err error
+ var Config = server.Config
+
+ for _, base := range Config.SearchBaseDNs {
+ result, err = server.Connection.Search(
+ server.getSearchRequest(base, logins),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ if len(result.Entries) > 0 {
+ break
}
}
- grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
+ serializedUsers, err := server.serializeUsers(result)
if err != nil {
- return err
+ return nil, err
+ }
+
+ return serializedUsers, nil
+}
+
+// validateGrafanaUser validates user access.
+// If there are no ldap group mappings access is true
+// otherwise a single group must match
+func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error {
+ if len(server.Config.Groups) > 0 && len(user.OrgRoles) < 1 {
+ server.log.Error(
+ "user does not belong in any of the specified LDAP groups",
+ "username", user.Login,
+ "groups", user.Groups,
+ )
+ return ErrInvalidCredentials
}
- query.User = grafanaUser
return nil
}
-// SyncUser syncs user with Grafana
-func (auth *Auth) SyncUser(query *models.LoginUserQuery) error {
- // connect to ldap server
- err := auth.Dial()
- if err != nil {
- return err
+// disableExternalUser marks external user as disabled in Grafana db
+func (server *Server) disableExternalUser(username string) error {
+ // Check if external user exist in Grafana
+ userQuery := &models.GetExternalUserInfoByLoginQuery{
+ LoginOrEmail: username,
}
- defer auth.conn.Close()
- err = auth.serverBind()
- if err != nil {
+ if err := bus.Dispatch(userQuery); err != nil {
return err
}
- // find user entry & attributes
- user, err := auth.searchForUser(query.Username)
- if err != nil {
- auth.log.Error("Failed searching for user in ldap", "error", err)
- return err
+ userInfo := userQuery.Result
+ if !userInfo.IsDisabled {
+ server.log.Debug("Disabling external user", "user", userQuery.Result.Login)
+ // Mark user as disabled in grafana db
+ disableUserCmd := &models.DisableUserCommand{
+ UserId: userQuery.Result.UserId,
+ IsDisabled: true,
+ }
+
+ if err := bus.Dispatch(disableUserCmd); err != nil {
+ server.log.Debug("Error disabling external user", "user", userQuery.Result.Login, "message", err.Error())
+ return err
+ }
}
+ return nil
+}
- auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
+// getSearchRequest returns LDAP search request for users
+func (server *Server) getSearchRequest(
+ base string,
+ logins []string,
+) *ldap.SearchRequest {
+ attributes := []string{}
+
+ inputs := server.Config.Attr
+ attributes = appendIfNotEmpty(
+ attributes,
+ inputs.Username,
+ inputs.Surname,
+ inputs.Email,
+ inputs.Name,
+ inputs.MemberOf,
+ )
+
+ search := ""
+ for _, login := range logins {
+ query := strings.Replace(
+ server.Config.SearchFilter,
+ "%s", ldap.EscapeFilter(login),
+ -1,
+ )
- grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
- if err != nil {
- return err
+ search = search + query
}
- query.User = grafanaUser
- return nil
+ filter := fmt.Sprintf("(|%s)", search)
+
+ return &ldap.SearchRequest{
+ BaseDN: base,
+ Scope: ldap.ScopeWholeSubtree,
+ DerefAliases: ldap.NeverDerefAliases,
+ Attributes: attributes,
+ Filter: filter,
+ }
}
-func (auth *Auth) GetGrafanaUserFor(
- ctx *models.ReqContext,
- user *UserInfo,
-) (*models.User, error) {
+// buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo
+func (server *Server) buildGrafanaUser(user *UserInfo) *models.ExternalUserInfo {
extUser := &models.ExternalUserInfo{
- AuthModule: "ldap",
+ AuthModule: models.AuthModuleLDAP,
AuthId: user.DN,
- Name: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
- Login: user.Username,
- Email: user.Email,
- Groups: user.MemberOf,
- OrgRoles: map[int64]models.RoleType{},
+ Name: strings.TrimSpace(
+ fmt.Sprintf("%s %s", user.FirstName, user.LastName),
+ ),
+ Login: user.Username,
+ Email: user.Email,
+ Groups: user.MemberOf,
+ OrgRoles: map[int64]models.RoleType{},
}
- for _, group := range auth.server.Groups {
+ for _, group := range server.Config.Groups {
// only use the first match for each org
if extUser.OrgRoles[group.OrgId] != "" {
continue
@@ -220,49 +303,28 @@ func (auth *Auth) GetGrafanaUserFor(
}
}
- // validate that the user has access
- // if there are no ldap group mappings access is true
- // otherwise a single group must match
- if len(auth.server.Groups) > 0 && len(extUser.OrgRoles) < 1 {
- auth.log.Info(
- "Ldap Auth: user does not belong in any of the specified ldap groups",
- "username", user.Username,
- "groups", user.MemberOf,
- )
- return nil, ErrInvalidCredentials
- }
-
- // add/update user in grafana
- upsertUserCmd := &models.UpsertUserCommand{
- ReqContext: ctx,
- ExternalUser: extUser,
- SignupAllowed: setting.LdapAllowSignup,
- }
-
- err := bus.Dispatch(upsertUserCmd)
- if err != nil {
- return nil, err
- }
-
- return upsertUserCmd.Result, nil
+ return extUser
}
-func (auth *Auth) serverBind() error {
+func (server *Server) serverBind() error {
bindFn := func() error {
- return auth.conn.Bind(auth.server.BindDN, auth.server.BindPassword)
+ return server.Connection.Bind(
+ server.Config.BindDN,
+ server.Config.BindPassword,
+ )
}
- if auth.server.BindPassword == "" {
+ if server.Config.BindPassword == "" {
bindFn = func() error {
- return auth.conn.UnauthenticatedBind(auth.server.BindDN)
+ return server.Connection.UnauthenticatedBind(server.Config.BindDN)
}
}
// bind_dn and bind_password to bind
if err := bindFn(); err != nil {
- auth.log.Info("LDAP initial bind failed, %v", err)
+ server.log.Info("LDAP initial bind failed, %v", err)
- if ldapErr, ok := err.(*LDAP.Error); ok {
+ if ldapErr, ok := err.(*ldap.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
@@ -273,11 +335,15 @@ func (auth *Auth) serverBind() error {
return nil
}
-func (auth *Auth) secondBind(user *UserInfo, userPassword string) error {
- if err := auth.conn.Bind(user.DN, userPassword); err != nil {
- auth.log.Info("Second bind failed", "error", err)
+func (server *Server) secondBind(
+ user *models.ExternalUserInfo,
+ userPassword string,
+) error {
+ err := server.Connection.Bind(user.AuthId, userPassword)
+ if err != nil {
+ server.log.Info("Second bind failed", "error", err)
- if ldapErr, ok := err.(*LDAP.Error); ok {
+ if ldapErr, ok := err.(*ldap.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
@@ -288,31 +354,32 @@ func (auth *Auth) secondBind(user *UserInfo, userPassword string) error {
return nil
}
-func (auth *Auth) initialBind(username, userPassword string) error {
- if auth.server.BindPassword != "" || auth.server.BindDN == "" {
- userPassword = auth.server.BindPassword
- auth.requireSecondBind = true
+// InitialBind intiates first bind to LDAP server
+func (server *Server) InitialBind(username, userPassword string) error {
+ if server.Config.BindPassword != "" || server.Config.BindDN == "" {
+ userPassword = server.Config.BindPassword
+ server.requireSecondBind = true
}
- bindPath := auth.server.BindDN
+ bindPath := server.Config.BindDN
if strings.Contains(bindPath, "%s") {
- bindPath = fmt.Sprintf(auth.server.BindDN, username)
+ bindPath = fmt.Sprintf(server.Config.BindDN, username)
}
bindFn := func() error {
- return auth.conn.Bind(bindPath, userPassword)
+ return server.Connection.Bind(bindPath, userPassword)
}
if userPassword == "" {
bindFn = func() error {
- return auth.conn.UnauthenticatedBind(bindPath)
+ return server.Connection.UnauthenticatedBind(bindPath)
}
}
if err := bindFn(); err != nil {
- auth.log.Info("Initial bind failed", "error", err)
+ server.log.Info("Initial bind failed", "error", err)
- if ldapErr, ok := err.(*LDAP.Error); ok {
+ if ldapErr, ok := err.(*ldap.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
@@ -323,199 +390,124 @@ func (auth *Auth) initialBind(username, userPassword string) error {
return nil
}
-func (auth *Auth) searchForUser(username string) (*UserInfo, error) {
- var searchResult *LDAP.SearchResult
- var err error
-
- for _, searchBase := range auth.server.SearchBaseDNs {
- attributes := make([]string, 0)
- inputs := auth.server.Attr
- attributes = appendIfNotEmpty(attributes,
- inputs.Username,
- inputs.Surname,
- inputs.Email,
- inputs.Name,
- inputs.MemberOf)
-
- searchReq := LDAP.SearchRequest{
- BaseDN: searchBase,
- Scope: LDAP.ScopeWholeSubtree,
- DerefAliases: LDAP.NeverDerefAliases,
- Attributes: attributes,
- Filter: strings.Replace(
- auth.server.SearchFilter,
- "%s", LDAP.EscapeFilter(username),
- -1,
- ),
- }
-
- auth.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
-
- searchResult, err = auth.conn.Search(&searchReq)
- if err != nil {
- return nil, err
- }
-
- if len(searchResult.Entries) > 0 {
- break
- }
- }
-
- if len(searchResult.Entries) == 0 {
- return nil, ErrInvalidCredentials
- }
-
- if len(searchResult.Entries) > 1 {
- return nil, errors.New("Ldap search matched more than one entry, please review your filter setting")
- }
-
+// requestMemberOf use this function when POSIX LDAP schema does not support memberOf, so it manually search the groups
+func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string, error) {
var memberOf []string
- if auth.server.GroupSearchFilter == "" {
- memberOf = getLdapAttrArray(auth.server.Attr.MemberOf, searchResult)
- } else {
- // If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
- var groupSearchResult *LDAP.SearchResult
- for _, groupSearchBase := range auth.server.GroupSearchBaseDNs {
- var filter_replace string
- if auth.server.GroupSearchFilterUserAttribute == "" {
- filter_replace = getLdapAttr(auth.server.Attr.Username, searchResult)
- } else {
- filter_replace = getLdapAttr(auth.server.GroupSearchFilterUserAttribute, searchResult)
- }
- filter := strings.Replace(
- auth.server.GroupSearchFilter, "%s",
- LDAP.EscapeFilter(filter_replace),
- -1,
- )
-
- auth.log.Info("Searching for user's groups", "filter", filter)
-
- // support old way of reading settings
- groupIdAttribute := auth.server.Attr.MemberOf
- // but prefer dn attribute if default settings are used
- if groupIdAttribute == "" || groupIdAttribute == "memberOf" {
- groupIdAttribute = "dn"
- }
-
- groupSearchReq := LDAP.SearchRequest{
- BaseDN: groupSearchBase,
- Scope: LDAP.ScopeWholeSubtree,
- DerefAliases: LDAP.NeverDerefAliases,
- Attributes: []string{groupIdAttribute},
- Filter: filter,
- }
-
- groupSearchResult, err = auth.conn.Search(&groupSearchReq)
- if err != nil {
- return nil, err
- }
-
- if len(groupSearchResult.Entries) > 0 {
- for i := range groupSearchResult.Entries {
- memberOf = append(memberOf, getLdapAttrN(groupIdAttribute, groupSearchResult, i))
- }
- break
- }
+ for _, groupSearchBase := range server.Config.GroupSearchBaseDNs {
+ var filterReplace string
+ if server.Config.GroupSearchFilterUserAttribute == "" {
+ filterReplace = getLDAPAttr(server.Config.Attr.Username, searchResult)
+ } else {
+ filterReplace = getLDAPAttr(server.Config.GroupSearchFilterUserAttribute, searchResult)
}
- }
-
- return &UserInfo{
- DN: searchResult.Entries[0].DN,
- LastName: getLdapAttr(auth.server.Attr.Surname, searchResult),
- FirstName: getLdapAttr(auth.server.Attr.Name, searchResult),
- Username: getLdapAttr(auth.server.Attr.Username, searchResult),
- Email: getLdapAttr(auth.server.Attr.Email, searchResult),
- MemberOf: memberOf,
- }, nil
-}
-
-func (ldap *Auth) Users() ([]*UserInfo, error) {
- var result *LDAP.SearchResult
- var err error
- server := ldap.server
- if err := ldap.Dial(); err != nil {
- return nil, err
- }
- defer ldap.conn.Close()
-
- for _, base := range server.SearchBaseDNs {
- attributes := make([]string, 0)
- inputs := server.Attr
- attributes = appendIfNotEmpty(
- attributes,
- inputs.Username,
- inputs.Surname,
- inputs.Email,
- inputs.Name,
- inputs.MemberOf,
+ filter := strings.Replace(
+ server.Config.GroupSearchFilter, "%s",
+ ldap.EscapeFilter(filterReplace),
+ -1,
)
- req := LDAP.SearchRequest{
- BaseDN: base,
- Scope: LDAP.ScopeWholeSubtree,
- DerefAliases: LDAP.NeverDerefAliases,
- Attributes: attributes,
+ server.log.Info("Searching for user's groups", "filter", filter)
+
+ // support old way of reading settings
+ groupIDAttribute := server.Config.Attr.MemberOf
+ // but prefer dn attribute if default settings are used
+ if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
+ groupIDAttribute = "dn"
+ }
- // Doing a star here to get all the users in one go
- Filter: strings.Replace(server.SearchFilter, "%s", "*", -1),
+ groupSearchReq := ldap.SearchRequest{
+ BaseDN: groupSearchBase,
+ Scope: ldap.ScopeWholeSubtree,
+ DerefAliases: ldap.NeverDerefAliases,
+ Attributes: []string{groupIDAttribute},
+ Filter: filter,
}
- result, err = ldap.conn.Search(&req)
+ groupSearchResult, err := server.Connection.Search(&groupSearchReq)
if err != nil {
return nil, err
}
- if len(result.Entries) > 0 {
+ if len(groupSearchResult.Entries) > 0 {
+ for i := range groupSearchResult.Entries {
+ memberOf = append(memberOf, getLDAPAttrN(groupIDAttribute, groupSearchResult, i))
+ }
break
}
}
- return ldap.serializeUsers(result), nil
+ return memberOf, nil
}
-func (ldap *Auth) serializeUsers(users *LDAP.SearchResult) []*UserInfo {
- var serialized []*UserInfo
+// serializeUsers serializes the users
+// from LDAP result to ExternalInfo struct
+func (server *Server) serializeUsers(
+ users *ldap.SearchResult,
+) ([]*models.ExternalUserInfo, error) {
+ var serialized []*models.ExternalUserInfo
for index := range users.Entries {
- serialize := &UserInfo{
- DN: getLdapAttrN(
+ memberOf, err := server.getMemberOf(users)
+ if err != nil {
+ return nil, err
+ }
+
+ userInfo := &UserInfo{
+ DN: getLDAPAttrN(
"dn",
users,
index,
),
- LastName: getLdapAttrN(
- ldap.server.Attr.Surname,
- users,
- index,
- ),
- FirstName: getLdapAttrN(
- ldap.server.Attr.Name,
+ LastName: getLDAPAttrN(
+ server.Config.Attr.Surname,
users,
index,
),
- Username: getLdapAttrN(
- ldap.server.Attr.Username,
+ FirstName: getLDAPAttrN(
+ server.Config.Attr.Name,
users,
index,
),
- Email: getLdapAttrN(
- ldap.server.Attr.Email,
+ Username: getLDAPAttrN(
+ server.Config.Attr.Username,
users,
index,
),
- MemberOf: getLdapAttrArrayN(
- ldap.server.Attr.MemberOf,
+ Email: getLDAPAttrN(
+ server.Config.Attr.Email,
users,
index,
),
+ MemberOf: memberOf,
}
- serialized = append(serialized, serialize)
+ serialized = append(
+ serialized,
+ server.buildGrafanaUser(userInfo),
+ )
+ }
+
+ return serialized, nil
+}
+
+// getMemberOf finds memberOf property or request it
+func (server *Server) getMemberOf(search *ldap.SearchResult) (
+ []string, error,
+) {
+ if server.Config.GroupSearchFilter == "" {
+ memberOf := getLDAPAttrArray(server.Config.Attr.MemberOf, search)
+
+ return memberOf, nil
+ }
+
+ memberOf, err := server.requestMemberOf(search)
+ if err != nil {
+ return nil, err
}
- return serialized
+ return memberOf, nil
}
func appendIfNotEmpty(slice []string, values ...string) []string {
@@ -527,11 +519,11 @@ func appendIfNotEmpty(slice []string, values ...string) []string {
return slice
}
-func getLdapAttr(name string, result *LDAP.SearchResult) string {
- return getLdapAttrN(name, result, 0)
+func getLDAPAttr(name string, result *ldap.SearchResult) string {
+ return getLDAPAttrN(name, result, 0)
}
-func getLdapAttrN(name string, result *LDAP.SearchResult, n int) string {
+func getLDAPAttrN(name string, result *ldap.SearchResult, n int) string {
if strings.ToLower(name) == "dn" {
return result.Entries[n].DN
}
@@ -545,11 +537,11 @@ func getLdapAttrN(name string, result *LDAP.SearchResult, n int) string {
return ""
}
-func getLdapAttrArray(name string, result *LDAP.SearchResult) []string {
- return getLdapAttrArrayN(name, result, 0)
+func getLDAPAttrArray(name string, result *ldap.SearchResult) []string {
+ return getLDAPAttrArrayN(name, result, 0)
}
-func getLdapAttrArrayN(name string, result *LDAP.SearchResult, n int) []string {
+func getLDAPAttrArrayN(name string, result *ldap.SearchResult, n int) []string {
for _, attr := range result.Entries[n].Attributes {
if attr.Name == name {
return attr.Values
diff --git a/pkg/services/ldap/ldap_helpers_test.go b/pkg/services/ldap/ldap_helpers_test.go
new file mode 100644
index 000000000000..48e6bce8b5ba
--- /dev/null
+++ b/pkg/services/ldap/ldap_helpers_test.go
@@ -0,0 +1,140 @@
+package ldap
+
+import (
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+ "gopkg.in/ldap.v3"
+
+ "github.com/grafana/grafana/pkg/infra/log"
+)
+
+func TestLDAPHelpers(t *testing.T) {
+ Convey("serializeUsers()", t, func() {
+ Convey("simple case", func() {
+ server := &Server{
+ Config: &ServerConfig{
+ Attr: AttributeMap{
+ Username: "username",
+ Name: "name",
+ MemberOf: "memberof",
+ Email: "email",
+ },
+ SearchBaseDNs: []string{"BaseDNHere"},
+ },
+ Connection: &MockConnection{},
+ log: log.New("test-logger"),
+ }
+
+ entry := ldap.Entry{
+ DN: "dn", Attributes: []*ldap.EntryAttribute{
+ {Name: "username", Values: []string{"roelgerrits"}},
+ {Name: "surname", Values: []string{"Gerrits"}},
+ {Name: "email", Values: []string{"roel@test.com"}},
+ {Name: "name", Values: []string{"Roel"}},
+ {Name: "memberof", Values: []string{"admins"}},
+ }}
+ users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+
+ result, err := server.serializeUsers(users)
+
+ So(err, ShouldBeNil)
+ So(result[0].Login, ShouldEqual, "roelgerrits")
+ So(result[0].Email, ShouldEqual, "roel@test.com")
+ So(result[0].Groups, ShouldContain, "admins")
+ })
+
+ Convey("without lastname", func() {
+ server := &Server{
+ Config: &ServerConfig{
+ Attr: AttributeMap{
+ Username: "username",
+ Name: "name",
+ MemberOf: "memberof",
+ Email: "email",
+ },
+ SearchBaseDNs: []string{"BaseDNHere"},
+ },
+ Connection: &MockConnection{},
+ log: log.New("test-logger"),
+ }
+
+ entry := ldap.Entry{
+ DN: "dn", Attributes: []*ldap.EntryAttribute{
+ {Name: "username", Values: []string{"roelgerrits"}},
+ {Name: "email", Values: []string{"roel@test.com"}},
+ {Name: "name", Values: []string{"Roel"}},
+ {Name: "memberof", Values: []string{"admins"}},
+ }}
+ users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+
+ result, err := server.serializeUsers(users)
+
+ So(err, ShouldBeNil)
+ So(result[0].Name, ShouldEqual, "Roel")
+ })
+ })
+
+ Convey("serverBind()", t, func() {
+ Convey("Given bind dn and password configured", func() {
+ connection := &MockConnection{}
+ var actualUsername, actualPassword string
+ connection.bindProvider = func(username, password string) error {
+ actualUsername = username
+ actualPassword = password
+ return nil
+ }
+ server := &Server{
+ Connection: connection,
+ Config: &ServerConfig{
+ BindDN: "o=users,dc=grafana,dc=org",
+ BindPassword: "bindpwd",
+ },
+ }
+ err := server.serverBind()
+ So(err, ShouldBeNil)
+ So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
+ So(actualPassword, ShouldEqual, "bindpwd")
+ })
+
+ Convey("Given bind dn configured", func() {
+ connection := &MockConnection{}
+ unauthenticatedBindWasCalled := false
+ var actualUsername string
+ connection.unauthenticatedBindProvider = func(username string) error {
+ unauthenticatedBindWasCalled = true
+ actualUsername = username
+ return nil
+ }
+ server := &Server{
+ Connection: connection,
+ Config: &ServerConfig{
+ BindDN: "o=users,dc=grafana,dc=org",
+ },
+ }
+ err := server.serverBind()
+ So(err, ShouldBeNil)
+ So(unauthenticatedBindWasCalled, ShouldBeTrue)
+ So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
+ })
+
+ Convey("Given empty bind dn and password", func() {
+ connection := &MockConnection{}
+ unauthenticatedBindWasCalled := false
+ var actualUsername string
+ connection.unauthenticatedBindProvider = func(username string) error {
+ unauthenticatedBindWasCalled = true
+ actualUsername = username
+ return nil
+ }
+ server := &Server{
+ Connection: connection,
+ Config: &ServerConfig{},
+ }
+ err := server.serverBind()
+ So(err, ShouldBeNil)
+ So(unauthenticatedBindWasCalled, ShouldBeTrue)
+ So(actualUsername, ShouldBeEmpty)
+ })
+ })
+}
diff --git a/pkg/services/ldap/ldap_login_test.go b/pkg/services/ldap/ldap_login_test.go
index 5afed5462463..573a9a560e84 100644
--- a/pkg/services/ldap/ldap_login_test.go
+++ b/pkg/services/ldap/ldap_login_test.go
@@ -6,24 +6,95 @@ import (
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/ldap.v3"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/user"
)
-func TestLdapLogin(t *testing.T) {
- Convey("Login using ldap", t, func() {
- AuthScenario("When login with invalid credentials", func(scenario *scenarioContext) {
- conn := &mockLdapConn{}
+func TestLDAPLogin(t *testing.T) {
+ Convey("Login()", t, func() {
+ serverScenario("When user is log in and updated", func(sc *scenarioContext) {
+ // arrange
+ mockConnection := &MockConnection{}
+
+ server := &Server{
+ Config: &ServerConfig{
+ Host: "",
+ RootCACert: "",
+ Groups: []*GroupToOrgRole{
+ {GroupDN: "*", OrgRole: "Admin"},
+ },
+ Attr: AttributeMap{
+ Username: "username",
+ Surname: "surname",
+ Email: "email",
+ Name: "name",
+ MemberOf: "memberof",
+ },
+ SearchBaseDNs: []string{"BaseDNHere"},
+ },
+ Connection: mockConnection,
+ log: log.New("test-logger"),
+ }
+
+ entry := ldap.Entry{
+ DN: "dn", Attributes: []*ldap.EntryAttribute{
+ {Name: "username", Values: []string{"roelgerrits"}},
+ {Name: "surname", Values: []string{"Gerrits"}},
+ {Name: "email", Values: []string{"roel@test.com"}},
+ {Name: "name", Values: []string{"Roel"}},
+ {Name: "memberof", Values: []string{"admins"}},
+ }}
+ result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+ mockConnection.setSearchResult(&result)
+
+ query := &models.LoginUserQuery{
+ Username: "roelgerrits",
+ }
+
+ sc.userQueryReturns(&models.User{
+ Id: 1,
+ Email: "roel@test.net",
+ Name: "Roel Gerrits",
+ Login: "roelgerrits",
+ })
+ sc.userOrgsQueryReturns([]*models.UserOrgDTO{})
+
+ // act
+ extUser, _ := server.Login(query)
+ userInfo, err := user.Upsert(&user.UpsertArgs{
+ SignupAllowed: true,
+ ExternalUser: extUser,
+ })
+
+ // assert
+
+ // Check absence of the error
+ So(err, ShouldBeNil)
+
+ // User should be searched in ldap
+ So(mockConnection.SearchCalled, ShouldBeTrue)
+
+ // Info should be updated (email differs)
+ So(userInfo.Email, ShouldEqual, "roel@test.com")
+
+ // User should have admin privileges
+ So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
+ })
+
+ serverScenario("When login with invalid credentials", func(scenario *scenarioContext) {
+ connection := &MockConnection{}
entry := ldap.Entry{}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
- conn.setSearchResult(&result)
+ connection.setSearchResult(&result)
- conn.bindProvider = func(username, password string) error {
+ connection.bindProvider = func(username, password string) error {
return &ldap.Error{
ResultCode: 49,
}
}
- auth := &Auth{
- server: &ServerConfig{
+ server := &Server{
+ Config: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
@@ -31,19 +102,19 @@ func TestLdapLogin(t *testing.T) {
},
SearchBaseDNs: []string{"BaseDNHere"},
},
- conn: conn,
- log: log.New("test-logger"),
+ Connection: connection,
+ log: log.New("test-logger"),
}
- err := auth.Login(scenario.loginUserQuery)
+ _, err := server.Login(scenario.loginUserQuery)
Convey("it should return invalid credentials error", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
})
})
- AuthScenario("When login with valid credentials", func(scenario *scenarioContext) {
- conn := &mockLdapConn{}
+ serverScenario("When login with valid credentials", func(scenario *scenarioContext) {
+ connection := &MockConnection{}
entry := ldap.Entry{
DN: "dn", Attributes: []*ldap.EntryAttribute{
{Name: "username", Values: []string{"markelog"}},
@@ -54,13 +125,13 @@ func TestLdapLogin(t *testing.T) {
},
}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
- conn.setSearchResult(&result)
+ connection.setSearchResult(&result)
- conn.bindProvider = func(username, password string) error {
+ connection.bindProvider = func(username, password string) error {
return nil
}
- auth := &Auth{
- server: &ServerConfig{
+ server := &Server{
+ Config: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
@@ -68,18 +139,110 @@ func TestLdapLogin(t *testing.T) {
},
SearchBaseDNs: []string{"BaseDNHere"},
},
- conn: conn,
- log: log.New("test-logger"),
+ Connection: connection,
+ log: log.New("test-logger"),
}
- err := auth.Login(scenario.loginUserQuery)
+ resp, err := server.Login(scenario.loginUserQuery)
- Convey("it should not return error", func() {
- So(err, ShouldBeNil)
+ So(err, ShouldBeNil)
+ So(resp.Login, ShouldEqual, "markelog")
+ })
+
+ serverScenario("When user not found in LDAP, but exist in Grafana", func(scenario *scenarioContext) {
+ connection := &MockConnection{}
+ result := ldap.SearchResult{Entries: []*ldap.Entry{}}
+ connection.setSearchResult(&result)
+
+ externalUser := &models.ExternalUserInfo{UserId: 42, IsDisabled: false}
+ scenario.getExternalUserInfoByLoginQueryReturns(externalUser)
+
+ connection.bindProvider = func(username, password string) error {
+ return nil
+ }
+ server := &Server{
+ Config: &ServerConfig{
+ SearchBaseDNs: []string{"BaseDNHere"},
+ },
+ Connection: connection,
+ log: log.New("test-logger"),
+ }
+
+ _, err := server.Login(scenario.loginUserQuery)
+
+ Convey("it should disable user", func() {
+ So(scenario.disableExternalUserCalled, ShouldBeTrue)
+ So(scenario.disableUserCmd.IsDisabled, ShouldBeTrue)
+ So(scenario.disableUserCmd.UserId, ShouldEqual, 42)
+ })
+
+ Convey("it should return invalid credentials error", func() {
+ So(err, ShouldEqual, ErrInvalidCredentials)
+ })
+ })
+
+ serverScenario("When user not found in LDAP, and disabled in Grafana already", func(scenario *scenarioContext) {
+ connection := &MockConnection{}
+ result := ldap.SearchResult{Entries: []*ldap.Entry{}}
+ connection.setSearchResult(&result)
+
+ externalUser := &models.ExternalUserInfo{UserId: 42, IsDisabled: true}
+ scenario.getExternalUserInfoByLoginQueryReturns(externalUser)
+
+ connection.bindProvider = func(username, password string) error {
+ return nil
+ }
+ server := &Server{
+ Config: &ServerConfig{
+ SearchBaseDNs: []string{"BaseDNHere"},
+ },
+ Connection: connection,
+ log: log.New("test-logger"),
+ }
+
+ _, err := server.Login(scenario.loginUserQuery)
+
+ Convey("it should't call disable function", func() {
+ So(scenario.disableExternalUserCalled, ShouldBeFalse)
+ })
+
+ Convey("it should return invalid credentials error", func() {
+ So(err, ShouldEqual, ErrInvalidCredentials)
})
+ })
+
+ serverScenario("When user found in LDAP, and disabled in Grafana", func(scenario *scenarioContext) {
+ connection := &MockConnection{}
+ entry := ldap.Entry{}
+ result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+ connection.setSearchResult(&result)
+ scenario.userQueryReturns(&models.User{Id: 42, IsDisabled: true})
- Convey("it should get user", func() {
- So(scenario.loginUserQuery.User.Login, ShouldEqual, "markelog")
+ connection.bindProvider = func(username, password string) error {
+ return nil
+ }
+ server := &Server{
+ Config: &ServerConfig{
+ SearchBaseDNs: []string{"BaseDNHere"},
+ },
+ Connection: connection,
+ log: log.New("test-logger"),
+ }
+
+ extUser, _ := server.Login(scenario.loginUserQuery)
+ _, err := user.Upsert(&user.UpsertArgs{
+ SignupAllowed: true,
+ ExternalUser: extUser,
+ })
+
+ Convey("it should re-enable user", func() {
+ So(scenario.disableExternalUserCalled, ShouldBeTrue)
+ So(scenario.disableUserCmd.IsDisabled, ShouldBeFalse)
+ So(scenario.disableUserCmd.UserId, ShouldEqual, 42)
+ })
+
+ Convey("it should not return error", func() {
+ So(err, ShouldBeNil)
})
})
})
diff --git a/pkg/services/ldap/ldap_test.go b/pkg/services/ldap/ldap_test.go
index c5232a54e2bd..98b15ec44576 100644
--- a/pkg/services/ldap/ldap_test.go
+++ b/pkg/services/ldap/ldap_test.go
@@ -1,496 +1,118 @@
package ldap
import (
- "context"
"testing"
. "github.com/smartystreets/goconvey/convey"
- "gopkg.in/ldap.v3"
+ ldap "gopkg.in/ldap.v3"
- "github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
)
-func TestAuth(t *testing.T) {
- Convey("initialBind", t, func() {
- Convey("Given bind dn and password configured", func() {
- conn := &mockLdapConn{}
- var actualUsername, actualPassword string
- conn.bindProvider = func(username, password string) error {
- actualUsername = username
- actualPassword = password
- return nil
+func TestPublicAPI(t *testing.T) {
+ Convey("Users()", t, func() {
+ Convey("find one user", func() {
+ MockConnection := &MockConnection{}
+ entry := ldap.Entry{
+ DN: "dn", Attributes: []*ldap.EntryAttribute{
+ {Name: "username", Values: []string{"roelgerrits"}},
+ {Name: "surname", Values: []string{"Gerrits"}},
+ {Name: "email", Values: []string{"roel@test.com"}},
+ {Name: "name", Values: []string{"Roel"}},
+ {Name: "memberof", Values: []string{"admins"}},
+ }}
+ result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+ MockConnection.setSearchResult(&result)
+
+ // Set up attribute map without surname and email
+ server := &Server{
+ Config: &ServerConfig{
+ Attr: AttributeMap{
+ Username: "username",
+ Name: "name",
+ MemberOf: "memberof",
+ },
+ SearchBaseDNs: []string{"BaseDNHere"},
+ },
+ Connection: MockConnection,
+ log: log.New("test-logger"),
}
- Auth := &Auth{
- conn: conn,
- server: &ServerConfig{
- BindDN: "cn=%s,o=users,dc=grafana,dc=org",
- BindPassword: "bindpwd",
- },
- }
- err := Auth.initialBind("user", "pwd")
- So(err, ShouldBeNil)
- So(Auth.requireSecondBind, ShouldBeTrue)
- So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
- So(actualPassword, ShouldEqual, "bindpwd")
- })
- Convey("Given bind dn configured", func() {
- conn := &mockLdapConn{}
- var actualUsername, actualPassword string
- conn.bindProvider = func(username, password string) error {
- actualUsername = username
- actualPassword = password
- return nil
- }
- Auth := &Auth{
- conn: conn,
- server: &ServerConfig{
- BindDN: "cn=%s,o=users,dc=grafana,dc=org",
- },
- }
- err := Auth.initialBind("user", "pwd")
- So(err, ShouldBeNil)
- So(Auth.requireSecondBind, ShouldBeFalse)
- So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
- So(actualPassword, ShouldEqual, "pwd")
- })
+ searchResult, err := server.Users([]string{"roelgerrits"})
- Convey("Given empty bind dn and password", func() {
- conn := &mockLdapConn{}
- unauthenticatedBindWasCalled := false
- var actualUsername string
- conn.unauthenticatedBindProvider = func(username string) error {
- unauthenticatedBindWasCalled = true
- actualUsername = username
- return nil
- }
- Auth := &Auth{
- conn: conn,
- server: &ServerConfig{},
- }
- err := Auth.initialBind("user", "pwd")
So(err, ShouldBeNil)
- So(Auth.requireSecondBind, ShouldBeTrue)
- So(unauthenticatedBindWasCalled, ShouldBeTrue)
- So(actualUsername, ShouldBeEmpty)
+ So(searchResult, ShouldNotBeNil)
+
+ // User should be searched in ldap
+ So(MockConnection.SearchCalled, ShouldBeTrue)
+
+ // No empty attributes should be added to the search request
+ So(len(MockConnection.SearchAttributes), ShouldEqual, 3)
})
})
- Convey("serverBind", t, func() {
+ Convey("InitialBind", t, func() {
Convey("Given bind dn and password configured", func() {
- conn := &mockLdapConn{}
+ connection := &MockConnection{}
var actualUsername, actualPassword string
- conn.bindProvider = func(username, password string) error {
+ connection.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
- Auth := &Auth{
- conn: conn,
- server: &ServerConfig{
- BindDN: "o=users,dc=grafana,dc=org",
+ server := &Server{
+ Connection: connection,
+ Config: &ServerConfig{
+ BindDN: "cn=%s,o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
- err := Auth.serverBind()
+ err := server.InitialBind("user", "pwd")
So(err, ShouldBeNil)
- So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
+ So(server.requireSecondBind, ShouldBeTrue)
+ So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn configured", func() {
- conn := &mockLdapConn{}
- unauthenticatedBindWasCalled := false
- var actualUsername string
- conn.unauthenticatedBindProvider = func(username string) error {
- unauthenticatedBindWasCalled = true
+ connection := &MockConnection{}
+ var actualUsername, actualPassword string
+ connection.bindProvider = func(username, password string) error {
actualUsername = username
+ actualPassword = password
return nil
}
- Auth := &Auth{
- conn: conn,
- server: &ServerConfig{
- BindDN: "o=users,dc=grafana,dc=org",
+ server := &Server{
+ Connection: connection,
+ Config: &ServerConfig{
+ BindDN: "cn=%s,o=users,dc=grafana,dc=org",
},
}
- err := Auth.serverBind()
+ err := server.InitialBind("user", "pwd")
So(err, ShouldBeNil)
- So(unauthenticatedBindWasCalled, ShouldBeTrue)
- So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
+ So(server.requireSecondBind, ShouldBeFalse)
+ So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
+ So(actualPassword, ShouldEqual, "pwd")
})
Convey("Given empty bind dn and password", func() {
- conn := &mockLdapConn{}
+ connection := &MockConnection{}
unauthenticatedBindWasCalled := false
var actualUsername string
- conn.unauthenticatedBindProvider = func(username string) error {
+ connection.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
- Auth := &Auth{
- conn: conn,
- server: &ServerConfig{},
+ server := &Server{
+ Connection: connection,
+ Config: &ServerConfig{},
}
- err := Auth.serverBind()
+ err := server.InitialBind("user", "pwd")
So(err, ShouldBeNil)
+ So(server.requireSecondBind, ShouldBeTrue)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
-
- Convey("When translating ldap user to grafana user", t, func() {
-
- var user1 = &m.User{}
-
- bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpsertUserCommand) error {
- cmd.Result = user1
- cmd.Result.Login = "torkelo"
- return nil
- })
-
- Convey("Given no ldap group map match", func() {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{{}},
- })
- _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
-
- So(err, ShouldEqual, ErrInvalidCredentials)
- })
-
- AuthScenario("Given wildcard group match", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "*", OrgRole: "Admin"},
- },
- })
-
- sc.userQueryReturns(user1)
-
- result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
- So(err, ShouldBeNil)
- So(result, ShouldEqual, user1)
- })
-
- AuthScenario("Given exact group match", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=users", OrgRole: "Admin"},
- },
- })
-
- sc.userQueryReturns(user1)
-
- result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"cn=users"}})
- So(err, ShouldBeNil)
- So(result, ShouldEqual, user1)
- })
-
- AuthScenario("Given group match with different case", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=users", OrgRole: "Admin"},
- },
- })
-
- sc.userQueryReturns(user1)
-
- result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"CN=users"}})
- So(err, ShouldBeNil)
- So(result, ShouldEqual, user1)
- })
-
- AuthScenario("Given no existing grafana user", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=admin", OrgRole: "Admin"},
- {GroupDN: "cn=editor", OrgRole: "Editor"},
- {GroupDN: "*", OrgRole: "Viewer"},
- },
- })
-
- sc.userQueryReturns(nil)
-
- result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
- DN: "torkelo",
- Username: "torkelo",
- Email: "my@email.com",
- MemberOf: []string{"cn=editor"},
- })
-
- So(err, ShouldBeNil)
-
- Convey("Should return new user", func() {
- So(result.Login, ShouldEqual, "torkelo")
- })
-
- Convey("Should set isGrafanaAdmin to false by default", func() {
- So(result.IsAdmin, ShouldBeFalse)
- })
-
- })
-
- })
-
- Convey("When syncing ldap groups to grafana org roles", t, func() {
- AuthScenario("given no current user orgs", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=users", OrgRole: "Admin"},
- },
- })
-
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
- _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
- MemberOf: []string{"cn=users"},
- })
-
- Convey("Should create new org user", func() {
- So(err, ShouldBeNil)
- So(sc.addOrgUserCmd, ShouldNotBeNil)
- So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
- })
- })
-
- AuthScenario("given different current org role", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"},
- },
- })
-
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
- _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
- MemberOf: []string{"cn=users"},
- })
-
- Convey("Should update org role", func() {
- So(err, ShouldBeNil)
- So(sc.updateOrgUserCmd, ShouldNotBeNil)
- So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
- So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
- })
- })
-
- AuthScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
- },
- })
-
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{
- {OrgId: 1, Role: m.ROLE_EDITOR},
- {OrgId: 2, Role: m.ROLE_EDITOR},
- })
- _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
- MemberOf: []string{"cn=users"},
- })
-
- Convey("Should remove org role", func() {
- So(err, ShouldBeNil)
- So(sc.removeOrgUserCmd, ShouldNotBeNil)
- So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2)
- })
- })
-
- AuthScenario("given org role is updated in config", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=admin", OrgId: 1, OrgRole: "Admin"},
- {GroupDN: "cn=users", OrgId: 1, OrgRole: "Viewer"},
- },
- })
-
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
- _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
- MemberOf: []string{"cn=users"},
- })
-
- Convey("Should update org role", func() {
- So(err, ShouldBeNil)
- So(sc.removeOrgUserCmd, ShouldBeNil)
- So(sc.updateOrgUserCmd, ShouldNotBeNil)
- So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
- })
- })
-
- AuthScenario("given multiple matching ldap groups", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
- {GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
- },
- })
-
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
- _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
- MemberOf: []string{"cn=admins"},
- })
-
- Convey("Should take first match, and ignore subsequent matches", func() {
- So(err, ShouldBeNil)
- So(sc.updateOrgUserCmd, ShouldBeNil)
- So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
- })
- })
-
- AuthScenario("given multiple matching ldap groups and no existing groups", func(sc *scenarioContext) {
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
- {GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
- },
- })
-
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
- _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
- MemberOf: []string{"cn=admins"},
- })
-
- Convey("Should take first match, and ignore subsequent matches", func() {
- So(err, ShouldBeNil)
- So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
- So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
- })
-
- Convey("Should not update permissions unless specified", func() {
- So(err, ShouldBeNil)
- So(sc.updateUserPermissionsCmd, ShouldBeNil)
- })
- })
-
- AuthScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
- trueVal := true
-
- Auth := New(&ServerConfig{
- Groups: []*GroupToOrgRole{
- {GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
- },
- })
-
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
- _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
- MemberOf: []string{"cn=admins"},
- })
-
- Convey("Should create user with admin set to true", func() {
- So(err, ShouldBeNil)
- So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
- })
- })
- })
-
- Convey("When calling SyncUser", t, func() {
- mockLdapConnection := &mockLdapConn{}
-
- auth := &Auth{
- server: &ServerConfig{
- Host: "",
- RootCACert: "",
- Groups: []*GroupToOrgRole{
- {GroupDN: "*", OrgRole: "Admin"},
- },
- Attr: AttributeMap{
- Username: "username",
- Surname: "surname",
- Email: "email",
- Name: "name",
- MemberOf: "memberof",
- },
- SearchBaseDNs: []string{"BaseDNHere"},
- },
- conn: mockLdapConnection,
- log: log.New("test-logger"),
- }
-
- dialCalled := false
- dial = func(network, addr string) (IConnection, error) {
- dialCalled = true
- return mockLdapConnection, nil
- }
-
- entry := ldap.Entry{
- DN: "dn", Attributes: []*ldap.EntryAttribute{
- {Name: "username", Values: []string{"roelgerrits"}},
- {Name: "surname", Values: []string{"Gerrits"}},
- {Name: "email", Values: []string{"roel@test.com"}},
- {Name: "name", Values: []string{"Roel"}},
- {Name: "memberof", Values: []string{"admins"}},
- }}
- result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
- mockLdapConnection.setSearchResult(&result)
-
- AuthScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
- // arrange
- query := &m.LoginUserQuery{
- Username: "roelgerrits",
- }
-
- hookDial = nil
-
- sc.userQueryReturns(&m.User{
- Id: 1,
- Email: "roel@test.net",
- Name: "Roel Gerrits",
- Login: "roelgerrits",
- })
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
-
- // act
- syncErrResult := auth.SyncUser(query)
-
- // assert
- So(dialCalled, ShouldBeTrue)
- So(syncErrResult, ShouldBeNil)
- // User should be searched in ldap
- So(mockLdapConnection.searchCalled, ShouldBeTrue)
- // Info should be updated (email differs)
- So(sc.updateUserCmd.Email, ShouldEqual, "roel@test.com")
- // User should have admin privileges
- So(sc.addOrgUserCmd.UserId, ShouldEqual, 1)
- So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
- })
- })
-
- Convey("When searching for a user and not all five attributes are mapped", t, func() {
- mockLdapConnection := &mockLdapConn{}
- entry := ldap.Entry{
- DN: "dn", Attributes: []*ldap.EntryAttribute{
- {Name: "username", Values: []string{"roelgerrits"}},
- {Name: "surname", Values: []string{"Gerrits"}},
- {Name: "email", Values: []string{"roel@test.com"}},
- {Name: "name", Values: []string{"Roel"}},
- {Name: "memberof", Values: []string{"admins"}},
- }}
- result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
- mockLdapConnection.setSearchResult(&result)
-
- // Set up attribute map without surname and email
- Auth := &Auth{
- server: &ServerConfig{
- Attr: AttributeMap{
- Username: "username",
- Name: "name",
- MemberOf: "memberof",
- },
- SearchBaseDNs: []string{"BaseDNHere"},
- },
- conn: mockLdapConnection,
- log: log.New("test-logger"),
- }
-
- searchResult, err := Auth.searchForUser("roelgerrits")
-
- So(err, ShouldBeNil)
- So(searchResult, ShouldNotBeNil)
-
- // User should be searched in ldap
- So(mockLdapConnection.searchCalled, ShouldBeTrue)
-
- // No empty attributes should be added to the search request
- So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3)
- })
}
diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go
index 633920d0a194..df63f10a6fa6 100644
--- a/pkg/services/ldap/settings.go
+++ b/pkg/services/ldap/settings.go
@@ -5,18 +5,20 @@ import (
"sync"
"github.com/BurntSushi/toml"
- "github.com/grafana/grafana/pkg/util/errutil"
"golang.org/x/xerrors"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
+ "github.com/grafana/grafana/pkg/util/errutil"
)
+// Config holds list of connections to LDAP
type Config struct {
Servers []*ServerConfig `toml:"servers"`
}
+// ServerConfig holds connection data to LDAP
type ServerConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
@@ -63,26 +65,27 @@ var loadingMutex = &sync.Mutex{}
// IsEnabled checks if ldap is enabled
func IsEnabled() bool {
- return setting.LdapEnabled
+ return setting.LDAPEnabled
}
// ReloadConfig reads the config from the disc and caches it.
func ReloadConfig() error {
- if IsEnabled() == false {
+ if !IsEnabled() {
return nil
}
+
loadingMutex.Lock()
defer loadingMutex.Unlock()
var err error
- config, err = readConfig(setting.LdapConfigFile)
+ config, err = readConfig(setting.LDAPConfigFile)
return err
}
// GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of
// the config or it reads it and caches it first.
func GetConfig() (*Config, error) {
- if IsEnabled() == false {
+ if !IsEnabled() {
return nil, nil
}
@@ -95,7 +98,7 @@ func GetConfig() (*Config, error) {
defer loadingMutex.Unlock()
var err error
- config, err = readConfig(setting.LdapConfigFile)
+ config, err = readConfig(setting.LDAPConfigFile)
return config, err
}
@@ -103,15 +106,15 @@ func GetConfig() (*Config, error) {
func readConfig(configFile string) (*Config, error) {
result := &Config{}
- logger.Info("Ldap enabled, reading config file", "file", configFile)
+ logger.Info("LDAP enabled, reading config file", "file", configFile)
_, err := toml.DecodeFile(configFile, result)
if err != nil {
- return nil, errutil.Wrap("Failed to load ldap config file", err)
+ return nil, errutil.Wrap("Failed to load LDAP config file", err)
}
if len(result.Servers) == 0 {
- return nil, xerrors.New("ldap enabled but no ldap servers defined in config file")
+ return nil, xerrors.New("LDAP enabled but no LDAP servers defined in config file")
}
// set default org id
diff --git a/pkg/services/ldap/test.go b/pkg/services/ldap/test.go
index 98d169b9a1ad..6319cddd2807 100644
--- a/pkg/services/ldap/test.go
+++ b/pkg/services/ldap/test.go
@@ -12,15 +12,24 @@ import (
"github.com/grafana/grafana/pkg/services/login"
)
-type mockLdapConn struct {
- result *ldap.SearchResult
- searchCalled bool
- searchAttributes []string
+// MockConnection struct for testing
+type MockConnection struct {
+ SearchResult *ldap.SearchResult
+ SearchCalled bool
+ SearchAttributes []string
+
+ AddParams *ldap.AddRequest
+ AddCalled bool
+
+ DelParams *ldap.DelRequest
+ DelCalled bool
+
bindProvider func(username, password string) error
unauthenticatedBindProvider func(username string) error
}
-func (c *mockLdapConn) Bind(username, password string) error {
+// Bind mocks Bind connection function
+func (c *MockConnection) Bind(username, password string) error {
if c.bindProvider != nil {
return c.bindProvider(username, password)
}
@@ -28,7 +37,8 @@ func (c *mockLdapConn) Bind(username, password string) error {
return nil
}
-func (c *mockLdapConn) UnauthenticatedBind(username string) error {
+// UnauthenticatedBind mocks UnauthenticatedBind connection function
+func (c *MockConnection) UnauthenticatedBind(username string) error {
if c.unauthenticatedBindProvider != nil {
return c.unauthenticatedBindProvider(username)
}
@@ -36,23 +46,40 @@ func (c *mockLdapConn) UnauthenticatedBind(username string) error {
return nil
}
-func (c *mockLdapConn) Close() {}
+// Close mocks Close connection function
+func (c *MockConnection) Close() {}
+
+func (c *MockConnection) setSearchResult(result *ldap.SearchResult) {
+ c.SearchResult = result
+}
+
+// Search mocks Search connection function
+func (c *MockConnection) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
+ c.SearchCalled = true
+ c.SearchAttributes = sr.Attributes
+ return c.SearchResult, nil
+}
-func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) {
- c.result = result
+// Add mocks Add connection function
+func (c *MockConnection) Add(request *ldap.AddRequest) error {
+ c.AddCalled = true
+ c.AddParams = request
+ return nil
}
-func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
- c.searchCalled = true
- c.searchAttributes = sr.Attributes
- return c.result, nil
+// Del mocks Del connection function
+func (c *MockConnection) Del(request *ldap.DelRequest) error {
+ c.DelCalled = true
+ c.DelParams = request
+ return nil
}
-func (c *mockLdapConn) StartTLS(*tls.Config) error {
+// StartTLS mocks StartTLS connection function
+func (c *MockConnection) StartTLS(*tls.Config) error {
return nil
}
-func AuthScenario(desc string, fn scenarioFunc) {
+func serverScenario(desc string, fn scenarioFunc) {
Convey(desc, func() {
defer bus.ClearBusHandlers()
@@ -64,10 +91,6 @@ func AuthScenario(desc string, fn scenarioFunc) {
},
}
- hookDial = func(auth *Auth) error {
- return nil
- }
-
loginService := &login.LoginService{
Bus: bus.GetBus(),
}
@@ -100,6 +123,18 @@ func AuthScenario(desc string, fn scenarioFunc) {
return nil
})
+ bus.AddHandler("test", func(cmd *models.GetExternalUserInfoByLoginQuery) error {
+ sc.getExternalUserInfoByLoginQuery = cmd
+ sc.getExternalUserInfoByLoginQuery.Result = &models.ExternalUserInfo{UserId: 42, IsDisabled: false}
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.DisableUserCommand) error {
+ sc.disableExternalUserCalled = true
+ sc.disableUserCmd = cmd
+ return nil
+ })
+
bus.AddHandler("test", func(cmd *models.AddOrgUserCommand) error {
sc.addOrgUserCmd = cmd
return nil
@@ -130,16 +165,19 @@ func AuthScenario(desc string, fn scenarioFunc) {
}
type scenarioContext struct {
- loginUserQuery *models.LoginUserQuery
- getUserByAuthInfoQuery *models.GetUserByAuthInfoQuery
- getUserOrgListQuery *models.GetUserOrgListQuery
- createUserCmd *models.CreateUserCommand
- addOrgUserCmd *models.AddOrgUserCommand
- updateOrgUserCmd *models.UpdateOrgUserCommand
- removeOrgUserCmd *models.RemoveOrgUserCommand
- updateUserCmd *models.UpdateUserCommand
- setUsingOrgCmd *models.SetUsingOrgCommand
- updateUserPermissionsCmd *models.UpdateUserPermissionsCommand
+ loginUserQuery *models.LoginUserQuery
+ getUserByAuthInfoQuery *models.GetUserByAuthInfoQuery
+ getExternalUserInfoByLoginQuery *models.GetExternalUserInfoByLoginQuery
+ getUserOrgListQuery *models.GetUserOrgListQuery
+ createUserCmd *models.CreateUserCommand
+ disableUserCmd *models.DisableUserCommand
+ addOrgUserCmd *models.AddOrgUserCommand
+ updateOrgUserCmd *models.UpdateOrgUserCommand
+ removeOrgUserCmd *models.RemoveOrgUserCommand
+ updateUserCmd *models.UpdateUserCommand
+ setUsingOrgCmd *models.SetUsingOrgCommand
+ updateUserPermissionsCmd *models.UpdateUserPermissionsCommand
+ disableExternalUserCalled bool
}
func (sc *scenarioContext) userQueryReturns(user *models.User) {
@@ -162,4 +200,15 @@ func (sc *scenarioContext) userOrgsQueryReturns(orgs []*models.UserOrgDTO) {
})
}
+func (sc *scenarioContext) getExternalUserInfoByLoginQueryReturns(externalUser *models.ExternalUserInfo) {
+ bus.AddHandler("test", func(cmd *models.GetExternalUserInfoByLoginQuery) error {
+ sc.getExternalUserInfoByLoginQuery = cmd
+ sc.getExternalUserInfoByLoginQuery.Result = &models.ExternalUserInfo{
+ UserId: externalUser.UserId,
+ IsDisabled: externalUser.IsDisabled,
+ }
+ return nil
+ })
+}
+
type scenarioFunc func(c *scenarioContext)
diff --git a/pkg/services/login/errors.go b/pkg/services/login/errors.go
index 0889b4613a6b..dbf15ea76cd4 100644
--- a/pkg/services/login/errors.go
+++ b/pkg/services/login/errors.go
@@ -9,7 +9,7 @@ var (
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
- ErrPasswordEmpty = errors.New("No password provided.")
+ ErrPasswordEmpty = errors.New("No password provided")
ErrUsersQuotaReached = errors.New("Users quota reached")
ErrGettingUserQuota = errors.New("Error getting user quota")
)
diff --git a/pkg/services/login/login.go b/pkg/services/login/login.go
index 9b2a258dea02..cf7b8dd92c55 100644
--- a/pkg/services/login/login.go
+++ b/pkg/services/login/login.go
@@ -2,8 +2,8 @@ package login
import (
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/quota"
)
@@ -27,10 +27,10 @@ func (ls *LoginService) Init() error {
return nil
}
-func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
+func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error {
extUser := cmd.ExternalUser
- userQuery := &m.GetUserByAuthInfoQuery{
+ userQuery := &models.GetUserByAuthInfoQuery{
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
UserId: extUser.UserId,
@@ -39,7 +39,7 @@ func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
}
err := bus.Dispatch(userQuery)
- if err != m.ErrUserNotFound && err != nil {
+ if err != models.ErrUserNotFound && err != nil {
return err
}
@@ -64,7 +64,7 @@ func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
}
if extUser.AuthModule != "" {
- cmd2 := &m.SetAuthInfoCommand{
+ cmd2 := &models.SetAuthInfoCommand{
UserId: cmd.Result.Id,
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
@@ -90,6 +90,13 @@ func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
return err
}
}
+
+ if extUser.AuthModule == models.AuthModuleLDAP && userQuery.Result.IsDisabled {
+ // Re-enable user when it found in LDAP
+ if err := ls.Bus.Dispatch(&models.DisableUserCommand{UserId: cmd.Result.Id, IsDisabled: false}); err != nil {
+ return err
+ }
+ }
}
err = syncOrgRoles(cmd.Result, extUser)
@@ -100,12 +107,12 @@ func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
// Sync isGrafanaAdmin permission
if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
- if err := ls.Bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
+ if err := ls.Bus.Dispatch(&models.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
return err
}
}
- err = ls.Bus.Dispatch(&m.SyncTeamsCommand{
+ err = ls.Bus.Dispatch(&models.SyncTeamsCommand{
User: cmd.Result,
ExternalUser: extUser,
})
@@ -117,8 +124,8 @@ func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
return err
}
-func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
- cmd := &m.CreateUserCommand{
+func createUser(extUser *models.ExternalUserInfo) (*models.User, error) {
+ cmd := &models.CreateUserCommand{
Login: extUser.Login,
Email: extUser.Email,
Name: extUser.Name,
@@ -132,9 +139,9 @@ func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
return &cmd.Result, nil
}
-func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
+func updateUser(user *models.User, extUser *models.ExternalUserInfo) error {
// sync user info
- updateCmd := &m.UpdateUserCommand{
+ updateCmd := &models.UpdateUserCommand{
UserId: user.Id,
}
@@ -165,8 +172,8 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
return bus.Dispatch(updateCmd)
}
-func updateUserAuth(user *m.User, extUser *m.ExternalUserInfo) error {
- updateCmd := &m.UpdateAuthInfoCommand{
+func updateUserAuth(user *models.User, extUser *models.ExternalUserInfo) error {
+ updateCmd := &models.UpdateAuthInfoCommand{
AuthModule: extUser.AuthModule,
AuthId: extUser.AuthId,
UserId: user.Id,
@@ -177,13 +184,13 @@ func updateUserAuth(user *m.User, extUser *m.ExternalUserInfo) error {
return bus.Dispatch(updateCmd)
}
-func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
+func syncOrgRoles(user *models.User, extUser *models.ExternalUserInfo) error {
// don't sync org roles if none are specified
if len(extUser.OrgRoles) == 0 {
return nil
}
- orgsQuery := &m.GetUserOrgListQuery{UserId: user.Id}
+ orgsQuery := &models.GetUserOrgListQuery{UserId: user.Id}
if err := bus.Dispatch(orgsQuery); err != nil {
return err
}
@@ -199,7 +206,7 @@ func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
deleteOrgIds = append(deleteOrgIds, org.OrgId)
} else if extUser.OrgRoles[org.OrgId] != org.Role {
// update role
- cmd := &m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extUser.OrgRoles[org.OrgId]}
+ cmd := &models.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extUser.OrgRoles[org.OrgId]}
if err := bus.Dispatch(cmd); err != nil {
return err
}
@@ -213,16 +220,16 @@ func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
}
// add role
- cmd := &m.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId}
+ cmd := &models.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId}
err := bus.Dispatch(cmd)
- if err != nil && err != m.ErrOrgNotFound {
+ if err != nil && err != models.ErrOrgNotFound {
return err
}
}
// delete any removed org roles
for _, orgId := range deleteOrgIds {
- cmd := &m.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id}
+ cmd := &models.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id}
if err := bus.Dispatch(cmd); err != nil {
return err
}
@@ -235,7 +242,7 @@ func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
break
}
- return bus.Dispatch(&m.SetUsingOrgCommand{
+ return bus.Dispatch(&models.SetUsingOrgCommand{
UserId: user.Id,
OrgId: user.OrgId,
})
diff --git a/pkg/services/multildap/multildap.go b/pkg/services/multildap/multildap.go
new file mode 100644
index 000000000000..6c2baf1671af
--- /dev/null
+++ b/pkg/services/multildap/multildap.go
@@ -0,0 +1,152 @@
+package multildap
+
+import (
+ "errors"
+
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/ldap"
+)
+
+// GetConfig gets LDAP config
+var GetConfig = ldap.GetConfig
+
+// IsEnabled checks if LDAP is enabled
+var IsEnabled = ldap.IsEnabled
+
+// ErrInvalidCredentials is returned if username and password do not match
+var ErrInvalidCredentials = ldap.ErrInvalidCredentials
+
+// ErrNoLDAPServers is returned when there is no LDAP servers specified
+var ErrNoLDAPServers = errors.New("No LDAP servers are configured")
+
+// ErrDidNotFindUser if request for user is unsuccessful
+var ErrDidNotFindUser = errors.New("Did not find a user")
+
+// IMultiLDAP is interface for MultiLDAP
+type IMultiLDAP interface {
+ Login(query *models.LoginUserQuery) (
+ *models.ExternalUserInfo, error,
+ )
+
+ Users(logins []string) (
+ []*models.ExternalUserInfo, error,
+ )
+
+ User(login string) (
+ *models.ExternalUserInfo, error,
+ )
+}
+
+// MultiLDAP is basic struct of LDAP authorization
+type MultiLDAP struct {
+ configs []*ldap.ServerConfig
+}
+
+// New creates the new LDAP auth
+func New(configs []*ldap.ServerConfig) IMultiLDAP {
+ return &MultiLDAP{
+ configs: configs,
+ }
+}
+
+// Login tries to log in the user in multiples LDAP
+func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) (
+ *models.ExternalUserInfo, error,
+) {
+ if len(multiples.configs) == 0 {
+ return nil, ErrNoLDAPServers
+ }
+
+ for _, config := range multiples.configs {
+ server := ldap.New(config)
+
+ if err := server.Dial(); err != nil {
+ return nil, err
+ }
+
+ defer server.Close()
+
+ user, err := server.Login(query)
+
+ if user != nil {
+ return user, nil
+ }
+
+ // Continue if we couldn't find the user
+ if err == ErrInvalidCredentials {
+ continue
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return user, nil
+ }
+
+ // Return invalid credentials if we couldn't find the user anywhere
+ return nil, ErrInvalidCredentials
+}
+
+// User gets a user by login
+func (multiples *MultiLDAP) User(login string) (
+ *models.ExternalUserInfo,
+ error,
+) {
+
+ if len(multiples.configs) == 0 {
+ return nil, ErrNoLDAPServers
+ }
+
+ search := []string{login}
+ for _, config := range multiples.configs {
+ server := ldap.New(config)
+
+ if err := server.Dial(); err != nil {
+ return nil, err
+ }
+
+ defer server.Close()
+
+ users, err := server.Users(search)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(users) != 0 {
+ return users[0], nil
+ }
+ }
+
+ return nil, ErrDidNotFindUser
+}
+
+// Users gets users from multiple LDAP servers
+func (multiples *MultiLDAP) Users(logins []string) (
+ []*models.ExternalUserInfo,
+ error,
+) {
+ var result []*models.ExternalUserInfo
+
+ if len(multiples.configs) == 0 {
+ return nil, ErrNoLDAPServers
+ }
+
+ for _, config := range multiples.configs {
+ server := ldap.New(config)
+
+ if err := server.Dial(); err != nil {
+ return nil, err
+ }
+
+ defer server.Close()
+
+ users, err := server.Users(logins)
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, users...)
+ }
+
+ return result, nil
+}
diff --git a/pkg/services/notifications/codes.go b/pkg/services/notifications/codes.go
index 6382b609036b..b2bc9439549f 100644
--- a/pkg/services/notifications/codes.go
+++ b/pkg/services/notifications/codes.go
@@ -1,7 +1,7 @@
package notifications
import (
- "crypto/sha1"
+ "crypto/sha1" // #nosec
"encoding/hex"
"fmt"
"time"
diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go
index f88deb2bf960..584914f5ff71 100644
--- a/pkg/services/notifications/mailer.go
+++ b/pkg/services/notifications/mailer.go
@@ -12,9 +12,9 @@ import (
"net"
"strconv"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
- "github.com/pkg/errors"
+ "github.com/grafana/grafana/pkg/util/errutil"
gomail "gopkg.in/mail.v2"
)
@@ -38,11 +38,11 @@ func (ns *NotificationService) send(msg *Message) (int, error) {
e := dialer.DialAndSend(m)
if e != nil {
- err = errors.Wrapf(e, "Failed to send notification to email address: %s", address)
+ err = errutil.Wrapf(e, "Failed to send notification to email address: %s", address)
continue
}
- num += 1
+ num++
}
return num, err
@@ -83,9 +83,9 @@ func (ns *NotificationService) createDialer() (*gomail.Dialer, error) {
return d, nil
}
-func (ns *NotificationService) buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
+func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) (*Message, error) {
if !ns.Cfg.Smtp.Enabled {
- return nil, m.ErrSmtpNotEnabled
+ return nil, models.ErrSmtpNotEnabled
}
var buffer bytes.Buffer
diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go
index 769fdd06fd04..3168fdcd5e95 100644
--- a/pkg/services/notifications/notifications.go
+++ b/pkg/services/notifications/notifications.go
@@ -11,7 +11,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go
index 50cedc079e33..d004221a3525 100644
--- a/pkg/services/provisioning/dashboards/config_reader.go
+++ b/pkg/services/provisioning/dashboards/config_reader.go
@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
yaml "gopkg.in/yaml.v2"
)
diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go
index 18d8022d62d0..03aa19af11e8 100644
--- a/pkg/services/provisioning/dashboards/config_reader_test.go
+++ b/pkg/services/provisioning/dashboards/config_reader_test.go
@@ -4,7 +4,7 @@ import (
"os"
"testing"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
. "github.com/smartystreets/goconvey/convey"
)
diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go
index fd3a824420c6..108b1b126c91 100644
--- a/pkg/services/provisioning/dashboards/dashboard.go
+++ b/pkg/services/provisioning/dashboards/dashboard.go
@@ -3,8 +3,10 @@ package dashboards
import (
"context"
"fmt"
- "github.com/grafana/grafana/pkg/log"
- "github.com/pkg/errors"
+ "os"
+
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/util/errutil"
)
type DashboardProvisionerImpl struct {
@@ -18,13 +20,13 @@ func NewDashboardProvisionerImpl(configDirectory string) (*DashboardProvisionerI
configs, err := cfgReader.readConfig()
if err != nil {
- return nil, errors.Wrap(err, "Failed to read dashboards config")
+ return nil, errutil.Wrap("Failed to read dashboards config", err)
}
fileReaders, err := getFileReaders(configs, logger)
if err != nil {
- return nil, errors.Wrap(err, "Failed to initialize file readers")
+ return nil, errutil.Wrap("Failed to initialize file readers", err)
}
d := &DashboardProvisionerImpl{
@@ -37,9 +39,14 @@ func NewDashboardProvisionerImpl(configDirectory string) (*DashboardProvisionerI
func (provider *DashboardProvisionerImpl) Provision() error {
for _, reader := range provider.fileReaders {
- err := reader.startWalkingDisk()
- if err != nil {
- return errors.Wrapf(err, "Failed to provision config %v", reader.Cfg.Name)
+ if err := reader.startWalkingDisk(); err != nil {
+ if os.IsNotExist(err) {
+ // don't stop the provisioning service in case the folder is missing. The folder can appear after the startup
+ provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err)
+ return nil
+ }
+
+ return errutil.Wrapf(err, "Failed to provision config %v", reader.Cfg.Name)
}
}
@@ -73,7 +80,7 @@ func getFileReaders(configs []*DashboardsAsConfig, logger log.Logger) ([]*fileRe
case "file":
fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name))
if err != nil {
- return nil, errors.Wrapf(err, "Failed to create file reader for config %v", config.Name)
+ return nil, errutil.Wrapf(err, "Failed to create file reader for config %v", config.Name)
}
readers = append(readers, fileReader)
default:
diff --git a/pkg/services/provisioning/dashboards/dashboard_mock.go b/pkg/services/provisioning/dashboards/dashboard_mock.go
index 303338106e63..2ad13744b295 100644
--- a/pkg/services/provisioning/dashboards/dashboard_mock.go
+++ b/pkg/services/provisioning/dashboards/dashboard_mock.go
@@ -25,9 +25,8 @@ func (dpm *DashboardProvisionerMock) Provision() error {
dpm.Calls.Provision = append(dpm.Calls.Provision, nil)
if dpm.ProvisionFunc != nil {
return dpm.ProvisionFunc()
- } else {
- return nil
}
+ return nil
}
func (dpm *DashboardProvisionerMock) PollChanges(ctx context.Context) {
@@ -41,7 +40,6 @@ func (dpm *DashboardProvisionerMock) GetProvisionerResolvedPath(name string) str
dpm.Calls.PollChanges = append(dpm.Calls.GetProvisionerResolvedPath, name)
if dpm.GetProvisionerResolvedPathFunc != nil {
return dpm.GetProvisionerResolvedPathFunc(name)
- } else {
- return ""
}
+ return ""
}
diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go
index 96da9c8f6dfb..3b47e5014c13 100644
--- a/pkg/services/provisioning/dashboards/file_reader.go
+++ b/pkg/services/provisioning/dashboards/file_reader.go
@@ -16,7 +16,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
@@ -72,9 +72,7 @@ func (fr *fileReader) startWalkingDisk() error {
fr.log.Debug("Start walking disk", "path", fr.Path)
resolvedPath := fr.resolvedPath()
if _, err := os.Stat(resolvedPath); err != nil {
- if os.IsNotExist(err) {
- return err
- }
+ return err
}
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardProvisioningService)
diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_linux_test.go
index d62a59a4f4c7..4adeeafe49b4 100644
--- a/pkg/services/provisioning/dashboards/file_reader_linux_test.go
+++ b/pkg/services/provisioning/dashboards/file_reader_linux_test.go
@@ -6,7 +6,7 @@ import (
"path/filepath"
"testing"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
)
var (
diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go
index efc6052cbe79..668608d79eca 100644
--- a/pkg/services/provisioning/dashboards/file_reader_test.go
+++ b/pkg/services/provisioning/dashboards/file_reader_test.go
@@ -13,7 +13,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
. "github.com/smartystreets/goconvey/convey"
)
diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go
index 334ce879e5ad..60794e3b4b03 100644
--- a/pkg/services/provisioning/datasources/config_reader.go
+++ b/pkg/services/provisioning/datasources/config_reader.go
@@ -6,7 +6,7 @@ import (
"path/filepath"
"strings"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"gopkg.in/yaml.v2"
)
diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go
index 6aba82622141..6bf60b30fc1b 100644
--- a/pkg/services/provisioning/datasources/config_reader_test.go
+++ b/pkg/services/provisioning/datasources/config_reader_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go
index de6c876baada..f25d717b915f 100644
--- a/pkg/services/provisioning/datasources/datasources.go
+++ b/pkg/services/provisioning/datasources/datasources.go
@@ -5,7 +5,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
diff --git a/pkg/services/provisioning/datasources/types.go b/pkg/services/provisioning/datasources/types.go
index bbd072052dd7..d1a76246c9db 100644
--- a/pkg/services/provisioning/datasources/types.go
+++ b/pkg/services/provisioning/datasources/types.go
@@ -2,7 +2,7 @@ package datasources
import (
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
diff --git a/pkg/services/provisioning/notifiers/alert_notifications.go b/pkg/services/provisioning/notifiers/alert_notifications.go
index 3659a73832ee..865e215e38bf 100644
--- a/pkg/services/provisioning/notifiers/alert_notifications.go
+++ b/pkg/services/provisioning/notifiers/alert_notifications.go
@@ -4,7 +4,7 @@ import (
"errors"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
diff --git a/pkg/services/provisioning/notifiers/config_reader.go b/pkg/services/provisioning/notifiers/config_reader.go
index 896d5105cedb..5dbb3d87b786 100644
--- a/pkg/services/provisioning/notifiers/config_reader.go
+++ b/pkg/services/provisioning/notifiers/config_reader.go
@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"gopkg.in/yaml.v2"
diff --git a/pkg/services/provisioning/notifiers/config_reader_test.go b/pkg/services/provisioning/notifiers/config_reader_test.go
index d705bd0d93d9..e09531774252 100644
--- a/pkg/services/provisioning/notifiers/config_reader_test.go
+++ b/pkg/services/provisioning/notifiers/config_reader_test.go
@@ -4,7 +4,7 @@ import (
"os"
"testing"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/alerting/notifiers"
@@ -66,6 +66,8 @@ func TestNotificationAsConfig(t *testing.T) {
So(nt.Settings, ShouldResemble, map[string]interface{}{
"recipient": "XXX", "token": "xoxb", "uploadImage": true, "url": "https://slack.com",
})
+ So(nt.SendReminder, ShouldBeTrue)
+ So(nt.Frequency, ShouldEqual, "1h")
nt = nts[1]
So(nt.Name, ShouldEqual, "another-not-default-notification")
diff --git a/pkg/services/provisioning/notifiers/testdata/test-configs/correct-properties/correct-properties.yaml b/pkg/services/provisioning/notifiers/testdata/test-configs/correct-properties/correct-properties.yaml
index 1d846f64473b..5c71e0229335 100644
--- a/pkg/services/provisioning/notifiers/testdata/test-configs/correct-properties/correct-properties.yaml
+++ b/pkg/services/provisioning/notifiers/testdata/test-configs/correct-properties/correct-properties.yaml
@@ -3,8 +3,9 @@ notifiers:
type: slack
uid: notifier1
org_id: 2
- uid: "notifier1"
is_default: true
+ send_reminder: true
+ frequency: 1h
settings:
recipient: "XXX"
token: "xoxb"
diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go
index 29f2d1391643..025b5fe311d1 100644
--- a/pkg/services/provisioning/provisioning.go
+++ b/pkg/services/provisioning/provisioning.go
@@ -5,8 +5,8 @@ import (
"path"
"sync"
- "github.com/grafana/grafana/pkg/log"
- "github.com/pkg/errors"
+ "github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
@@ -103,20 +103,20 @@ func (ps *provisioningServiceImpl) Run(ctx context.Context) error {
func (ps *provisioningServiceImpl) ProvisionDatasources() error {
datasourcePath := path.Join(ps.Cfg.ProvisioningPath, "datasources")
err := ps.provisionDatasources(datasourcePath)
- return errors.Wrap(err, "Datasource provisioning error")
+ return errutil.Wrap("Datasource provisioning error", err)
}
func (ps *provisioningServiceImpl) ProvisionNotifications() error {
alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "notifiers")
err := ps.provisionNotifiers(alertNotificationsPath)
- return errors.Wrap(err, "Alert notification provisioning error")
+ return errutil.Wrap("Alert notification provisioning error", err)
}
func (ps *provisioningServiceImpl) ProvisionDashboards() error {
dashboardPath := path.Join(ps.Cfg.ProvisioningPath, "dashboards")
dashProvisioner, err := ps.newDashboardProvisioner(dashboardPath)
if err != nil {
- return errors.Wrap(err, "Failed to create provisioner")
+ return errutil.Wrap("Failed to create provisioner", err)
}
ps.mutex.Lock()
@@ -127,7 +127,7 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error {
if err := dashProvisioner.Provision(); err != nil {
// If we fail to provision with the new provisioner, mutex will unlock and the polling we restart with the
// old provisioner as we did not switch them yet.
- return errors.Wrap(err, "Failed to provision dashboards")
+ return errutil.Wrap("Failed to provision dashboards", err)
}
ps.dashboardProvisioner = dashProvisioner
return nil
diff --git a/pkg/services/provisioning/provisioning_mock.go b/pkg/services/provisioning/provisioning_mock.go
index 7977e59b2e47..dc487f566465 100644
--- a/pkg/services/provisioning/provisioning_mock.go
+++ b/pkg/services/provisioning/provisioning_mock.go
@@ -25,34 +25,30 @@ func (mock *ProvisioningServiceMock) ProvisionDatasources() error {
mock.Calls.ProvisionDatasources = append(mock.Calls.ProvisionDatasources, nil)
if mock.ProvisionDatasourcesFunc != nil {
return mock.ProvisionDatasourcesFunc()
- } else {
- return nil
}
+ return nil
}
func (mock *ProvisioningServiceMock) ProvisionNotifications() error {
mock.Calls.ProvisionNotifications = append(mock.Calls.ProvisionNotifications, nil)
if mock.ProvisionNotificationsFunc != nil {
return mock.ProvisionNotificationsFunc()
- } else {
- return nil
}
+ return nil
}
func (mock *ProvisioningServiceMock) ProvisionDashboards() error {
mock.Calls.ProvisionDashboards = append(mock.Calls.ProvisionDashboards, nil)
if mock.ProvisionDashboardsFunc != nil {
return mock.ProvisionDashboardsFunc()
- } else {
- return nil
}
+ return nil
}
func (mock *ProvisioningServiceMock) GetDashboardProvisionerResolvedPath(name string) string {
mock.Calls.GetDashboardProvisionerResolvedPath = append(mock.Calls.GetDashboardProvisionerResolvedPath, name)
if mock.GetDashboardProvisionerResolvedPathFunc != nil {
return mock.GetDashboardProvisionerResolvedPathFunc(name)
- } else {
- return ""
}
+ return ""
}
diff --git a/pkg/services/provisioning/values/values.go b/pkg/services/provisioning/values/values.go
index 48c7cd6444e6..63da76a9338d 100644
--- a/pkg/services/provisioning/values/values.go
+++ b/pkg/services/provisioning/values/values.go
@@ -1,4 +1,4 @@
-// A set of value types to use in provisioning. They add custom unmarshaling logic that puts the string values
+// Package values is a set of value types to use in provisioning. They add custom unmarshaling logic that puts the string values
// through os.ExpandEnv.
// Usage:
// type Data struct {
@@ -11,10 +11,11 @@
package values
import (
- "github.com/pkg/errors"
"os"
"reflect"
"strconv"
+
+ "github.com/grafana/grafana/pkg/util/errutil"
)
type IntValue struct {
@@ -33,7 +34,7 @@ func (val *IntValue) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
val.Raw = interpolated.raw
val.value, err = strconv.Atoi(interpolated.value)
- return errors.Wrap(err, "cannot convert value int")
+ return errutil.Wrap("cannot convert value int", err)
}
func (val *IntValue) Value() int {
diff --git a/pkg/services/rendering/phantomjs.go b/pkg/services/rendering/phantomjs.go
index 35389fe7918e..a67d43c3c239 100644
--- a/pkg/services/rendering/phantomjs.go
+++ b/pkg/services/rendering/phantomjs.go
@@ -10,7 +10,7 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
)
diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go
index 06db73771b26..259b5db2d7af 100644
--- a/pkg/services/rendering/rendering.go
+++ b/pkg/services/rendering/rendering.go
@@ -10,7 +10,7 @@ import (
plugin "github.com/hashicorp/go-plugin"
pluginModel "github.com/grafana/grafana-plugin-model/go/renderer"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
diff --git a/pkg/services/sqlstore/logger.go b/pkg/services/sqlstore/logger.go
index 9b0b068c9184..498c2b58c17d 100644
--- a/pkg/services/sqlstore/logger.go
+++ b/pkg/services/sqlstore/logger.go
@@ -3,7 +3,7 @@ package sqlstore
import (
"fmt"
- glog "github.com/grafana/grafana/pkg/log"
+ glog "github.com/grafana/grafana/pkg/infra/log"
"github.com/go-xorm/core"
)
diff --git a/pkg/services/sqlstore/login_attempt.go b/pkg/services/sqlstore/login_attempt.go
index fe77dd7e9146..a9adbca5bfed 100644
--- a/pkg/services/sqlstore/login_attempt.go
+++ b/pkg/services/sqlstore/login_attempt.go
@@ -43,7 +43,7 @@ func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
if err != nil {
return err
}
-
+ // nolint: gosimple
if result == nil || len(result) == 0 || result[0] == nil {
return nil
}
diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go
index e273cb7d5424..8202dfc1cbbc 100644
--- a/pkg/services/sqlstore/migrations/user_mig.go
+++ b/pkg/services/sqlstore/migrations/user_mig.go
@@ -116,6 +116,12 @@ func addUserMigrations(mg *Migrator) {
// Adds salt & rands for old users who used ldap or oauth
mg.AddMigration("Add missing user data", &AddMissingUserSaltAndRandsMigration{})
+
+ // is_disabled indicates whether user disabled or not. Disabled user should not be able to log in.
+ // This field used in couple with LDAP auth to disable users removed from LDAP rather than delete it immediately.
+ mg.AddMigration("Add is_disabled column to user", NewAddColumnMigration(userV2, &Column{
+ Name: "is_disabled", Type: DB_Bool, Nullable: false, Default: "0",
+ }))
}
type AddMissingUserSaltAndRandsMigration struct {
diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go
index ce0b2663def4..68820c7feb6a 100644
--- a/pkg/services/sqlstore/migrator/migrator.go
+++ b/pkg/services/sqlstore/migrator/migrator.go
@@ -5,7 +5,7 @@ import (
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
diff --git a/pkg/services/sqlstore/session.go b/pkg/services/sqlstore/session.go
index 29d7392678fe..a705af39da3b 100644
--- a/pkg/services/sqlstore/session.go
+++ b/pkg/services/sqlstore/session.go
@@ -18,6 +18,11 @@ func (sess *DBSession) publishAfterCommit(msg interface{}) {
sess.events = append(sess.events, msg)
}
+// NewSession returns a new DBSession
+func (ss *SqlStore) NewSession() *DBSession {
+ return &DBSession{Session: ss.engine.NewSession()}
+}
+
func newSession() *DBSession {
return &DBSession{Session: x.NewSession()}
}
@@ -41,6 +46,16 @@ func startSession(ctx context.Context, engine *xorm.Engine, beginTran bool) (*DB
return newSess, nil
}
+// WithDbSession calls the callback with an session attached to the context.
+func (ss *SqlStore) WithDbSession(ctx context.Context, callback dbTransactionFunc) error {
+ sess, err := startSession(ctx, ss.engine, false)
+ if err != nil {
+ return err
+ }
+
+ return callback(sess)
+}
+
func withDbSession(ctx context.Context, callback dbTransactionFunc) error {
sess, err := startSession(ctx, x, false)
if err != nil {
diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go
index 116c246cbe34..675af5f02bb3 100644
--- a/pkg/services/sqlstore/sqlstore.go
+++ b/pkg/services/sqlstore/sqlstore.go
@@ -8,13 +8,12 @@ import (
"path"
"path/filepath"
"strings"
- "testing"
"time"
"github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/annotations"
@@ -26,7 +25,6 @@ import (
_ "github.com/grafana/grafana/pkg/tsdb/mssql"
"github.com/grafana/grafana/pkg/util"
_ "github.com/lib/pq"
- sqlite3 "github.com/mattn/go-sqlite3"
)
var (
@@ -39,6 +37,11 @@ var (
const ContextSessionName = "db-session"
func init() {
+ // This change will make xorm use an empty default schema for postgres and
+ // by that mimic the functionality of how it was functioning before
+ // xorm's changes above.
+ xorm.DefaultPostgresSchema = ""
+
registry.Register(®istry.Descriptor{
Name: "SqlStore",
Instance: &SqlStore{},
@@ -58,64 +61,6 @@ type SqlStore struct {
skipEnsureAdmin bool
}
-// NewSession returns a new DBSession
-func (ss *SqlStore) NewSession() *DBSession {
- return &DBSession{Session: ss.engine.NewSession()}
-}
-
-// WithDbSession calls the callback with an session attached to the context.
-func (ss *SqlStore) WithDbSession(ctx context.Context, callback dbTransactionFunc) error {
- sess, err := startSession(ctx, ss.engine, false)
- if err != nil {
- return err
- }
-
- return callback(sess)
-}
-
-// WithTransactionalDbSession calls the callback with an session within a transaction
-func (ss *SqlStore) WithTransactionalDbSession(ctx context.Context, callback dbTransactionFunc) error {
- return ss.inTransactionWithRetryCtx(ctx, callback, 0)
-}
-
-func (ss *SqlStore) inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc, retry int) error {
- sess, err := startSession(ctx, ss.engine, true)
- if err != nil {
- return err
- }
-
- defer sess.Close()
-
- err = callback(sess)
-
- // special handling of database locked errors for sqlite, then we can retry 3 times
- if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
- if sqlError.Code == sqlite3.ErrLocked {
- sess.Rollback()
- time.Sleep(time.Millisecond * time.Duration(10))
- sqlog.Info("Database table locked, sleeping then retrying", "retry", retry)
- return ss.inTransactionWithRetryCtx(ctx, callback, retry+1)
- }
- }
-
- if err != nil {
- sess.Rollback()
- return err
- } else if err = sess.Commit(); err != nil {
- return err
- }
-
- if len(sess.events) > 0 {
- for _, e := range sess.events {
- if err = bus.Publish(e); err != nil {
- log.Error(3, "Failed to publish event after commit. error: %v", err)
- }
- }
- }
-
- return nil
-}
-
func (ss *SqlStore) Init() error {
ss.log = log.New("sqlstore")
ss.readConfig()
@@ -230,7 +175,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
ss.dbCfg.User, ss.dbCfg.Pwd, protocol, ss.dbCfg.Host, ss.dbCfg.Name)
if ss.dbCfg.SslMode == "true" || ss.dbCfg.SslMode == "skip-verify" {
- tlsCert, err := makeCert("custom", ss.dbCfg)
+ tlsCert, err := makeCert(ss.dbCfg)
if err != nil {
return "", err
}
@@ -339,7 +284,14 @@ func (ss *SqlStore) readConfig() {
ss.dbCfg.CacheMode = sec.Key("cache_mode").MustString("private")
}
-func InitTestDB(t *testing.T) *SqlStore {
+// Interface of arguments for testing db
+type ITestDB interface {
+ Helper()
+ Fatalf(format string, args ...interface{})
+}
+
+// InitTestDB initiliaze test DB
+func InitTestDB(t ITestDB) *SqlStore {
t.Helper()
sqlstore := &SqlStore{}
sqlstore.skipEnsureAdmin = true
diff --git a/pkg/services/sqlstore/tls_mysql.go b/pkg/services/sqlstore/tls_mysql.go
index 3697135c717e..0ba3d6da8d52 100644
--- a/pkg/services/sqlstore/tls_mysql.go
+++ b/pkg/services/sqlstore/tls_mysql.go
@@ -5,9 +5,13 @@ import (
"crypto/x509"
"fmt"
"io/ioutil"
+
+ "github.com/grafana/grafana/pkg/infra/log"
)
-func makeCert(tlsPoolName string, config DatabaseConfig) (*tls.Config, error) {
+var tlslog = log.New("tls_mysql")
+
+func makeCert(config DatabaseConfig) (*tls.Config, error) {
rootCertPool := x509.NewCertPool()
pem, err := ioutil.ReadFile(config.CaCertPath)
if err != nil {
@@ -16,18 +20,16 @@ func makeCert(tlsPoolName string, config DatabaseConfig) (*tls.Config, error) {
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
return nil, err
}
- clientCert := make([]tls.Certificate, 0, 1)
- if config.ClientCertPath != "" && config.ClientKeyPath != "" {
- certs, err := tls.LoadX509KeyPair(config.ClientCertPath, config.ClientKeyPath)
- if err != nil {
- return nil, err
- }
- clientCert = append(clientCert, certs)
- }
tlsConfig := &tls.Config{
- RootCAs: rootCertPool,
- Certificates: clientCert,
+ RootCAs: rootCertPool,
+ }
+ if config.ClientCertPath != "" && config.ClientKeyPath != "" {
+ tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
+ tlslog.Debug("Loading client certificate")
+ cert, err := tls.LoadX509KeyPair(config.ClientCertPath, config.ClientKeyPath)
+ return &cert, err
+ }
}
tlsConfig.ServerName = config.ServerCertName
if config.SslMode == "skip-verify" {
diff --git a/pkg/services/sqlstore/transactions.go b/pkg/services/sqlstore/transactions.go
index edf29fffb8f3..9b744fd32884 100644
--- a/pkg/services/sqlstore/transactions.go
+++ b/pkg/services/sqlstore/transactions.go
@@ -4,63 +4,34 @@ import (
"context"
"time"
+ "github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
sqlite3 "github.com/mattn/go-sqlite3"
)
+// WithTransactionalDbSession calls the callback with an session within a transaction
+func (ss *SqlStore) WithTransactionalDbSession(ctx context.Context, callback dbTransactionFunc) error {
+ return inTransactionWithRetryCtx(ctx, ss.engine, callback, 0)
+}
+
func (ss *SqlStore) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return ss.inTransactionWithRetry(ctx, fn, 0)
}
func (ss *SqlStore) inTransactionWithRetry(ctx context.Context, fn func(ctx context.Context) error, retry int) error {
- sess, err := startSession(ctx, ss.engine, true)
- if err != nil {
- return err
- }
-
- defer sess.Close()
-
- withValue := context.WithValue(ctx, ContextSessionName, sess)
-
- err = fn(withValue)
-
- // special handling of database locked errors for sqlite, then we can retry 3 times
- if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
- if sqlError.Code == sqlite3.ErrLocked {
- sess.Rollback()
- time.Sleep(time.Millisecond * time.Duration(10))
- ss.log.Info("Database table locked, sleeping then retrying", "retry", retry)
- return ss.inTransactionWithRetry(ctx, fn, retry+1)
- }
- }
-
- if err != nil {
- sess.Rollback()
- return err
- }
-
- if err = sess.Commit(); err != nil {
- return err
- }
-
- if len(sess.events) > 0 {
- for _, e := range sess.events {
- if err = bus.Publish(e); err != nil {
- ss.log.Error("Failed to publish event after commit", err)
- }
- }
- }
-
- return nil
+ return inTransactionWithRetryCtx(ctx, ss.engine, func(sess *DBSession) error {
+ withValue := context.WithValue(ctx, ContextSessionName, sess)
+ return fn(withValue)
+ }, retry)
}
func inTransactionWithRetry(callback dbTransactionFunc, retry int) error {
- return inTransactionWithRetryCtx(context.Background(), callback, retry)
+ return inTransactionWithRetryCtx(context.Background(), x, callback, retry)
}
-func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc, retry int) error {
- sess, err := startSession(ctx, x, true)
+func inTransactionWithRetryCtx(ctx context.Context, engine *xorm.Engine, callback dbTransactionFunc, retry int) error {
+ sess, err := startSession(ctx, engine, true)
if err != nil {
return err
}
@@ -69,12 +40,12 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc,
err = callback(sess)
- // special handling of database locked errors for sqlite, then we can retry 3 times
+ // special handling of database locked errors for sqlite, then we can retry 5 times
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
- if sqlError.Code == sqlite3.ErrLocked {
+ if sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy {
sess.Rollback()
time.Sleep(time.Millisecond * time.Duration(10))
- sqlog.Info("Database table locked, sleeping then retrying", "retry", retry)
+ sqlog.Info("Database locked, sleeping then retrying", "error", err, "retry", retry)
return inTransactionWithRetry(callback, retry+1)
}
}
@@ -102,5 +73,5 @@ func inTransaction(callback dbTransactionFunc) error {
}
func inTransactionCtx(ctx context.Context, callback dbTransactionFunc) error {
- return inTransactionWithRetryCtx(ctx, callback, 0)
+ return inTransactionWithRetryCtx(ctx, x, callback, 0)
}
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index 311081af4f8e..641fc5f1344f 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -27,6 +27,8 @@ func (ss *SqlStore) addUserQueryAndCommandHandlers() {
bus.AddHandler("sql", GetUserProfile)
bus.AddHandler("sql", SearchUsers)
bus.AddHandler("sql", GetUserOrgList)
+ bus.AddHandler("sql", DisableUser)
+ bus.AddHandler("sql", BatchDisableUsers)
bus.AddHandler("sql", DeleteUser)
bus.AddHandler("sql", UpdateUserPermissions)
bus.AddHandler("sql", SetUserHelpFlag)
@@ -326,6 +328,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
Login: user.Login,
Theme: user.Theme,
IsGrafanaAdmin: user.IsAdmin,
+ IsDisabled: user.IsDisabled,
OrgId: user.OrgId,
}
@@ -450,7 +453,7 @@ func SearchUsers(query *m.SearchUsersQuery) error {
offset := query.Limit * (query.Page - 1)
sess.Limit(query.Limit, offset)
- sess.Cols("id", "email", "name", "login", "is_admin", "last_seen_at")
+ sess.Cols("id", "email", "name", "login", "is_admin", "is_disabled", "last_seen_at")
if err := sess.Find(&query.Result.Users); err != nil {
return err
}
@@ -473,6 +476,43 @@ func SearchUsers(query *m.SearchUsersQuery) error {
return err
}
+func DisableUser(cmd *m.DisableUserCommand) error {
+ user := m.User{}
+ sess := x.Table("user")
+ sess.ID(cmd.UserId).Get(&user)
+
+ user.IsDisabled = cmd.IsDisabled
+ sess.UseBool("is_disabled")
+
+ _, err := sess.ID(cmd.UserId).Update(&user)
+ return err
+}
+
+func BatchDisableUsers(cmd *m.BatchDisableUsersCommand) error {
+ return inTransaction(func(sess *DBSession) error {
+ userIds := cmd.UserIds
+
+ if len(userIds) == 0 {
+ return nil
+ }
+
+ user_id_params := strings.Repeat(",?", len(userIds)-1)
+ disableSQL := "UPDATE " + dialect.Quote("user") + " SET is_disabled=? WHERE Id IN (?" + user_id_params + ")"
+
+ disableParams := []interface{}{disableSQL, cmd.IsDisabled}
+ for _, v := range userIds {
+ disableParams = append(disableParams, v)
+ }
+
+ _, err := sess.Exec(disableParams...)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
func DeleteUser(cmd *m.DeleteUserCommand) error {
return inTransaction(func(sess *DBSession) error {
return deleteUserInTransaction(sess, cmd)
diff --git a/pkg/services/sqlstore/user_auth.go b/pkg/services/sqlstore/user_auth.go
index fd8ec3d057f6..19287b8a8668 100644
--- a/pkg/services/sqlstore/user_auth.go
+++ b/pkg/services/sqlstore/user_auth.go
@@ -5,7 +5,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/bus"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -14,17 +14,18 @@ var getTime = time.Now
func init() {
bus.AddHandler("sql", GetUserByAuthInfo)
+ bus.AddHandler("sql", GetExternalUserInfoByLogin)
bus.AddHandler("sql", GetAuthInfo)
bus.AddHandler("sql", SetAuthInfo)
bus.AddHandler("sql", UpdateAuthInfo)
bus.AddHandler("sql", DeleteAuthInfo)
}
-func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
- user := &m.User{}
+func GetUserByAuthInfo(query *models.GetUserByAuthInfoQuery) error {
+ user := &models.User{}
has := false
var err error
- authQuery := &m.GetAuthInfoQuery{}
+ authQuery := &models.GetAuthInfoQuery{}
// Try to find the user by auth module and id first
if query.AuthModule != "" && query.AuthId != "" {
@@ -32,14 +33,14 @@ func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
authQuery.AuthId = query.AuthId
err = GetAuthInfo(authQuery)
- if err != m.ErrUserNotFound {
+ if err != models.ErrUserNotFound {
if err != nil {
return err
}
// if user id was specified and doesn't match the user_auth entry, remove it
if query.UserId != 0 && query.UserId != authQuery.Result.UserId {
- err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
+ err = DeleteAuthInfo(&models.DeleteAuthInfoCommand{
UserAuth: authQuery.Result,
})
if err != nil {
@@ -55,7 +56,7 @@ func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
if !has {
// if the user has been deleted then remove the entry
- err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
+ err = DeleteAuthInfo(&models.DeleteAuthInfoCommand{
UserAuth: authQuery.Result,
})
if err != nil {
@@ -78,7 +79,7 @@ func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
// If not found, try to find the user by email address
if !has && query.Email != "" {
- user = &m.User{Email: query.Email}
+ user = &models.User{Email: query.Email}
has, err = x.Get(user)
if err != nil {
return err
@@ -87,7 +88,7 @@ func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
// If not found, try to find the user by login
if !has && query.Login != "" {
- user = &m.User{Login: query.Login}
+ user = &models.User{Login: query.Login}
has, err = x.Get(user)
if err != nil {
return err
@@ -96,12 +97,12 @@ func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
// No user found
if !has {
- return m.ErrUserNotFound
+ return models.ErrUserNotFound
}
// create authInfo record to link accounts
if authQuery.Result == nil && query.AuthModule != "" {
- cmd2 := &m.SetAuthInfoCommand{
+ cmd2 := &models.SetAuthInfoCommand{
UserId: user.Id,
AuthModule: query.AuthModule,
AuthId: query.AuthId,
@@ -115,8 +116,32 @@ func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
return nil
}
-func GetAuthInfo(query *m.GetAuthInfoQuery) error {
- userAuth := &m.UserAuth{
+func GetExternalUserInfoByLogin(query *models.GetExternalUserInfoByLoginQuery) error {
+ userQuery := models.GetUserByLoginQuery{LoginOrEmail: query.LoginOrEmail}
+ err := bus.Dispatch(&userQuery)
+ if err != nil {
+ return err
+ }
+
+ authInfoQuery := &models.GetAuthInfoQuery{UserId: userQuery.Result.Id}
+ if err := bus.Dispatch(authInfoQuery); err != nil {
+ return err
+ }
+
+ query.Result = &models.ExternalUserInfo{
+ UserId: userQuery.Result.Id,
+ Login: userQuery.Result.Login,
+ Email: userQuery.Result.Email,
+ Name: userQuery.Result.Name,
+ IsDisabled: userQuery.Result.IsDisabled,
+ AuthModule: authInfoQuery.Result.AuthModule,
+ AuthId: authInfoQuery.Result.AuthId,
+ }
+ return nil
+}
+
+func GetAuthInfo(query *models.GetAuthInfoQuery) error {
+ userAuth := &models.UserAuth{
UserId: query.UserId,
AuthModule: query.AuthModule,
AuthId: query.AuthId,
@@ -126,7 +151,7 @@ func GetAuthInfo(query *m.GetAuthInfoQuery) error {
return err
}
if !has {
- return m.ErrUserNotFound
+ return models.ErrUserNotFound
}
secretAccessToken, err := decodeAndDecrypt(userAuth.OAuthAccessToken)
@@ -149,9 +174,9 @@ func GetAuthInfo(query *m.GetAuthInfoQuery) error {
return nil
}
-func SetAuthInfo(cmd *m.SetAuthInfoCommand) error {
+func SetAuthInfo(cmd *models.SetAuthInfoCommand) error {
return inTransaction(func(sess *DBSession) error {
- authUser := &m.UserAuth{
+ authUser := &models.UserAuth{
UserId: cmd.UserId,
AuthModule: cmd.AuthModule,
AuthId: cmd.AuthId,
@@ -183,9 +208,9 @@ func SetAuthInfo(cmd *m.SetAuthInfoCommand) error {
})
}
-func UpdateAuthInfo(cmd *m.UpdateAuthInfoCommand) error {
+func UpdateAuthInfo(cmd *models.UpdateAuthInfoCommand) error {
return inTransaction(func(sess *DBSession) error {
- authUser := &m.UserAuth{
+ authUser := &models.UserAuth{
UserId: cmd.UserId,
AuthModule: cmd.AuthModule,
AuthId: cmd.AuthId,
@@ -212,7 +237,7 @@ func UpdateAuthInfo(cmd *m.UpdateAuthInfoCommand) error {
authUser.OAuthExpiry = cmd.OAuthToken.Expiry
}
- cond := &m.UserAuth{
+ cond := &models.UserAuth{
UserId: cmd.UserId,
AuthModule: cmd.AuthModule,
}
@@ -222,7 +247,7 @@ func UpdateAuthInfo(cmd *m.UpdateAuthInfoCommand) error {
})
}
-func DeleteAuthInfo(cmd *m.DeleteAuthInfoCommand) error {
+func DeleteAuthInfo(cmd *models.DeleteAuthInfoCommand) error {
return inTransaction(func(sess *DBSession) error {
_, err := sess.Delete(cmd.UserAuth)
return err
diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go
index 84640687ed9f..e5807ea7bf57 100644
--- a/pkg/services/sqlstore/user_test.go
+++ b/pkg/services/sqlstore/user_test.go
@@ -175,6 +175,40 @@ func TestUserDataAccess(t *testing.T) {
So(found, ShouldBeTrue)
})
})
+
+ Convey("When batch disabling users", func() {
+ userIdsToDisable := []int64{}
+ for i := 0; i < 3; i++ {
+ userIdsToDisable = append(userIdsToDisable, users[i].Id)
+ }
+ disableCmd := m.BatchDisableUsersCommand{UserIds: userIdsToDisable, IsDisabled: true}
+
+ err = BatchDisableUsers(&disableCmd)
+ So(err, ShouldBeNil)
+
+ Convey("Should disable all provided users", func() {
+ query := m.SearchUsersQuery{}
+ err = SearchUsers(&query)
+
+ So(query.Result.TotalCount, ShouldEqual, 5)
+ for _, user := range query.Result.Users {
+ shouldBeDisabled := false
+
+ // Check if user id is in the userIdsToDisable list
+ for _, disabledUserId := range userIdsToDisable {
+ if user.Id == disabledUserId {
+ So(user.IsDisabled, ShouldBeTrue)
+ shouldBeDisabled = true
+ }
+ }
+
+ // Otherwise user shouldn't be disabled
+ if !shouldBeDisabled {
+ So(user.IsDisabled, ShouldBeFalse)
+ }
+ }
+ })
+ })
})
Convey("Given one grafana admin user", func() {
diff --git a/pkg/services/user/user.go b/pkg/services/user/user.go
new file mode 100644
index 000000000000..94762c811b08
--- /dev/null
+++ b/pkg/services/user/user.go
@@ -0,0 +1,39 @@
+package user
+
+import (
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/models"
+)
+
+// UpsertArgs are object for Upsert method
+type UpsertArgs struct {
+ ReqContext *models.ReqContext
+ ExternalUser *models.ExternalUserInfo
+ SignupAllowed bool
+}
+
+// Upsert add/update grafana user
+func Upsert(args *UpsertArgs) (*models.User, error) {
+ query := &models.UpsertUserCommand{
+ ReqContext: args.ReqContext,
+ ExternalUser: args.ExternalUser,
+ SignupAllowed: args.SignupAllowed,
+ }
+ err := bus.Dispatch(query)
+ if err != nil {
+ return nil, err
+ }
+
+ return query.Result, nil
+}
+
+// Get the users
+func Get(
+ query *models.SearchUsersQuery,
+) ([]*models.UserSearchHitDTO, error) {
+ if err := bus.Dispatch(query); err != nil {
+ return nil, err
+ }
+
+ return query.Result.Users, nil
+}
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index 788ea7677c99..eef813a3afe5 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -20,7 +20,7 @@ import (
"github.com/go-macaron/session"
ini "gopkg.in/ini.v1"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util"
)
@@ -47,10 +47,11 @@ var (
var (
// App settings.
- Env = DEV
- AppUrl string
- AppSubUrl string
- InstanceName string
+ Env = DEV
+ AppUrl string
+ AppSubUrl string
+ ServeFromSubPath bool
+ InstanceName string
// build
BuildVersion string
@@ -138,7 +139,7 @@ var (
AuthProxyHeaderName string
AuthProxyHeaderProperty string
AuthProxyAutoSignUp bool
- AuthProxyLdapSyncTtl int
+ AuthProxyLDAPSyncTtl int
AuthProxyWhitelist string
AuthProxyHeaders map[string]string
@@ -165,11 +166,11 @@ var (
GoogleTagManagerId string
// LDAP
- LdapEnabled bool
- LdapConfigFile string
- LdapSyncCron string
- LdapAllowSignup bool
- LdapActiveSyncEnabled bool
+ LDAPEnabled bool
+ LDAPConfigFile string
+ LDAPSyncCron string
+ LDAPAllowSignup bool
+ LDAPActiveSyncEnabled bool
// QUOTA
Quota QuotaSettings
@@ -205,8 +206,9 @@ type Cfg struct {
Logger log.Logger
// HTTP Server Settings
- AppUrl string
- AppSubUrl string
+ AppUrl string
+ AppSubUrl string
+ ServeFromSubPath bool
// Paths
ProvisioningPath string
@@ -486,9 +488,9 @@ func (cfg *Cfg) loadConfiguration(args *CommandLineArgs) (*ini.File, error) {
// load specified config file
err = loadSpecifedConfigFile(args.Config, parsedFile)
if err != nil {
- err = cfg.initLogging(parsedFile)
- if err != nil {
- return nil, err
+ err2 := cfg.initLogging(parsedFile)
+ if err2 != nil {
+ return nil, err2
}
log.Fatal(3, err.Error())
}
@@ -610,8 +612,11 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
if err != nil {
return err
}
+ ServeFromSubPath = server.Key("serve_from_sub_path").MustBool(false)
+
cfg.AppUrl = AppUrl
cfg.AppSubUrl = AppSubUrl
+ cfg.ServeFromSubPath = ServeFromSubPath
Protocol = HTTP
protocolStr, err := valueAsString(server, "protocol", "http")
@@ -805,6 +810,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
// auth proxy
authProxy := iniFile.Section("auth.proxy")
AuthProxyEnabled = authProxy.Key("enabled").MustBool(false)
+
AuthProxyHeaderName, err = valueAsString(authProxy, "header_name", "")
if err != nil {
return err
@@ -814,7 +820,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
return err
}
AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true)
- AuthProxyLdapSyncTtl = authProxy.Key("ldap_sync_ttl").MustInt()
+ AuthProxyLDAPSyncTtl = authProxy.Key("ldap_sync_ttl").MustInt()
AuthProxyWhitelist, err = valueAsString(authProxy, "whitelist", "")
if err != nil {
return err
@@ -977,11 +983,11 @@ type RemoteCacheOptions struct {
func (cfg *Cfg) readLDAPConfig() {
ldapSec := cfg.Raw.Section("auth.ldap")
- LdapConfigFile = ldapSec.Key("config_file").String()
- LdapSyncCron = ldapSec.Key("sync_cron").String()
- LdapEnabled = ldapSec.Key("enabled").MustBool(false)
- LdapActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false)
- LdapAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true)
+ LDAPConfigFile = ldapSec.Key("config_file").String()
+ LDAPSyncCron = ldapSec.Key("sync_cron").String()
+ LDAPEnabled = ldapSec.Key("enabled").MustBool(false)
+ LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false)
+ LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true)
}
func (cfg *Cfg) readSessionConfig() {
diff --git a/pkg/setting/setting_session_test.go b/pkg/setting/setting_session_test.go
index 7bf31123473e..0a73b47a91d8 100644
--- a/pkg/setting/setting_session_test.go
+++ b/pkg/setting/setting_session_test.go
@@ -4,7 +4,7 @@ import (
"path/filepath"
"testing"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
. "github.com/smartystreets/goconvey/convey"
)
diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go
index cae8d8bfb73b..abbf6fd69b53 100644
--- a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go
+++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go
@@ -85,14 +85,17 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
azlog.Debug("AzureMonitor", "target", azureMonitorTarget)
urlComponents := map[string]string{}
+ urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString())
urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorTarget["resourceGroup"])
urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorTarget["metricDefinition"])
urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorTarget["resourceName"])
ub := urlBuilder{
- ResourceGroup: urlComponents["resourceGroup"],
- MetricDefinition: urlComponents["metricDefinition"],
- ResourceName: urlComponents["resourceName"],
+ DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(),
+ Subscription: urlComponents["subscription"],
+ ResourceGroup: urlComponents["resourceGroup"],
+ MetricDefinition: urlComponents["metricDefinition"],
+ ResourceName: urlComponents["resourceName"],
}
azureURL := ub.Build()
@@ -199,8 +202,7 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo *mode
}
cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor")
- subscriptionID := dsInfo.JsonData.Get("subscriptionId").MustString()
- proxyPass := fmt.Sprintf("%s/subscriptions/%s", cloudName, subscriptionID)
+ proxyPass := fmt.Sprintf("%s/subscriptions", cloudName)
u, _ := url.Parse(dsInfo.Url)
u.Path = path.Join(u.Path, "render")
diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go
index 39cfe3f76713..94c2aef6c03a 100644
--- a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go
+++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
@@ -27,7 +28,13 @@ func TestAzureMonitorDatasource(t *testing.T) {
},
Queries: []*tsdb.Query{
{
+ DataSource: &models.DataSource{
+ JsonData: simplejson.NewFromAny(map[string]interface{}{
+ "subscriptionId": "default-subscription",
+ }),
+ },
Model: simplejson.NewFromAny(map[string]interface{}{
+ "subscription": "12345678-aaaa-bbbb-cccc-123456789abc",
"azureMonitor": map[string]interface{}{
"timeGrain": "PT1M",
"aggregation": "Average",
@@ -49,7 +56,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
So(len(queries), ShouldEqual, 1)
So(queries[0].RefID, ShouldEqual, "A")
- So(queries[0].URL, ShouldEqual, "resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics")
+ So(queries[0].URL, ShouldEqual, "12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics")
So(queries[0].Target, ShouldEqual, "aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
So(len(queries[0].Params), ShouldEqual, 5)
So(queries[0].Params["timespan"][0], ShouldEqual, "2018-03-15T13:00:00Z/2018-03-15T13:34:00Z")
diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go
index 31a42d21a12f..39014bf38dae 100644
--- a/pkg/tsdb/azuremonitor/azuremonitor.go
+++ b/pkg/tsdb/azuremonitor/azuremonitor.go
@@ -5,7 +5,7 @@ import (
"fmt"
"net/http"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
diff --git a/pkg/tsdb/azuremonitor/url-builder.go b/pkg/tsdb/azuremonitor/url-builder.go
index c252048f5172..445f815e0cd6 100644
--- a/pkg/tsdb/azuremonitor/url-builder.go
+++ b/pkg/tsdb/azuremonitor/url-builder.go
@@ -7,22 +7,30 @@ import (
// urlBuilder builds the URL for calling the Azure Monitor API
type urlBuilder struct {
- ResourceGroup string
- MetricDefinition string
- ResourceName string
+ DefaultSubscription string
+ Subscription string
+ ResourceGroup string
+ MetricDefinition string
+ ResourceName string
}
// Build checks the metric definition property to see which form of the url
// should be returned
func (ub *urlBuilder) Build() string {
+ subscription := ub.Subscription
+
+ if ub.Subscription == "" {
+ subscription = ub.DefaultSubscription
+ }
+
if strings.Count(ub.MetricDefinition, "/") > 1 {
rn := strings.Split(ub.ResourceName, "/")
lastIndex := strings.LastIndex(ub.MetricDefinition, "/")
service := ub.MetricDefinition[lastIndex+1:]
md := ub.MetricDefinition[0:lastIndex]
- return fmt.Sprintf("resourceGroups/%s/providers/%s/%s/%s/%s/providers/microsoft.insights/metrics", ub.ResourceGroup, md, rn[0], service, rn[1])
+ return fmt.Sprintf("%s/resourceGroups/%s/providers/%s/%s/%s/%s/providers/microsoft.insights/metrics", subscription, ub.ResourceGroup, md, rn[0], service, rn[1])
}
- return fmt.Sprintf("resourceGroups/%s/providers/%s/%s/providers/microsoft.insights/metrics", ub.ResourceGroup, ub.MetricDefinition, ub.ResourceName)
+ return fmt.Sprintf("%s/resourceGroups/%s/providers/%s/%s/providers/microsoft.insights/metrics", subscription, ub.ResourceGroup, ub.MetricDefinition, ub.ResourceName)
}
diff --git a/pkg/tsdb/azuremonitor/url-builder_test.go b/pkg/tsdb/azuremonitor/url-builder_test.go
index 85c4f81bc835..2af784a7554a 100644
--- a/pkg/tsdb/azuremonitor/url-builder_test.go
+++ b/pkg/tsdb/azuremonitor/url-builder_test.go
@@ -11,35 +11,51 @@ func TestURLBuilder(t *testing.T) {
Convey("when metric definition is in the short form", func() {
ub := &urlBuilder{
- ResourceGroup: "rg",
- MetricDefinition: "Microsoft.Compute/virtualMachines",
- ResourceName: "rn",
+ DefaultSubscription: "default-sub",
+ ResourceGroup: "rg",
+ MetricDefinition: "Microsoft.Compute/virtualMachines",
+ ResourceName: "rn",
}
url := ub.Build()
- So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
+ So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
+ })
+
+ Convey("when metric definition is in the short form and a subscription is defined", func() {
+ ub := &urlBuilder{
+ DefaultSubscription: "default-sub",
+ Subscription: "specified-sub",
+ ResourceGroup: "rg",
+ MetricDefinition: "Microsoft.Compute/virtualMachines",
+ ResourceName: "rn",
+ }
+
+ url := ub.Build()
+ So(url, ShouldEqual, "specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
})
Convey("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func() {
ub := &urlBuilder{
- ResourceGroup: "rg",
- MetricDefinition: "Microsoft.Storage/storageAccounts/blobServices",
- ResourceName: "rn1/default",
+ DefaultSubscription: "default-sub",
+ ResourceGroup: "rg",
+ MetricDefinition: "Microsoft.Storage/storageAccounts/blobServices",
+ ResourceName: "rn1/default",
}
url := ub.Build()
- So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
+ So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
})
Convey("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func() {
ub := &urlBuilder{
- ResourceGroup: "rg",
- MetricDefinition: "Microsoft.Storage/storageAccounts/fileServices",
- ResourceName: "rn1/default",
+ DefaultSubscription: "default-sub",
+ ResourceGroup: "rg",
+ MetricDefinition: "Microsoft.Storage/storageAccounts/fileServices",
+ ResourceName: "rn1/default",
}
url := ub.Build()
- So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
+ So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
})
})
}
diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index 56e9bd30e98b..34d57f4d71fc 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -10,7 +10,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"golang.org/x/sync/errgroup"
diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index b25e6b105356..844adfadccfd 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -56,6 +56,7 @@ func init() {
"AWS/Connect": {"CallBackNotDialableNumber", "CallRecordingUploadError", "CallsBreachingConcurrencyQuota", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MisconfiguredPhoneNumbers", "MissedCalls", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
"AWS/DDoSProtection": {"AllowedRequests", "BlockedRequests", "CountedRequests", "DDoSAttackBitsPerSecond", "DDoSAttackPacketsPerSecond", "DDoSAttackRequestsPerSecond", "DDoSDetected", "PassedRequests"},
"AWS/DMS": {"CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCIncomingChanges", "CDCLatencySource", "CDCLatencyTarget", "CDCThroughputBandwidthSource", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CPUUtilization", "FreeStorageSpace", "FreeableMemory", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SwapUsage", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+ "AWS/DocDB": {"BackupRetentionPeriodStorageUsed", "BufferCacheHitRatio", "CPUUtilization", "DatabaseConnections", "DBInstanceReplicaLag", "DBClusterReplicaLagMaximum", "DBClusterReplicaLagMinimum", "DiskQueueDepth", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SnapshotStorageUsed", "SwapUsage", "TotalBackupStorageBilled", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/DX": {"ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelRx", "ConnectionLightLevelTx", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionState"},
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "PendingReplicationCount", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReplicationLatency", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "TimeToLiveDeletedItemCount", "UserErrors", "WriteThrottleEvents"},
"AWS/EBS": {"BurstBalance", "VolumeConsumedReadWriteOps", "VolumeIdleTime", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeThroughputPercentage", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps"},
@@ -135,6 +136,7 @@ func init() {
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
"AWS/DDoSProtection": {"Region", "Rule", "RuleGroup", "WebACL"},
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
+ "AWS/DocDB": {"DBClusterIdentifier"},
"AWS/DX": {"ConnectionId"},
"AWS/DynamoDB": {"GlobalSecondaryIndexName", "Operation", "ReceivingRegion", "StreamLabel", "TableName"},
"AWS/EBS": {"VolumeId"},
diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go
index 131bcdc97844..48f9cce0a5a2 100644
--- a/pkg/tsdb/elasticsearch/client/client.go
+++ b/pkg/tsdb/elasticsearch/client/client.go
@@ -13,7 +13,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/models"
diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go
index 857b847f0f93..01c9194877c9 100644
--- a/pkg/tsdb/elasticsearch/elasticsearch.go
+++ b/pkg/tsdb/elasticsearch/elasticsearch.go
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go
index 9a5de205a23a..b74112c3001e 100644
--- a/pkg/tsdb/graphite/graphite.go
+++ b/pkg/tsdb/graphite/graphite.go
@@ -13,7 +13,7 @@ import (
"golang.org/x/net/context/ctxhttp"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go
index 6bff961f981f..3b8f365211a5 100644
--- a/pkg/tsdb/influxdb/influxdb.go
+++ b/pkg/tsdb/influxdb/influxdb.go
@@ -10,7 +10,7 @@ import (
"path"
"strings"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index ec3f103a4ef2..c0794863efe5 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -6,6 +6,7 @@ import (
"strings"
"time"
+ "github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/tsdb"
)
@@ -74,7 +75,7 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
}
- interval, err := time.ParseDuration(strings.Trim(args[1], `'"`))
+ interval, err := gtime.ParseInterval(strings.Trim(args[1], `'"`))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
@@ -109,7 +110,7 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
}
- interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+ interval, err := gtime.ParseInterval(strings.Trim(args[1], `'`))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go
index 6503632aea3a..81a7e159a868 100644
--- a/pkg/tsdb/mssql/mssql.go
+++ b/pkg/tsdb/mssql/mssql.go
@@ -8,7 +8,7 @@ import (
_ "github.com/denisenkom/go-mssqldb"
"github.com/go-xorm/core"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util"
diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index bbd928b05631..8cd83d2f9991 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -4,8 +4,8 @@ import (
"fmt"
"regexp"
"strings"
- "time"
+ "github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/tsdb"
)
@@ -69,7 +69,7 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
}
- interval, err := time.ParseDuration(strings.Trim(args[1], `'"`))
+ interval, err := gtime.ParseInterval(strings.Trim(args[1], `'"`))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
@@ -104,7 +104,7 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
}
- interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+ interval, err := gtime.ParseInterval(strings.Trim(args[1], `'`))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go
index 95a9e02598f0..1f569f215db9 100644
--- a/pkg/tsdb/mysql/mysql.go
+++ b/pkg/tsdb/mysql/mysql.go
@@ -10,7 +10,7 @@ import (
"github.com/go-sql-driver/mysql"
"github.com/go-xorm/core"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go
index d0f61e052335..372f0ef67849 100644
--- a/pkg/tsdb/opentsdb/opentsdb.go
+++ b/pkg/tsdb/opentsdb/opentsdb.go
@@ -15,7 +15,7 @@ import (
"net/url"
"github.com/grafana/grafana/pkg/components/null"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 2efba13d31ac..f7a194e63cd1 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -6,6 +6,7 @@ import (
"strings"
"time"
+ "github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/tsdb"
)
@@ -95,7 +96,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
}
- interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+ interval, err := gtime.ParseInterval(strings.Trim(args[1], `'`))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
@@ -139,7 +140,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
}
- interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+ interval, err := gtime.ParseInterval(strings.Trim(args[1], `'`))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index 7840a47fb187..dc3a006123b3 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -7,7 +7,7 @@ import (
"strconv"
"github.com/go-xorm/core"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go
index bb343aea26e7..04fef37c2cf2 100644
--- a/pkg/tsdb/prometheus/prometheus.go
+++ b/pkg/tsdb/prometheus/prometheus.go
@@ -12,7 +12,7 @@ import (
"net/http"
"github.com/grafana/grafana/pkg/components/null"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
api "github.com/prometheus/client_golang/api"
diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index ab7230e9b029..b4ceead85678 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -12,7 +12,7 @@ import (
"sync"
"time"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/components/null"
diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go
index 76b346553ad2..e811e70c775d 100644
--- a/pkg/tsdb/stackdriver/stackdriver.go
+++ b/pkg/tsdb/stackdriver/stackdriver.go
@@ -21,7 +21,7 @@ import (
"github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go
index 964e6f586f33..3e7aa0541a8a 100644
--- a/pkg/tsdb/testdata/scenarios.go
+++ b/pkg/tsdb/testdata/scenarios.go
@@ -2,6 +2,7 @@ package testdata
import (
"encoding/json"
+ "fmt"
"math"
"math/rand"
"strconv"
@@ -9,7 +10,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/null"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tsdb"
)
@@ -261,6 +262,84 @@ func init() {
return queryRes
},
})
+
+ registerScenario(&Scenario{
+ Id: "logs",
+ Name: "Logs",
+
+ Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
+ from := context.TimeRange.GetFromAsMsEpoch()
+ to := context.TimeRange.GetToAsMsEpoch()
+ lines := query.Model.Get("lines").MustInt64(10)
+ includeLevelColumn := query.Model.Get("levelColumn").MustBool(false)
+
+ logLevelGenerator := newRandomStringProvider([]string{
+ "emerg",
+ "alert",
+ "crit",
+ "critical",
+ "warn",
+ "warning",
+ "err",
+ "eror",
+ "error",
+ "info",
+ "notice",
+ "dbug",
+ "debug",
+ "trace",
+ "",
+ })
+ containerIDGenerator := newRandomStringProvider([]string{
+ "f36a9eaa6d34310686f2b851655212023a216de955cbcc764210cefa71179b1a",
+ "5a354a630364f3742c602f315132e16def594fe68b1e4a195b2fce628e24c97a",
+ })
+ hostnameGenerator := newRandomStringProvider([]string{
+ "srv-001",
+ "srv-002",
+ })
+
+ table := tsdb.Table{
+ Columns: []tsdb.TableColumn{
+ {Text: "time"},
+ {Text: "message"},
+ {Text: "container_id"},
+ {Text: "hostname"},
+ },
+ Rows: []tsdb.RowValues{},
+ }
+
+ if includeLevelColumn {
+ table.Columns = append(table.Columns, tsdb.TableColumn{Text: "level"})
+ }
+
+ for i := int64(0); i < lines && to > from; i++ {
+ row := tsdb.RowValues{float64(to)}
+
+ logLevel := logLevelGenerator.Next()
+ timeFormatted := time.Unix(to/1000, 0).Format(time.RFC3339)
+ lvlString := ""
+ if !includeLevelColumn {
+ lvlString = fmt.Sprintf("lvl=%s ", logLevel)
+ }
+
+ row = append(row, fmt.Sprintf("t=%s %smsg=\"Request Completed\" logger=context userId=1 orgId=1 uname=admin method=GET path=/api/datasources/proxy/152/api/prom/label status=502 remote_addr=[::1] time_ms=1 size=0 referer=\"http://localhost:3000/explore?left=%%5B%%22now-6h%%22,%%22now%%22,%%22Prometheus%%202.x%%22,%%7B%%7D,%%7B%%22ui%%22:%%5Btrue,true,true,%%22none%%22%%5D%%7D%%5D\"", timeFormatted, lvlString))
+ row = append(row, containerIDGenerator.Next())
+ row = append(row, hostnameGenerator.Next())
+
+ if includeLevelColumn {
+ row = append(row, logLevel)
+ }
+
+ table.Rows = append(table.Rows, row)
+ to -= query.IntervalMs
+ }
+
+ queryRes := tsdb.NewQueryResult()
+ queryRes.Tables = append(queryRes.Tables, &table)
+ return queryRes
+ },
+ })
}
func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResult {
diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go
index c2c2ea3f696c..11b25c73542f 100644
--- a/pkg/tsdb/testdata/testdata.go
+++ b/pkg/tsdb/testdata/testdata.go
@@ -3,7 +3,7 @@ package testdata
import (
"context"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
diff --git a/pkg/tsdb/testdata/utils.go b/pkg/tsdb/testdata/utils.go
new file mode 100644
index 000000000000..85c02e8a296d
--- /dev/null
+++ b/pkg/tsdb/testdata/utils.go
@@ -0,0 +1,22 @@
+package testdata
+
+import (
+ "math/rand"
+ "time"
+)
+
+type randomStringProvider struct {
+ r *rand.Rand
+ data []string
+}
+
+func newRandomStringProvider(data []string) *randomStringProvider {
+ return &randomStringProvider{
+ r: rand.New(rand.NewSource(time.Now().UnixNano())),
+ data: data,
+ }
+}
+
+func (p *randomStringProvider) Next() string {
+ return p.data[p.r.Int31n(int32(len(p.data)))]
+}
diff --git a/pkg/util/errutil/errors.go b/pkg/util/errutil/errors.go
index 37f26ff9564c..65edd11779b9 100644
--- a/pkg/util/errutil/errors.go
+++ b/pkg/util/errutil/errors.go
@@ -1,11 +1,29 @@
package errutil
-import "golang.org/x/xerrors"
+import (
+ "fmt"
+
+ "golang.org/x/xerrors"
+)
// Wrap is a simple wrapper around Errorf that is doing error wrapping. You can read how that works in
// https://godoc.org/golang.org/x/xerrors#Errorf but its API is very implicit which is a reason for this wrapper.
// There is also a discussion (https://github.com/golang/go/issues/29934) where many comments make arguments for such
// wrapper so hopefully it will be added in the standard lib later.
func Wrap(message string, err error) error {
+ if err == nil {
+ return nil
+ }
+
return xerrors.Errorf("%v: %w", message, err)
}
+
+// Wrapf is a simple wrapper around Errorf that is doing error wrapping
+// Wrapf allows you to send a format and args instead of just a message.
+func Wrapf(err error, message string, a ...interface{}) error {
+ if err == nil {
+ return nil
+ }
+
+ return Wrap(fmt.Sprintf(message, a...), err)
+}
diff --git a/pkg/util/strings.go b/pkg/util/strings.go
index 9eaa141edbfb..9ce5d03e126c 100644
--- a/pkg/util/strings.go
+++ b/pkg/util/strings.go
@@ -4,6 +4,7 @@ import (
"fmt"
"math"
"regexp"
+ "strings"
"time"
)
@@ -66,3 +67,19 @@ func GetAgeString(t time.Time) string {
return "< 1m"
}
+
+// ToCamelCase changes kebab case, snake case or mixed strings to camel case. See unit test for examples.
+func ToCamelCase(str string) string {
+ var finalParts []string
+ parts := strings.Split(str, "_")
+
+ for _, part := range parts {
+ finalParts = append(finalParts, strings.Split(part, "-")...)
+ }
+
+ for index, part := range finalParts[1:] {
+ finalParts[index+1] = strings.Title(part)
+ }
+
+ return strings.Join(finalParts, "")
+}
diff --git a/pkg/util/strings_test.go b/pkg/util/strings_test.go
index 0cc1905baff8..4bc52ee75217 100644
--- a/pkg/util/strings_test.go
+++ b/pkg/util/strings_test.go
@@ -37,3 +37,12 @@ func TestDateAge(t *testing.T) {
So(GetAgeString(time.Now().Add(-time.Hour*24*409)), ShouldEqual, "1y")
})
}
+
+func TestToCamelCase(t *testing.T) {
+ Convey("ToCamelCase", t, func() {
+ So(ToCamelCase("kebab-case-string"), ShouldEqual, "kebabCaseString")
+ So(ToCamelCase("snake_case_string"), ShouldEqual, "snakeCaseString")
+ So(ToCamelCase("mixed-case_string"), ShouldEqual, "mixedCaseString")
+ So(ToCamelCase("alreadyCamelCase"), ShouldEqual, "alreadyCamelCase")
+ })
+}
diff --git a/public/app/core/actions/application.ts b/public/app/core/actions/application.ts
new file mode 100644
index 000000000000..9bde989e8ca6
--- /dev/null
+++ b/public/app/core/actions/application.ts
@@ -0,0 +1,3 @@
+import { noPayloadActionCreatorFactory } from 'app/core/redux';
+
+export const toggleLogActions = noPayloadActionCreatorFactory('TOGGLE_LOG_ACTIONS').create();
diff --git a/public/app/core/actions/location.ts b/public/app/core/actions/location.ts
index 34ab43da7d22..567934e12cbe 100644
--- a/public/app/core/actions/location.ts
+++ b/public/app/core/actions/location.ts
@@ -1,4 +1,4 @@
-import { LocationUpdate } from 'app/types';
+import { LocationUpdate } from '@grafana/runtime';
import { actionCreatorFactory } from 'app/core/redux';
export const updateLocation = actionCreatorFactory
('UPDATE_LOCATION').create();
diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts
index 6da0ca1a44df..30163be0edf5 100644
--- a/public/app/core/angular_wrappers.ts
+++ b/public/app/core/angular_wrappers.ts
@@ -4,7 +4,6 @@ import { AnnotationQueryEditor as StackdriverAnnotationQueryEditor } from 'app/p
import { PasswordStrength } from './components/PasswordStrength';
import PageHeader from './components/PageHeader/PageHeader';
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
-import { SearchResult } from './components/search/SearchResult';
import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu';
import { MetricSelect } from './components/Select/MetricSelect';
@@ -20,7 +19,6 @@ export function registerAngularDirectives() {
react2AngularDirective('appNotificationsList', AppNotificationList, []);
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
- react2AngularDirective('searchResult', SearchResult, []);
react2AngularDirective('searchField', SearchField, [
'query',
'autoFocus',
diff --git a/public/app/core/components/AppNotifications/AppNotificationItem.tsx b/public/app/core/components/AppNotifications/AppNotificationItem.tsx
index d1fc506d54cd..f1617a0a6351 100644
--- a/public/app/core/components/AppNotifications/AppNotificationItem.tsx
+++ b/public/app/core/components/AppNotifications/AppNotificationItem.tsx
@@ -4,11 +4,11 @@ import { AlertBox } from '../AlertBox/AlertBox';
interface Props {
appNotification: AppNotification;
- onClearNotification: (id) => void;
+ onClearNotification: (id: number) => void;
}
export default class AppNotificationItem extends Component {
- shouldComponentUpdate(nextProps) {
+ shouldComponentUpdate(nextProps: Props) {
return this.props.appNotification.id !== nextProps.appNotification.id;
}
diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx
index c91f8372384f..991ac37eda6c 100644
--- a/public/app/core/components/AppNotifications/AppNotificationList.tsx
+++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx
@@ -20,12 +20,12 @@ export class AppNotificationList extends PureComponent {
componentDidMount() {
const { notifyApp } = this.props;
- appEvents.on('alert-warning', options => notifyApp(createWarningNotification(options[0], options[1])));
- appEvents.on('alert-success', options => notifyApp(createSuccessNotification(options[0], options[1])));
- appEvents.on('alert-error', options => notifyApp(createErrorNotification(options[0], options[1])));
+ appEvents.on('alert-warning', (options: string[]) => notifyApp(createWarningNotification(options[0], options[1])));
+ appEvents.on('alert-success', (options: string[]) => notifyApp(createSuccessNotification(options[0], options[1])));
+ appEvents.on('alert-error', (options: string[]) => notifyApp(createErrorNotification(options[0], options[1])));
}
- onClearAppNotification = id => {
+ onClearAppNotification = (id: number) => {
this.props.clearAppNotification(id);
};
diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx
index f83d5c957f74..016c47776686 100644
--- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx
+++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx
@@ -14,10 +14,10 @@ export interface Props {
export default class OrgActionBar extends PureComponent {
render() {
const { searchQuery, layoutMode, onSetLayoutMode, linkButton, setSearchQuery, target } = this.props;
- const linkProps = { href: linkButton.href, target: undefined };
+ const linkProps = { href: linkButton.href };
if (target) {
- linkProps.target = target;
+ (linkProps as any).target = target;
}
return (
diff --git a/public/app/core/components/PageHeader/PageHeader.test.tsx b/public/app/core/components/PageHeader/PageHeader.test.tsx
index a9ba8d008a3a..1999831a6d55 100644
--- a/public/app/core/components/PageHeader/PageHeader.test.tsx
+++ b/public/app/core/components/PageHeader/PageHeader.test.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import PageHeader from './PageHeader';
-import { shallow } from 'enzyme';
+import { shallow, ShallowWrapper } from 'enzyme';
describe('PageHeader', () => {
- let wrapper;
+ let wrapper: ShallowWrapper;
describe('when the nav tree has a node with a title', () => {
beforeAll(() => {
diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx
index 1d676a00a375..6c6bb697f653 100644
--- a/public/app/core/components/PasswordStrength.tsx
+++ b/public/app/core/components/PasswordStrength.tsx
@@ -5,7 +5,7 @@ export interface Props {
}
export class PasswordStrength extends React.Component {
- constructor(props) {
+ constructor(props: Props) {
super(props);
}
diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx
index d55ec3511ea0..b668229939ff 100644
--- a/public/app/core/components/PermissionList/AddPermission.tsx
+++ b/public/app/core/components/PermissionList/AddPermission.tsx
@@ -22,7 +22,7 @@ class AddPermissions extends Component {
showPermissionLevels: true,
};
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.state = this.getCleanState();
}
@@ -36,7 +36,7 @@ class AddPermissions extends Component {
};
}
- onTypeChanged = evt => {
+ onTypeChanged = (evt: any) => {
const type = evt.target.value as AclTarget;
switch (type) {
@@ -65,7 +65,7 @@ class AddPermissions extends Component {
this.setState({ permission: permission.value });
};
- onSubmit = async evt => {
+ onSubmit = async (evt: React.SyntheticEvent) => {
evt.preventDefault();
await this.props.onAddPermission(this.state);
this.setState(this.getCleanState());
diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx
index b949209d6a38..f590f5fd8ae3 100644
--- a/public/app/core/components/PermissionList/PermissionListItem.tsx
+++ b/public/app/core/components/PermissionList/PermissionListItem.tsx
@@ -1,13 +1,13 @@
import React, { PureComponent } from 'react';
-import { Select } from '@grafana/ui';
+import { Select, SelectOptionItem } from '@grafana/ui';
import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
import { FolderInfo } from 'app/types';
-const setClassNameHelper = inherited => {
+const setClassNameHelper = (inherited: boolean) => {
return inherited ? 'gf-form-disabled' : '';
};
-function ItemAvatar({ item }) {
+function ItemAvatar({ item }: { item: DashboardAcl }) {
if (item.userAvatarUrl) {
return ;
}
@@ -21,7 +21,7 @@ function ItemAvatar({ item }) {
return ;
}
-function ItemDescription({ item }) {
+function ItemDescription({ item }: { item: DashboardAcl }) {
if (item.userId) {
return (User);
}
@@ -39,8 +39,8 @@ interface Props {
}
export default class PermissionsListItem extends PureComponent {
- onPermissionChanged = option => {
- this.props.onPermissionChanged(this.props.item, option.value as PermissionLevel);
+ onPermissionChanged = (option: SelectOptionItem) => {
+ this.props.onPermissionChanged(this.props.item, option.value);
};
onRemoveItem = () => {
diff --git a/public/app/core/components/PluginHelp/PluginHelp.tsx b/public/app/core/components/PluginHelp/PluginHelp.tsx
index c37498afc459..40aed4a6c0c8 100644
--- a/public/app/core/components/PluginHelp/PluginHelp.tsx
+++ b/public/app/core/components/PluginHelp/PluginHelp.tsx
@@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
+// @ts-ignore
import Remarkable from 'remarkable';
-import { getBackendSrv } from '../../services/backend_srv';
+import { getBackendSrv } from '@grafana/runtime';
interface Props {
plugin: {
@@ -37,7 +38,7 @@ export class PluginHelp extends PureComponent {
getBackendSrv()
.get(`/api/plugins/${plugin.id}/markdown/${type}`)
- .then(response => {
+ .then((response: string) => {
const markdown = new Remarkable();
const helpHtml = markdown.render(response);
diff --git a/public/app/core/components/Select/MetricSelect.tsx b/public/app/core/components/Select/MetricSelect.tsx
index e4b6cfb8abd9..aa0ea5d40c45 100644
--- a/public/app/core/components/Select/MetricSelect.tsx
+++ b/public/app/core/components/Select/MetricSelect.tsx
@@ -19,13 +19,13 @@ interface State {
}
export class MetricSelect extends React.Component {
- static defaultProps = {
+ static defaultProps: Partial = {
variables: [],
options: [],
isSearchable: true,
};
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.state = { options: [] };
}
@@ -45,7 +45,7 @@ export class MetricSelect extends React.Component {
return nextProps.value !== this.props.value || !_.isEqual(nextOptions, this.state.options);
}
- buildOptions({ variables = [], options }) {
+ buildOptions({ variables = [], options }: Props) {
return variables.length > 0 ? [this.getVariablesGroup(), ...options] : options;
}
diff --git a/public/app/core/components/Select/TeamPicker.test.tsx b/public/app/core/components/Select/TeamPicker.test.tsx
index 3db9f7bb4eb4..8ee09461c7fa 100644
--- a/public/app/core/components/Select/TeamPicker.test.tsx
+++ b/public/app/core/components/Select/TeamPicker.test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+// @ts-ignore
import renderer from 'react-test-renderer';
import { TeamPicker } from './TeamPicker';
diff --git a/public/app/core/components/Select/TeamPicker.tsx b/public/app/core/components/Select/TeamPicker.tsx
index 8d9e1d48d810..16df350ad67f 100644
--- a/public/app/core/components/Select/TeamPicker.tsx
+++ b/public/app/core/components/Select/TeamPicker.tsx
@@ -23,7 +23,7 @@ export interface State {
export class TeamPicker extends Component {
debouncedSearch: any;
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.state = { isLoading: false };
this.search = this.search.bind(this);
@@ -42,8 +42,8 @@ export class TeamPicker extends Component {
query = '';
}
- return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => {
- const teams = result.teams.map(team => {
+ return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then((result: any) => {
+ const teams = result.teams.map((team: any) => {
return {
id: team.id,
value: team.id,
diff --git a/public/app/core/components/Select/UserPicker.test.tsx b/public/app/core/components/Select/UserPicker.test.tsx
index 054ca643700e..b1dddb48cff0 100644
--- a/public/app/core/components/Select/UserPicker.test.tsx
+++ b/public/app/core/components/Select/UserPicker.test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+// @ts-ignore
import renderer from 'react-test-renderer';
import { UserPicker } from './UserPicker';
diff --git a/public/app/core/components/Select/UserPicker.tsx b/public/app/core/components/Select/UserPicker.tsx
index ff4ae32f0683..ba054a20620c 100644
--- a/public/app/core/components/Select/UserPicker.tsx
+++ b/public/app/core/components/Select/UserPicker.tsx
@@ -24,7 +24,7 @@ export interface State {
export class UserPicker extends Component {
debouncedSearch: any;
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.state = { isLoading: false };
this.search = this.search.bind(this);
@@ -45,8 +45,8 @@ export class UserPicker extends Component {
return backendSrv
.get(`/api/org/users?query=${query}&limit=10`)
- .then(result => {
- return result.map(user => ({
+ .then((result: any) => {
+ return result.map((user: any) => ({
id: user.userId,
value: user.userId,
label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx
index a39eefcec4b3..b6d19f1f8af2 100644
--- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx
+++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx
@@ -1,9 +1,9 @@
import React, { PureComponent } from 'react';
import { FormLabel, Select } from '@grafana/ui';
-import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
import { DashboardSearchHit, DashboardSearchHitType } from 'app/types';
+import { getBackendSrv } from 'app/core/services/backend_srv';
export interface Props {
resourceUri: string;
@@ -25,9 +25,9 @@ const timezones = [
];
export class SharedPreferences extends PureComponent {
- backendSrv: BackendSrv = getBackendSrv();
+ backendSrv = getBackendSrv();
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.state = {
@@ -72,7 +72,7 @@ export class SharedPreferences extends PureComponent {
});
}
- onSubmitForm = async event => {
+ onSubmitForm = async (event: React.SyntheticEvent) => {
event.preventDefault();
const { homeDashboardId, theme, timezone } = this.state;
diff --git a/public/app/core/components/TagFilter/TagBadge.tsx b/public/app/core/components/TagFilter/TagBadge.tsx
index b00b95b70cba..d7a0c8977958 100644
--- a/public/app/core/components/TagFilter/TagBadge.tsx
+++ b/public/app/core/components/TagFilter/TagBadge.tsx
@@ -9,7 +9,7 @@ export interface Props {
}
export class TagBadge extends React.Component {
- constructor(props) {
+ constructor(props: Props) {
super(props);
}
diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx
index 967792a9f7b2..89d3e8469b6b 100644
--- a/public/app/core/components/TagFilter/TagFilter.tsx
+++ b/public/app/core/components/TagFilter/TagFilter.tsx
@@ -1,10 +1,14 @@
+// Libraries
import React from 'react';
-import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
+// @ts-ignore
+import { components } from '@torkelo/react-select';
+// @ts-ignore
import AsyncSelect from '@torkelo/react-select/lib/Async';
+// Components
import { TagOption } from './TagOption';
import { TagBadge } from './TagBadge';
-import { components } from '@torkelo/react-select';
+import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
import { escapeStringForRegex } from '../FilterInput/FilterInput';
export interface Props {
@@ -16,12 +20,12 @@ export interface Props {
export class TagFilter extends React.Component {
inlineTags: boolean;
- constructor(props) {
+ constructor(props: Props) {
super(props);
}
- onLoadOptions = query => {
- return this.props.tagOptions().then(options => {
+ onLoadOptions = (query: string) => {
+ return this.props.tagOptions().then((options: any[]) => {
return options.map(option => ({
value: option.term,
label: option.term,
@@ -47,11 +51,11 @@ export class TagFilter extends React.Component {
placeholder: 'Tags',
loadingMessage: () => 'Loading...',
noOptionsMessage: () => 'No tags found',
- getOptionValue: i => i.value,
- getOptionLabel: i => i.label,
+ getOptionValue: (i: any) => i.value,
+ getOptionLabel: (i: any) => i.label,
value: tags,
styles: resetSelectStyles(),
- filterOption: (option, searchQuery) => {
+ filterOption: (option: any, searchQuery: string) => {
const regex = RegExp(escapeStringForRegex(searchQuery), 'i');
return regex.test(option.value);
},
@@ -59,10 +63,10 @@ export class TagFilter extends React.Component {
Option: TagOption,
IndicatorsContainer,
NoOptionsMessage,
- MultiValueLabel: () => {
+ MultiValueLabel: (): any => {
return null; // We want the whole tag to be clickable so we use MultiValueRemove instead
},
- MultiValueRemove: props => {
+ MultiValueRemove: (props: any) => {
const { data } = props;
return (
diff --git a/public/app/core/components/TagFilter/TagOption.tsx b/public/app/core/components/TagFilter/TagOption.tsx
index 9db15aaca0b3..4da6202bf3e8 100644
--- a/public/app/core/components/TagFilter/TagOption.tsx
+++ b/public/app/core/components/TagFilter/TagOption.tsx
@@ -1,4 +1,6 @@
+// Libraries
import React from 'react';
+// @ts-ignore
import { components } from '@torkelo/react-select';
import { OptionProps } from 'react-select/lib/components/Option';
import { TagBadge } from './TagBadge';
diff --git a/public/app/core/components/TagFilter/TagValue.tsx b/public/app/core/components/TagFilter/TagValue.tsx
index 43e41c7fdc29..0406ead16a62 100644
--- a/public/app/core/components/TagFilter/TagValue.tsx
+++ b/public/app/core/components/TagFilter/TagValue.tsx
@@ -9,18 +9,17 @@ export interface Props {
}
export class TagValue extends React.Component {
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.onClick = this.onClick.bind(this);
}
- onClick(event) {
+ onClick(event: React.SyntheticEvent) {
this.props.onRemove(this.props.value, event);
}
render() {
const { value } = this.props;
-
return ;
}
}
diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
index e550afc20c3f..07a39cfd1094 100644
--- a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
+++ b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
@@ -21,7 +21,7 @@ export default class ToggleButtonGroup extends PureComponent void;
+ onChange?: (value: any) => void;
selected?: boolean;
value: any;
className?: string;
@@ -37,9 +37,9 @@ export const ToggleButton: FC = ({
tooltip,
onChange,
}) => {
- const onClick = event => {
+ const onClick = (event: React.SyntheticEvent) => {
event.stopPropagation();
- if (onChange) {
+ if (!selected && onChange) {
onChange(value);
}
};
diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts
index ca9023e86570..542454988bd6 100644
--- a/public/app/core/components/code_editor/code_editor.ts
+++ b/public/app/core/components/code_editor/code_editor.ts
@@ -55,7 +55,7 @@ const DEFAULT_SNIPPETS = true;
const editorTemplate = ``;
-function link(scope, elem, attrs) {
+function link(scope: any, elem: any, attrs: any) {
// Options
const langMode = attrs.mode || DEFAULT_MODE;
const maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
@@ -116,7 +116,7 @@ function link(scope, elem, attrs) {
});
// Sync with outer scope - update editor content if model has been changed from outside of directive.
- scope.$watch('content', (newValue, oldValue) => {
+ scope.$watch('content', (newValue: any, oldValue: any) => {
const editorValue = codeEditor.getValue();
if (newValue !== editorValue && newValue !== oldValue) {
scope.$$postDigest(() => {
@@ -142,7 +142,7 @@ function link(scope, elem, attrs) {
},
});
- function setLangMode(lang) {
+ function setLangMode(lang: string) {
ace.acequire('ace/ext/language_tools');
codeEditor.setOptions({
enableBasicAutocompletion: true,
@@ -170,7 +170,7 @@ function link(scope, elem, attrs) {
codeEditor.setTheme(theme);
}
- function setEditorContent(value) {
+ function setEditorContent(value: string) {
codeEditor.setValue(value);
codeEditor.clearSelection();
}
diff --git a/public/app/core/components/colorpicker/spectrum_picker.ts b/public/app/core/components/colorpicker/spectrum_picker.ts
index 4576648df835..d9831aa96440 100644
--- a/public/app/core/components/colorpicker/spectrum_picker.ts
+++ b/public/app/core/components/colorpicker/spectrum_picker.ts
@@ -13,9 +13,9 @@ export function spectrumPicker() {
scope: true,
replace: true,
template: '',
- link: (scope, element, attrs, ngModel) => {
+ link: (scope: any, element: any, attrs: any, ngModel: any) => {
scope.ngModel = ngModel;
- scope.onColorChange = color => {
+ scope.onColorChange = (color: string) => {
ngModel.$setViewValue(color);
};
},
diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts
index e1809f3d42cd..f312c77f6a85 100644
--- a/public/app/core/components/dashboard_selector.ts
+++ b/public/app/core/components/dashboard_selector.ts
@@ -1,4 +1,5 @@
import coreModule from 'app/core/core_module';
+import { BackendSrv } from '../services/backend_srv';
const template = `
@@ -9,7 +10,7 @@ export class DashboardSelectorCtrl {
options: any;
/** @ngInject */
- constructor(private backendSrv) {}
+ constructor(private backendSrv: BackendSrv) {}
$onInit() {
this.options = [{ value: 0, text: 'Default' }];
diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts
index 7ef54339f497..63be172a5deb 100644
--- a/public/app/core/components/help/help.ts
+++ b/public/app/core/components/help/help.ts
@@ -13,8 +13,6 @@ export class HelpCtrl {
{ keys: ['g', 'h'], description: 'Go to Home Dashboard' },
{ keys: ['g', 'p'], description: 'Go to Profile' },
{ keys: ['s', 'o'], description: 'Open search' },
- { keys: ['s', 's'], description: 'Open search with starred filter' },
- { keys: ['s', 't'], description: 'Open search in tags view' },
{ keys: ['esc'], description: 'Exit edit/setting views' },
],
Dashboard: [
diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts
index 2ada91b09f1b..704514659303 100644
--- a/public/app/core/components/info_popover.ts
+++ b/public/app/core/components/info_popover.ts
@@ -1,5 +1,6 @@
import _ from 'lodash';
import coreModule from 'app/core/core_module';
+// @ts-ignore
import Drop from 'tether-drop';
export function infoPopover() {
@@ -7,7 +8,7 @@ export function infoPopover() {
restrict: 'E',
template: '',
transclude: true,
- link: (scope, elem, attrs, ctrl, transclude) => {
+ link: (scope: any, elem: any, attrs: any, ctrl: any, transclude: any) => {
const offset = attrs.offset || '0 -10px';
const position = attrs.position || 'right middle';
let classes = 'drop-help drop-hide-out-of-bounds';
@@ -23,7 +24,7 @@ export function infoPopover() {
elem.addClass('gf-form-help-icon--' + attrs.mode);
}
- transclude((clone, newScope) => {
+ transclude((clone: any, newScope: any) => {
const content = document.createElement('div');
content.className = 'markdown-html';
diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html
deleted file mode 100644
index 6d611692efc9..000000000000
--- a/public/app/core/components/navbar/navbar.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
diff --git a/public/app/core/components/navbar/navbar.ts b/public/app/core/components/navbar/navbar.ts
deleted file mode 100644
index f4de87451728..000000000000
--- a/public/app/core/components/navbar/navbar.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import coreModule from '../../core_module';
-import appEvents from 'app/core/app_events';
-import { NavModel } from '@grafana/ui';
-
-export class NavbarCtrl {
- model: NavModel;
-
- /** @ngInject */
- constructor() {}
-
- showSearch() {
- appEvents.emit('show-dash-search');
- }
-
- navItemClicked(navItem, evt) {
- if (navItem.clickHandler) {
- navItem.clickHandler();
- evt.preventDefault();
- }
- }
-}
-
-export function navbarDirective() {
- return {
- restrict: 'E',
- templateUrl: 'public/app/core/components/navbar/navbar.html',
- controller: NavbarCtrl,
- bindToController: true,
- controllerAs: 'ctrl',
- scope: {
- model: '=',
- },
- link: (scope, elem) => {},
- };
-}
-
-export function pageH1() {
- return {
- restrict: 'E',
- template: `
-
- `,
- scope: {
- model: '=',
- },
- };
-}
-
-coreModule.directive('pageH1', pageH1);
-coreModule.directive('navbar', navbarDirective);
diff --git a/public/app/core/components/org_switcher.ts b/public/app/core/components/org_switcher.ts
index dc8f00f0fdb4..0e25482e190a 100644
--- a/public/app/core/components/org_switcher.ts
+++ b/public/app/core/components/org_switcher.ts
@@ -1,6 +1,7 @@
import coreModule from 'app/core/core_module';
import { contextSrv } from 'app/core/services/context_srv';
import config from 'app/core/config';
+import { BackendSrv } from '../services/backend_srv';
const template = `
@@ -47,18 +48,18 @@ export class OrgSwitchCtrl {
currentOrgId: any;
/** @ngInject */
- constructor(private backendSrv) {
+ constructor(private backendSrv: BackendSrv) {
this.currentOrgId = contextSrv.user.orgId;
this.getUserOrgs();
}
getUserOrgs() {
- this.backendSrv.get('/api/user/orgs').then(orgs => {
+ this.backendSrv.get('/api/user/orgs').then((orgs: any) => {
this.orgs = orgs;
});
}
- setUsingOrg(org) {
+ setUsingOrg(org: any) {
return this.backendSrv.post('/api/user/using/' + org.orgId).then(() => {
this.setWindowLocation(config.appSubUrl + (config.appSubUrl.endsWith('/') ? '' : '/') + '?orgId=' + org.orgId);
});
diff --git a/public/app/core/components/query_part/query_part.ts b/public/app/core/components/query_part/query_part.ts
index 10ac878f4b56..b2466977c229 100644
--- a/public/app/core/components/query_part/query_part.ts
+++ b/public/app/core/components/query_part/query_part.ts
@@ -40,7 +40,7 @@ export class QueryPart {
return this.def.renderer(this, innerExpr);
}
- hasMultipleParamsInString(strValue, index) {
+ hasMultipleParamsInString(strValue: string, index: number) {
if (strValue.indexOf(',') === -1) {
return false;
}
@@ -48,7 +48,7 @@ export class QueryPart {
return this.def.params[index + 1] && this.def.params[index + 1].optional;
}
- updateParam(strValue, index) {
+ updateParam(strValue: string, index: number) {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this.hasMultipleParamsInString(strValue, index)) {
@@ -81,7 +81,7 @@ export class QueryPart {
}
}
-export function functionRenderer(part, innerExpr) {
+export function functionRenderer(part: any, innerExpr: string) {
const str = part.def.type + '(';
const parameters = _.map(part.params, (value, index) => {
const paramType = part.def.params[index];
@@ -105,14 +105,14 @@ export function functionRenderer(part, innerExpr) {
return str + parameters.join(', ') + ')';
}
-export function suffixRenderer(part, innerExpr) {
+export function suffixRenderer(part: QueryPartDef, innerExpr: string) {
return innerExpr + ' ' + part.params[0];
}
-export function identityRenderer(part, innerExpr) {
+export function identityRenderer(part: QueryPartDef, innerExpr: string) {
return part.params[0];
}
-export function quotedIdentityRenderer(part, innerExpr) {
+export function quotedIdentityRenderer(part: QueryPartDef, innerExpr: string) {
return '"' + part.params[0] + '"';
}
diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts
index 4e6b82e151be..4e7221749aa1 100644
--- a/public/app/core/components/query_part/query_part_editor.ts
+++ b/public/app/core/components/query_part/query_part_editor.ts
@@ -14,7 +14,7 @@ const template = `
`;
/** @ngInject */
-export function queryPartEditorDirective($compile, templateSrv) {
+export function queryPartEditorDirective(templateSrv: any) {
const paramTemplate = '
';
return {
@@ -25,7 +25,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
handleEvent: '&',
debounce: '@',
},
- link: function postLink($scope, elem) {
+ link: function postLink($scope: any, elem: any) {
const part = $scope.part;
const partDef = part.def;
const $paramsContainer = elem.find('.query-part-parameters');
@@ -33,7 +33,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
$scope.partActions = [];
- function clickFuncParam(this: any, paramIndex) {
+ function clickFuncParam(this: any, paramIndex: number) {
/*jshint validthis:true */
const $link = $(this);
const $input = $link.next();
@@ -53,7 +53,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
}
}
- function inputBlur(this: any, paramIndex) {
+ function inputBlur(this: any, paramIndex: number) {
/*jshint validthis:true */
const $input = $(this);
const $link = $input.prev();
@@ -72,7 +72,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
$link.show();
}
- function inputKeyPress(this: any, paramIndex, e) {
+ function inputKeyPress(this: any, paramIndex: number, e: any) {
/*jshint validthis:true */
if (e.which === 13) {
inputBlur.call(this, paramIndex);
@@ -84,12 +84,12 @@ export function queryPartEditorDirective($compile, templateSrv) {
this.style.width = (3 + this.value.length) * 8 + 'px';
}
- function addTypeahead($input, param, paramIndex) {
+ function addTypeahead($input: JQuery, param: any, paramIndex: number) {
if (!param.options && !param.dynamicLookup) {
return;
}
- const typeaheadSource = (query, callback) => {
+ const typeaheadSource = (query: string, callback: any) => {
if (param.options) {
let options = param.options;
if (param.type === 'int') {
@@ -101,7 +101,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
}
$scope.$apply(() => {
- $scope.handleEvent({ $event: { name: 'get-param-options' } }).then(result => {
+ $scope.handleEvent({ $event: { name: 'get-param-options' } }).then((result: any) => {
const dynamicOptions = _.map(result, op => {
return _.escape(op.value);
});
@@ -116,7 +116,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
source: typeaheadSource,
minLength: 0,
items: 1000,
- updater: value => {
+ updater: (value: string) => {
value = _.unescape(value);
setTimeout(() => {
inputBlur.call($input[0], paramIndex);
@@ -138,12 +138,12 @@ export function queryPartEditorDirective($compile, templateSrv) {
}
$scope.showActionsMenu = () => {
- $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then(res => {
+ $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then((res: any) => {
$scope.partActions = res;
});
};
- $scope.triggerPartAction = action => {
+ $scope.triggerPartAction = (action: string) => {
$scope.handleEvent({ $event: { name: 'action', action: action } });
};
diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts
index 49931ecaac45..fe7e729bb190 100644
--- a/public/app/core/components/scroll/scroll.ts
+++ b/public/app/core/components/scroll/scroll.ts
@@ -1,4 +1,5 @@
import $ from 'jquery';
+// @ts-ignore
import baron from 'baron';
import coreModule from 'app/core/core_module';
@@ -14,7 +15,7 @@ const scrollerClass = 'baron__scroller';
export function geminiScrollbar() {
return {
restrict: 'A',
- link: (scope, elem, attrs) => {
+ link: (scope: any, elem: any, attrs: any) => {
let scrollRoot = elem.parent();
const scroller = elem;
diff --git a/public/app/core/components/search/SearchField.tsx b/public/app/core/components/search/SearchField.tsx
index 5b6dd03165f5..a72782e21c75 100644
--- a/public/app/core/components/search/SearchField.tsx
+++ b/public/app/core/components/search/SearchField.tsx
@@ -1,4 +1,5 @@
import React, { useContext } from 'react';
+// @ts-ignore
import tinycolor from 'tinycolor2';
import { SearchQuery } from './search';
import { css, cx } from 'emotion';
diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx
deleted file mode 100644
index 13333c168f91..000000000000
--- a/public/app/core/components/search/SearchResult.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react';
-import classNames from 'classnames';
-
-export class SearchResult extends React.Component
{
- constructor(props) {
- super(props);
-
- this.state = {
- search: '',
- };
- }
-
- render() {
- return this.state.search.sections.map(section => {
- return ;
- });
- }
-}
-
-export interface SectionProps {
- section: any;
-}
-
-export class SearchResultSection extends React.Component {
- constructor(props) {
- super(props);
- }
-
- renderItem(item) {
- return (
-
-
-
-
-
- {item.title}
-
-
- );
- }
-
- toggleSection = () => {
- this.props.section.toggle();
- };
-
- render() {
- const collapseClassNames = classNames({
- fa: true,
- 'fa-plus': !this.props.section.expanded,
- 'fa-minus': this.props.section.expanded,
- 'search-section__header__toggle': true,
- });
-
- return (
-
-
-
- {this.props.section.title}
-
-
- {this.props.section.expanded && (
-
{this.props.section.items.map(this.renderItem)}
- )}
-
- );
- }
-}
diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts
index 9cf8e9cd8c12..fbeb90922986 100644
--- a/public/app/core/components/search/search.ts
+++ b/public/app/core/components/search/search.ts
@@ -6,6 +6,7 @@ import { contextSrv } from 'app/core/services/context_srv';
import appEvents from 'app/core/app_events';
import { parse, SearchParserOptions, SearchParserResult } from 'search-query-parser';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
+
export interface SearchQuery {
query: string;
parsedQuery: SearchParserResult;
@@ -32,6 +33,15 @@ class SearchQueryParser {
}
}
+interface SelectedIndicies {
+ dashboardIndex?: number;
+ folderIndex?: number;
+}
+
+interface OpenSearchParams {
+ query?: string;
+}
+
export class SearchCtrl {
isOpen: boolean;
query: SearchQuery;
@@ -49,7 +59,7 @@ export class SearchCtrl {
queryParser: SearchQueryParser;
/** @ngInject */
- constructor($scope, private $location, private $timeout, private searchSrv: SearchSrv) {
+ constructor($scope: any, private $location: any, private $timeout: any, private searchSrv: SearchSrv) {
appEvents.on('show-dash-search', this.openSearch.bind(this), $scope);
appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
appEvents.on('search-query', debounce(this.search.bind(this), 500), $scope);
@@ -88,7 +98,7 @@ export class SearchCtrl {
appEvents.emit('search-query');
}
- openSearch(evt, payload) {
+ openSearch(payload: OpenSearchParams = {}) {
if (this.isOpen) {
this.closeSearch();
return;
@@ -99,19 +109,16 @@ export class SearchCtrl {
this.selectedIndex = -1;
this.results = [];
this.query = {
- query: evt ? `${evt.query} ` : '',
- parsedQuery: this.queryParser.parse(evt && evt.query),
+ query: payload.query ? `${payload.query} ` : '',
+ parsedQuery: this.queryParser.parse(payload.query),
tags: [],
starred: false,
};
+
this.currentSearchId = 0;
this.ignoreClose = true;
this.isLoading = true;
- if (payload && payload.starred) {
- this.query.starred = true;
- }
-
this.$timeout(() => {
this.ignoreClose = false;
this.giveSearchFocus = true;
@@ -166,7 +173,7 @@ export class SearchCtrl {
}, 100);
}
- moveSelection(direction) {
+ moveSelection(direction: number) {
if (this.results.length === 0) {
return;
}
@@ -252,14 +259,14 @@ export class SearchCtrl {
return query.query === '' && query.starred === false && query.tags.length === 0;
}
- filterByTag(tag) {
+ filterByTag(tag: string) {
if (_.indexOf(this.query.tags, tag) === -1) {
this.query.tags.push(tag);
this.search();
}
}
- removeTag(tag, evt) {
+ removeTag(tag: string, evt: any) {
this.query.tags = _.without(this.query.tags, tag);
this.search();
this.giveSearchFocus = true;
@@ -298,14 +305,11 @@ export class SearchCtrl {
this.moveSelection(0);
}
- private getFlattenedResultForNavigation(): Array<{
- folderIndex: number;
- dashboardIndex: number;
- }> {
+ private getFlattenedResultForNavigation(): SelectedIndicies[] {
let folderIndex = 0;
- return _.flatMap(this.results, s => {
- let result = [];
+ return _.flatMap(this.results, (s: any) => {
+ let result: SelectedIndicies[] = [];
result.push({
folderIndex: folderIndex,
diff --git a/public/app/core/components/search/search_results.ts b/public/app/core/components/search/search_results.ts
index 35ee1365e222..20cd49d3443e 100644
--- a/public/app/core/components/search/search_results.ts
+++ b/public/app/core/components/search/search_results.ts
@@ -10,15 +10,15 @@ export class SearchResultsCtrl {
editable: boolean;
/** @ngInject */
- constructor(private $location) {}
+ constructor(private $location: any) {}
- toggleFolderExpand(section) {
+ toggleFolderExpand(section: any) {
if (section.toggle) {
if (!section.expanded && this.onFolderExpanding) {
this.onFolderExpanding();
}
- section.toggle(section).then(f => {
+ section.toggle(section).then((f: any) => {
if (this.editable && f.expanded) {
if (f.items) {
_.each(f.items, i => {
@@ -34,7 +34,7 @@ export class SearchResultsCtrl {
}
}
- navigateToFolder(section, evt) {
+ navigateToFolder(section: any, evt: any) {
this.$location.path(section.url);
if (evt) {
@@ -43,7 +43,7 @@ export class SearchResultsCtrl {
}
}
- toggleSelection(item, evt) {
+ toggleSelection(item: any, evt: any) {
item.checked = !item.checked;
if (item.items) {
@@ -62,14 +62,14 @@ export class SearchResultsCtrl {
}
}
- onItemClick(item) {
+ onItemClick(item: any) {
//Check if one string can be found in the other
if (this.$location.path().indexOf(item.url) > -1 || item.url.indexOf(this.$location.path()) > -1) {
appEvents.emit('hide-dash-search');
}
}
- selectTag(tag, evt) {
+ selectTag(tag: any, evt: any) {
if (this.onTagSelected) {
this.onTagSelected({ $tag: tag });
}
diff --git a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx
index 5ba4503da139..d59973fe02d6 100644
--- a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx
+++ b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx
@@ -10,7 +10,9 @@ jest.mock('../../app_events', () => ({
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
- link: {},
+ link: {
+ text: 'Hello',
+ },
user: {
id: 1,
isGrafanaAdmin: false,
@@ -87,9 +89,9 @@ describe('Functions', () => {
const wrapper = setup();
const mockEvent = { preventDefault: jest.fn() };
it('should emit show modal event if url matches shortcut', () => {
- const child = { url: '/shortcuts' };
+ const child = { url: '/shortcuts', text: 'hello' };
const instance = wrapper.instance() as BottomNavLinks;
- instance.itemClicked(mockEvent, child);
+ instance.itemClicked(mockEvent as any, child);
expect(appEvents.emit).toHaveBeenCalledWith('show-modal', { templateHtml: '' });
});
diff --git a/public/app/core/components/sidemenu/BottomNavLinks.tsx b/public/app/core/components/sidemenu/BottomNavLinks.tsx
index e1665c9ebc5a..fee58bad15fc 100644
--- a/public/app/core/components/sidemenu/BottomNavLinks.tsx
+++ b/public/app/core/components/sidemenu/BottomNavLinks.tsx
@@ -1,14 +1,15 @@
import React, { PureComponent } from 'react';
import appEvents from '../../app_events';
import { User } from '../../services/context_srv';
+import { NavModelItem } from '@grafana/ui';
export interface Props {
- link: any;
+ link: NavModelItem;
user: User;
}
class BottomNavLinks extends PureComponent {
- itemClicked = (event, child) => {
+ itemClicked = (event: React.SyntheticEvent, child: NavModelItem) => {
if (child.url === '/shortcuts') {
event.preventDefault();
appEvents.emit('show-modal', {
@@ -57,7 +58,7 @@ class BottomNavLinks extends PureComponent {
link.children.map((child, index) => {
if (!child.hideFromMenu) {
return (
-
+
this.itemClicked(event, child)}>
{child.icon && }
{child.text}
diff --git a/public/app/core/components/sidemenu/BottomSection.tsx b/public/app/core/components/sidemenu/BottomSection.tsx
index 85754a910fa0..f0668eaf4dd7 100644
--- a/public/app/core/components/sidemenu/BottomSection.tsx
+++ b/public/app/core/components/sidemenu/BottomSection.tsx
@@ -4,10 +4,11 @@ import SignIn from './SignIn';
import BottomNavLinks from './BottomNavLinks';
import { contextSrv } from 'app/core/services/context_srv';
import config from '../../config';
+import { NavModelItem } from '@grafana/ui';
export default function BottomSection() {
- const navTree: any = _.cloneDeep(config.bootData.navTree);
- const bottomNav: any = _.filter(navTree, item => item.hideFromMenu);
+ const navTree: NavModelItem[] = _.cloneDeep(config.bootData.navTree);
+ const bottomNav: NavModelItem[] = _.filter(navTree, item => item.hideFromMenu);
const isSignedIn = contextSrv.isSignedIn;
const user = contextSrv.user;
diff --git a/public/app/core/components/sidemenu/SideMenuDropDown.tsx b/public/app/core/components/sidemenu/SideMenuDropDown.tsx
index db2172039c6f..b3959d55d4a7 100644
--- a/public/app/core/components/sidemenu/SideMenuDropDown.tsx
+++ b/public/app/core/components/sidemenu/SideMenuDropDown.tsx
@@ -1,8 +1,9 @@
import React, { FC } from 'react';
import DropDownChild from './DropDownChild';
+import { NavModelItem } from '@grafana/ui';
interface Props {
- link: any;
+ link: NavModelItem;
}
const SideMenuDropDown: FC = props => {
diff --git a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap
index ae8c9c753aa2..8b15bb0ae361 100644
--- a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap
+++ b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap
@@ -67,7 +67,9 @@ exports[`Render should render component 1`] = `
>
+ >
+ Hello
+
diff --git a/public/app/core/components/sql_part/sql_part.ts b/public/app/core/components/sql_part/sql_part.ts
index a9b76b1ed2dc..543d9eaf761b 100644
--- a/public/app/core/components/sql_part/sql_part.ts
+++ b/public/app/core/components/sql_part/sql_part.ts
@@ -61,7 +61,7 @@ export class SqlPart {
this.params = part.params;
}
- updateParam(strValue, index) {
+ updateParam(strValue: string, index: number) {
// handle optional parameters
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
diff --git a/public/app/core/components/sql_part/sql_part_editor.ts b/public/app/core/components/sql_part/sql_part_editor.ts
index b095f5fefed7..7799537b0fc2 100644
--- a/public/app/core/components/sql_part/sql_part_editor.ts
+++ b/public/app/core/components/sql_part/sql_part_editor.ts
@@ -14,7 +14,7 @@ const template = `
`;
/** @ngInject */
-export function sqlPartEditorDirective($compile, templateSrv) {
+export function sqlPartEditorDirective(templateSrv: any) {
const paramTemplate = '';
return {
@@ -25,16 +25,16 @@ export function sqlPartEditorDirective($compile, templateSrv) {
handleEvent: '&',
debounce: '@',
},
- link: function postLink($scope, elem) {
+ link: function postLink($scope: any, elem: any) {
const part = $scope.part;
const partDef = part.def;
const $paramsContainer = elem.find('.query-part-parameters');
const debounceLookup = $scope.debounce;
- let cancelBlur = null;
+ let cancelBlur: any = null;
$scope.partActions = [];
- function clickFuncParam(this: any, paramIndex) {
+ function clickFuncParam(this: any, paramIndex: number) {
/*jshint validthis:true */
const $link = $(this);
const $input = $link.next();
@@ -54,13 +54,13 @@ export function sqlPartEditorDirective($compile, templateSrv) {
}
}
- function inputBlur($input, paramIndex) {
+ function inputBlur($input: JQuery, paramIndex: number) {
cancelBlur = setTimeout(() => {
switchToLink($input, paramIndex);
}, 200);
}
- function switchToLink($input, paramIndex) {
+ function switchToLink($input: JQuery, paramIndex: number) {
/*jshint validthis:true */
const $link = $input.prev();
const newValue = $input.val();
@@ -78,7 +78,7 @@ export function sqlPartEditorDirective($compile, templateSrv) {
$link.show();
}
- function inputKeyPress(this: any, paramIndex, e) {
+ function inputKeyPress(this: any, paramIndex: number, e: any) {
/*jshint validthis:true */
if (e.which === 13) {
switchToLink($(this), paramIndex);
@@ -90,12 +90,12 @@ export function sqlPartEditorDirective($compile, templateSrv) {
this.style.width = (3 + this.value.length) * 8 + 'px';
}
- function addTypeahead($input, param, paramIndex) {
+ function addTypeahead($input: JQuery, param: any, paramIndex: number) {
if (!param.options && !param.dynamicLookup) {
return;
}
- const typeaheadSource = (query, callback) => {
+ const typeaheadSource = (query: string, callback: any) => {
if (param.options) {
let options = param.options;
if (param.type === 'int') {
@@ -107,7 +107,7 @@ export function sqlPartEditorDirective($compile, templateSrv) {
}
$scope.$apply(() => {
- $scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(result => {
+ $scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then((result: any) => {
const dynamicOptions = _.map(result, op => {
return _.escape(op.value);
});
@@ -128,7 +128,7 @@ export function sqlPartEditorDirective($compile, templateSrv) {
source: typeaheadSource,
minLength: 0,
items: 1000,
- updater: value => {
+ updater: (value: string) => {
value = _.unescape(value);
if (value === part.params[paramIndex]) {
clearTimeout(cancelBlur);
@@ -152,12 +152,12 @@ export function sqlPartEditorDirective($compile, templateSrv) {
}
$scope.showActionsMenu = () => {
- $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then(res => {
+ $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then((res: any) => {
$scope.partActions = res;
});
};
- $scope.triggerPartAction = action => {
+ $scope.triggerPartAction = (action: string) => {
$scope.handleEvent({ $event: { name: 'action', action: action } });
};
diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts
index 4a1d3ff804fe..d6a66a1a362f 100644
--- a/public/app/core/components/switch.ts
+++ b/public/app/core/components/switch.ts
@@ -38,7 +38,7 @@ export class SwitchCtrl {
label: string;
/** @ngInject */
- constructor($scope, private $timeout) {
+ constructor($scope: any, private $timeout: any) {
this.show = true;
this.id = $scope.$id;
}
diff --git a/public/app/core/core.ts b/public/app/core/core.ts
index c7f03781710c..ba4230cfe451 100644
--- a/public/app/core/core.ts
+++ b/public/app/core/core.ts
@@ -20,7 +20,6 @@ import { colors } from '@grafana/ui/';
import { searchDirective } from './components/search/search';
import { infoPopover } from './components/info_popover';
-import { navbarDirective } from './components/navbar/navbar';
import { arrayJoin } from './directives/array_join';
import { liveSrv } from './live/live_srv';
import { Emitter } from './utils/emitter';
@@ -56,7 +55,6 @@ export {
registerAngularDirectives,
arrayJoin,
coreModule,
- navbarDirective,
searchDirective,
liveSrv,
layoutSelector,
diff --git a/public/app/core/jquery_extended.ts b/public/app/core/jquery_extended.ts
index fa9b1aeb8234..11c67be9656c 100644
--- a/public/app/core/jquery_extended.ts
+++ b/public/app/core/jquery_extended.ts
@@ -9,7 +9,7 @@ $.fn.place_tt = (() => {
offset: 5,
};
- return function(this: any, x, y, opts) {
+ return function(this: any, x: number, y: number, opts: any) {
opts = $.extend(true, {}, defaults, opts);
return this.each(() => {
diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts
index 2ba60ce428e9..5fe95a182d07 100644
--- a/public/app/core/logs_model.ts
+++ b/public/app/core/logs_model.ts
@@ -13,6 +13,14 @@ import {
toLegacyResponseData,
FieldCache,
FieldType,
+ getLogLevelFromKey,
+ LogRowModel,
+ LogsModel,
+ LogsMetaItem,
+ LogsMetaKind,
+ LogsParser,
+ LogLabelStatsModel,
+ LogsDedupStrategy,
} from '@grafana/ui';
import { getThemeColor } from 'app/core/utils/colors';
import { hasAnsiCodes } from 'app/core/utils/text';
@@ -28,102 +36,19 @@ export const LogLevelColor = {
[LogLevel.unknown]: getThemeColor('#8e8e8e', '#dde4ed'),
};
-export interface LogSearchMatch {
- start: number;
- length: number;
- text: string;
-}
-
-export interface LogRowModel {
- duplicates?: number;
- entry: string;
- hasAnsi: boolean;
- labels: Labels;
- logLevel: LogLevel;
- raw: string;
- searchWords?: string[];
- timestamp: string; // ISO with nanosec precision
- timeFromNow: string;
- timeEpochMs: number;
- timeLocal: string;
- uniqueLabels?: Labels;
-}
-
-export interface LogLabelStatsModel {
- active?: boolean;
- count: number;
- proportion: number;
- value: string;
-}
-
-export enum LogsMetaKind {
- Number,
- String,
- LabelsMap,
-}
-
-export interface LogsMetaItem {
- label: string;
- value: string | number | Labels;
- kind: LogsMetaKind;
-}
-
-export interface LogsModel {
- hasUniqueLabels: boolean;
- meta?: LogsMetaItem[];
- rows: LogRowModel[];
- series?: TimeSeries[];
-}
-
export enum LogsDedupDescription {
none = 'No de-duplication',
exact = 'De-duplication of successive lines that are identical, ignoring ISO datetimes.',
numbers = 'De-duplication of successive lines that are identical when ignoring numbers, e.g., IP addresses, latencies.',
signature = 'De-duplication of successive lines that have identical punctuation and whitespace.',
}
-
-export enum LogsDedupStrategy {
- none = 'none',
- exact = 'exact',
- numbers = 'numbers',
- signature = 'signature',
-}
-
-export interface LogsParser {
- /**
- * Value-agnostic matcher for a field label.
- * Used to filter rows, and first capture group contains the value.
- */
- buildMatcher: (label: string) => RegExp;
-
- /**
- * Returns all parsable substrings from a line, used for highlighting
- */
- getFields: (line: string) => string[];
-
- /**
- * Gets the label name from a parsable substring of a line
- */
- getLabelFromField: (field: string) => string;
-
- /**
- * Gets the label value from a parsable substring of a line
- */
- getValueFromField: (field: string) => string;
- /**
- * Function to verify if this is a valid parser for the given line.
- * The parser accepts the line unless it returns undefined.
- */
- test: (line: string) => any;
-}
-
const LOGFMT_REGEXP = /(?:^|\s)(\w+)=("[^"]*"|\S+)/;
export const LogsParsers: { [name: string]: LogsParser } = {
JSON: {
buildMatcher: label => new RegExp(`(?:{|,)\\s*"${label}"\\s*:\\s*"?([\\d\\.]+|[^"]*)"?`),
getFields: line => {
- const fields = [];
+ const fields: string[] = [];
try {
const parsed = JSON.parse(line);
_.map(parsed, (value, key) => {
@@ -149,7 +74,7 @@ export const LogsParsers: { [name: string]: LogsParser } = {
logfmt: {
buildMatcher: label => new RegExp(`(?:^|\\s)${label}=("[^"]*"|\\S+)`),
getFields: line => {
- const fields = [];
+ const fields: string[] = [];
line.replace(new RegExp(LOGFMT_REGEXP, 'g'), substring => {
fields.push(substring.trim());
return '';
@@ -273,9 +198,9 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
// intervalMs = intervalMs * 10;
// Graph time series by log level
- const seriesByLevel = {};
+ const seriesByLevel: any = {};
const bucketSize = intervalMs * 10;
- const seriesList = [];
+ const seriesList: any[] = [];
for (const row of rows) {
let series = seriesByLevel[row.logLevel];
@@ -312,7 +237,7 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
}
return seriesList.map(series => {
- series.datapoints.sort((a, b) => {
+ series.datapoints.sort((a: number[], b: number[]) => {
return a[1] - b[1];
});
@@ -444,9 +369,19 @@ export function processLogSeriesRow(
const timeEpochMs = time.valueOf();
const timeFromNow = time.fromNow();
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
- const logLevel = getLogLevel(message);
+
+ let logLevel = LogLevel.unknown;
+ const logLevelField = fieldCache.getFieldByName('level');
+
+ if (logLevelField) {
+ logLevel = getLogLevelFromKey(row[logLevelField.index]);
+ } else if (series.labels && Object.keys(series.labels).indexOf('level') !== -1) {
+ logLevel = getLogLevelFromKey(series.labels['level']);
+ } else {
+ logLevel = getLogLevel(message);
+ }
const hasAnsi = hasAnsiCodes(message);
- const search = series.meta && series.meta.search ? series.meta.search : '';
+ const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : [];
return {
logLevel,
@@ -455,10 +390,10 @@ export function processLogSeriesRow(
timeLocal,
uniqueLabels,
hasAnsi,
+ searchWords,
entry: hasAnsi ? ansicolor.strip(message) : message,
raw: message,
labels: series.labels,
- searchWords: search ? [search] : [],
timestamp: ts,
};
}
diff --git a/public/app/core/middlewares/application.ts b/public/app/core/middlewares/application.ts
new file mode 100644
index 000000000000..3ca9768d626f
--- /dev/null
+++ b/public/app/core/middlewares/application.ts
@@ -0,0 +1,27 @@
+import { Store, Dispatch } from 'redux';
+import { StoreState } from 'app/types/store';
+import { ActionOf } from '../redux/actionCreatorFactory';
+import { toggleLogActions } from '../actions/application';
+
+export const toggleLogActionsMiddleware = (store: Store) => (next: Dispatch) => (action: ActionOf) => {
+ const isLogActionsAction = action.type === toggleLogActions.type;
+ if (isLogActionsAction) {
+ return next(action);
+ }
+
+ const logActionsTrue =
+ window && window.location && window.location.search && window.location.search.indexOf('logActions=true') !== -1;
+ const logActionsFalse =
+ window && window.location && window.location.search && window.location.search.indexOf('logActions=false') !== -1;
+ const logActions = store.getState().application.logActions;
+
+ if (logActionsTrue && !logActions) {
+ store.dispatch(toggleLogActions());
+ }
+
+ if (logActionsFalse && logActions) {
+ store.dispatch(toggleLogActions());
+ }
+
+ return next(action);
+};
diff --git a/public/app/core/nav_model_srv.ts b/public/app/core/nav_model_srv.ts
index bf31c2169c5d..598db4a53f2b 100644
--- a/public/app/core/nav_model_srv.ts
+++ b/public/app/core/nav_model_srv.ts
@@ -15,7 +15,7 @@ export class NavModelSrv {
return _.find(this.navItems, { id: 'cfg' });
}
- getNav(...args) {
+ getNav(...args: string[]) {
let children = this.navItems;
const nav = {
breadcrumbs: [],
diff --git a/public/app/core/partials.ts b/public/app/core/partials.ts
index 864a3dcfa8a3..32203a18f42e 100644
--- a/public/app/core/partials.ts
+++ b/public/app/core/partials.ts
@@ -1,4 +1,4 @@
let templates = (require as any).context('../', true, /\.html$/);
-templates.keys().forEach(key => {
+templates.keys().forEach((key: string) => {
templates(key);
});
diff --git a/public/app/core/profiler.ts b/public/app/core/profiler.ts
index 2284401803c6..02f58b92d221 100644
--- a/public/app/core/profiler.ts
+++ b/public/app/core/profiler.ts
@@ -4,7 +4,7 @@ export class Profiler {
$rootScope: any;
window: any;
- init(config, $rootScope) {
+ init(config: any, $rootScope: any) {
this.$rootScope = $rootScope;
this.window = window;
@@ -13,7 +13,7 @@ export class Profiler {
}
}
- renderingCompleted(panelId) {
+ renderingCompleted() {
// add render counter to root scope
// used by phantomjs render.js to know when panel has rendered
this.panelsRendered = (this.panelsRendered || 0) + 1;
diff --git a/public/app/core/reducers/application.ts b/public/app/core/reducers/application.ts
new file mode 100644
index 000000000000..458f49316191
--- /dev/null
+++ b/public/app/core/reducers/application.ts
@@ -0,0 +1,17 @@
+import { ApplicationState } from 'app/types/application';
+import { reducerFactory } from 'app/core/redux';
+import { toggleLogActions } from '../actions/application';
+
+export const initialState: ApplicationState = {
+ logActions: false,
+};
+
+export const applicationReducer = reducerFactory(initialState)
+ .addMapper({
+ filter: toggleLogActions,
+ mapper: (state): ApplicationState => ({
+ ...state,
+ logActions: !state.logActions,
+ }),
+ })
+ .create();
diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts
index 1c8670ed0d6c..cc0c950ec4a0 100644
--- a/public/app/core/reducers/index.ts
+++ b/public/app/core/reducers/index.ts
@@ -1,9 +1,11 @@
import { navIndexReducer as navIndex } from './navModel';
import { locationReducer as location } from './location';
import { appNotificationsReducer as appNotifications } from './appNotification';
+import { applicationReducer as application } from './application';
export default {
navIndex,
location,
appNotifications,
+ application,
};
diff --git a/public/app/core/services/AngularLoader.ts b/public/app/core/services/AngularLoader.ts
index 817e9c9f3985..cd5506a94584 100644
--- a/public/app/core/services/AngularLoader.ts
+++ b/public/app/core/services/AngularLoader.ts
@@ -2,13 +2,13 @@ import angular from 'angular';
import coreModule from 'app/core/core_module';
import _ from 'lodash';
-export interface AngularComponent {
- destroy(): void;
- digest(): void;
- getScope(): any;
-}
+import {
+ AngularComponent,
+ AngularLoader as AngularLoaderInterface,
+ setAngularLoader as setAngularLoaderInterface,
+} from '@grafana/runtime';
-export class AngularLoader {
+export class AngularLoader implements AngularLoaderInterface {
/** @ngInject */
constructor(private $compile: any, private $rootScope: any) {}
@@ -38,15 +38,8 @@ export class AngularLoader {
}
}
-coreModule.service('angularLoader', AngularLoader);
-
-let angularLoaderInstance: AngularLoader;
-
-export function setAngularLoader(pl: AngularLoader) {
- angularLoaderInstance = pl;
+export function setAngularLoader(v: AngularLoader) {
+ setAngularLoaderInterface(v);
}
-// away to access it from react
-export function getAngularLoader(): AngularLoader {
- return angularLoaderInstance;
-}
+coreModule.service('angularLoader', AngularLoader);
diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts
index 96e7a2cc258e..0f099c93d767 100644
--- a/public/app/core/services/backend_srv.ts
+++ b/public/app/core/services/backend_srv.ts
@@ -7,8 +7,9 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardSearchHit } from 'app/types/search';
import { ContextSrv } from './context_srv';
import { FolderInfo, DashboardDTO } from 'app/types';
+import { BackendSrv as BackendService, getBackendSrv as getBackendService, BackendSrvRequest } from '@grafana/runtime';
-export class BackendSrv {
+export class BackendSrv implements BackendService {
private inFlightRequests: { [key: string]: Array> } = {};
private HTTP_REQUEST_CANCELED = -1;
private noBackendCache: boolean;
@@ -29,7 +30,7 @@ export class BackendSrv {
return this.request({ method: 'DELETE', url });
}
- post(url: string, data: any) {
+ post(url: string, data?: any) {
return this.request({ method: 'POST', url, data });
}
@@ -83,7 +84,7 @@ export class BackendSrv {
throw data;
}
- request(options: any) {
+ request(options: BackendSrvRequest) {
options.retry = options.retry || 0;
const requestIsLocal = !options.url.match(/^http/);
const firstAttempt = options.retry === 0;
@@ -385,16 +386,7 @@ export class BackendSrv {
coreModule.service('backendSrv', BackendSrv);
-//
-// Code below is to expore the service to react components
-//
-
-let singletonInstance: BackendSrv;
-
-export function setBackendSrv(instance: BackendSrv) {
- singletonInstance = instance;
-}
-
+// Used for testing and things that really need BackendSrv
export function getBackendSrv(): BackendSrv {
- return singletonInstance;
+ return getBackendService() as BackendSrv;
}
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index 6fb50dd4ec29..0a4bd1c028ca 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -43,21 +43,11 @@ export class KeybindingSrv {
this.bind('g h', this.goToHome);
this.bind('g a', this.openAlerting);
this.bind('g p', this.goToProfile);
- this.bind('s s', this.openSearchStarred);
this.bind('s o', this.openSearch);
- this.bind('s t', this.openSearchTags);
this.bind('f', this.openSearch);
this.bindGlobal('esc', this.exit);
}
- openSearchStarred() {
- appEvents.emit('show-dash-search', { starred: true });
- }
-
- openSearchTags() {
- appEvents.emit('show-dash-search', { tagsMode: true });
- }
-
openSearch() {
appEvents.emit('show-dash-search');
}
diff --git a/public/app/core/services/segment_srv.ts b/public/app/core/services/segment_srv.ts
index e7653ec5cf73..d093a5570d6a 100644
--- a/public/app/core/services/segment_srv.ts
+++ b/public/app/core/services/segment_srv.ts
@@ -2,39 +2,51 @@ import _ from 'lodash';
import coreModule from '../core_module';
/** @ngInject */
-export function uiSegmentSrv(this: any, $sce, templateSrv) {
+export function uiSegmentSrv(this: any, $sce: any, templateSrv: any) {
const self = this;
- function MetricSegment(this: any, options) {
- if (options === '*' || options.value === '*') {
- this.value = '*';
- this.html = $sce.trustAsHtml('');
- this.type = options.type;
- this.expandable = true;
- return;
- }
+ class MetricSegment {
+ value: string;
+ html: any;
+ type: any;
+ expandable: boolean;
+ text: string;
+ cssClass: string;
+ fake: boolean;
+ custom: boolean;
+ selectMode: any;
+
+ constructor(options: any) {
+ if (options === '*' || options.value === '*') {
+ this.value = '*';
+ this.html = $sce.trustAsHtml('');
+ this.type = options.type;
+ this.expandable = true;
+ return;
+ }
- if (_.isString(options)) {
- this.value = options;
- this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
- return;
- }
+ if (_.isString(options)) {
+ this.value = options;
+ this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
+ return;
+ }
- // temp hack to work around legacy inconsistency in segment model
- this.text = options.value;
-
- this.cssClass = options.cssClass;
- this.custom = options.custom;
- this.type = options.type;
- this.fake = options.fake;
- this.value = options.value;
- this.selectMode = options.selectMode;
- this.type = options.type;
- this.expandable = options.expandable;
- this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
+ // temp hack to work around legacy inconsistency in segment model
+ this.text = options.value;
+
+ this.cssClass = options.cssClass;
+ this.custom = options.custom;
+ this.type = options.type;
+ this.fake = options.fake;
+ this.value = options.value;
+ this.selectMode = options.selectMode;
+ this.type = options.type;
+ this.expandable = options.expandable;
+ this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
+ }
}
- this.getSegmentForValue = function(value, fallbackText) {
+ this.getSegmentForValue = function(value: string, fallbackText: string) {
if (value) {
return this.newSegment(value);
} else {
@@ -46,38 +58,38 @@ export function uiSegmentSrv(this: any, $sce, templateSrv) {
return new MetricSegment({ value: 'select measurement', fake: true });
};
- this.newFake = (text, type, cssClass) => {
+ this.newFake = (text: string, type: string, cssClass: string) => {
return new MetricSegment({ value: text, fake: true, type: type, cssClass: cssClass });
};
- this.newSegment = options => {
+ this.newSegment = (options: any) => {
return new MetricSegment(options);
};
- this.newKey = key => {
+ this.newKey = (key: string) => {
return new MetricSegment({ value: key, type: 'key', cssClass: 'query-segment-key' });
};
- this.newKeyValue = value => {
+ this.newKeyValue = (value: string) => {
return new MetricSegment({ value: value, type: 'value', cssClass: 'query-segment-value' });
};
- this.newCondition = condition => {
+ this.newCondition = (condition: string) => {
return new MetricSegment({ value: condition, type: 'condition', cssClass: 'query-keyword' });
};
- this.newOperator = op => {
+ this.newOperator = (op: string) => {
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
};
- this.newOperators = ops => {
+ this.newOperators = (ops: string[]) => {
return _.map(ops, op => {
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
});
};
- this.transformToSegments = (addTemplateVars, variableTypeFilter) => {
- return results => {
+ this.transformToSegments = (addTemplateVars: boolean, variableTypeFilter: string) => {
+ return (results: any[]) => {
const segments = _.map(results, segment => {
return self.newSegment({ value: segment.text, expandable: segment.expandable });
});
diff --git a/public/app/core/specs/file_export.test.ts b/public/app/core/specs/file_export.test.ts
index 9e2ff0a7ce16..ab254a94f2b4 100644
--- a/public/app/core/specs/file_export.test.ts
+++ b/public/app/core/specs/file_export.test.ts
@@ -92,6 +92,7 @@ describe('file_export', () => {
[0x123, 'some string with \n in the middle', 10.01, false],
[0b1011, 'some string with ; in the middle', -12.34, true],
[123, 'some string with ;; in the middle', -12.34, true],
+ [1234, '=a bogus formula ', '-and another', '+another', '@ref'],
],
};
@@ -108,7 +109,8 @@ describe('file_export', () => {
'501;"some string with "" at the end""";0.01;false\r\n' +
'291;"some string with \n in the middle";10.01;false\r\n' +
'11;"some string with ; in the middle";-12.34;true\r\n' +
- '123;"some string with ;; in the middle";-12.34;true';
+ '123;"some string with ;; in the middle";-12.34;true\r\n' +
+ '1234;"\'=a bogus formula";"\'-and another";"\'+another";"\'@ref"';
expect(returnedText).toBe(expectedText);
});
diff --git a/public/app/core/specs/logs_model.test.ts b/public/app/core/specs/logs_model.test.ts
index c30a3ebfa391..c83f0ce6c1c0 100644
--- a/public/app/core/specs/logs_model.test.ts
+++ b/public/app/core/specs/logs_model.test.ts
@@ -1,15 +1,12 @@
+import { SeriesData, FieldType, LogsModel, LogsMetaKind, LogsDedupStrategy, LogLevel } from '@grafana/ui';
import {
+ dedupLogRows,
calculateFieldStats,
calculateLogsLabelStats,
- dedupLogRows,
getParser,
- LogsDedupStrategy,
- LogsModel,
LogsParsers,
seriesDataToLogsModel,
- LogsMetaKind,
} from '../logs_model';
-import { SeriesData, FieldType } from '@grafana/ui';
describe('dedupLogRows()', () => {
test('should return rows as is when dedup is set to none', () => {
@@ -463,8 +460,12 @@ describe('seriesDataToLogsModel', () => {
name: 'message',
type: FieldType.string,
},
+ {
+ name: 'level',
+ type: FieldType.string,
+ },
],
- rows: [['1970-01-01T00:00:01Z', 'WARN boooo']],
+ rows: [['1970-01-01T00:00:01Z', 'WARN boooo', 'dbug']],
},
];
const logsModel = seriesDataToLogsModel(series, 0);
@@ -473,7 +474,7 @@ describe('seriesDataToLogsModel', () => {
{
entry: 'WARN boooo',
labels: undefined,
- logLevel: 'warning',
+ logLevel: LogLevel.debug,
uniqueLabels: {},
},
]);
@@ -485,6 +486,7 @@ describe('seriesDataToLogsModel', () => {
labels: {
foo: 'bar',
baz: '1',
+ level: 'dbug',
},
fields: [
{
@@ -503,6 +505,7 @@ describe('seriesDataToLogsModel', () => {
labels: {
foo: 'bar',
baz: '2',
+ level: 'err',
},
fields: [
{
@@ -524,19 +527,19 @@ describe('seriesDataToLogsModel', () => {
{
entry: 'INFO 2',
labels: { foo: 'bar', baz: '2' },
- logLevel: 'info',
+ logLevel: LogLevel.error,
uniqueLabels: { baz: '2' },
},
{
entry: 'WARN boooo',
labels: { foo: 'bar', baz: '1' },
- logLevel: 'warning',
+ logLevel: LogLevel.debug,
uniqueLabels: { baz: '1' },
},
{
entry: 'INFO 1',
labels: { foo: 'bar', baz: '2' },
- logLevel: 'info',
+ logLevel: LogLevel.error,
uniqueLabels: { baz: '2' },
},
]);
diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts
index ffd1e54dcbc6..d193e7fcbe74 100644
--- a/public/app/core/table_model.ts
+++ b/public/app/core/table_model.ts
@@ -26,15 +26,19 @@ export default class TableModel implements TableData {
if (table) {
if (table.columns) {
- table.columns.forEach(col => this.addColumn(col));
+ for (const col of table.columns) {
+ this.addColumn(col);
+ }
}
if (table.rows) {
- table.rows.forEach(row => this.addRow(row));
+ for (const row of table.rows) {
+ this.addRow(row);
+ }
}
}
}
- sort(options) {
+ sort(options: { col: number; desc: boolean }) {
if (options.col === null || this.columns.length <= options.col) {
return;
}
@@ -54,21 +58,21 @@ export default class TableModel implements TableData {
this.columns[options.col].desc = options.desc;
}
- addColumn(col) {
+ addColumn(col: Column) {
if (!this.columnMap[col.text]) {
this.columns.push(col);
this.columnMap[col.text] = col;
}
}
- addRow(row) {
+ addRow(row: any[]) {
this.rows.push(row);
}
}
// Returns true if both rows have matching non-empty fields as well as matching
// indexes where one field is empty and the other is not
-function areRowsMatching(columns, row, otherRow) {
+function areRowsMatching(columns: Column[], row: any[], otherRow: any[]) {
let foundFieldToMatch = false;
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) {
@@ -96,7 +100,7 @@ export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]):
}
// Track column indexes of union: name -> index
- const columnNames = {};
+ const columnNames: { [key: string]: any } = {};
// Union of all non-value columns
const columnsUnion = tables.slice().reduce((acc, series) => {
@@ -119,7 +123,7 @@ export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]):
const flattenedRows = tables.reduce((acc, series, seriesIndex) => {
const mapper = columnIndexMapper[seriesIndex];
series.rows.forEach(row => {
- const alteredRow = [];
+ const alteredRow: any[] = [];
// Shifting entries according to index mapper
mapper.forEach((to, from) => {
alteredRow[to] = row[from];
@@ -130,7 +134,8 @@ export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]):
}, []);
// Merge rows that have same values for columns
- const mergedRows = {};
+ const mergedRows: { [key: string]: any } = {};
+
const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => {
if (!mergedRows[rowIndex]) {
// Look from current row onwards
diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts
index 10c91c148d79..d7a57b77afc9 100644
--- a/public/app/core/time_series2.ts
+++ b/public/app/core/time_series2.ts
@@ -1,8 +1,8 @@
import { getFlotTickDecimals } from 'app/core/utils/ticks';
import _ from 'lodash';
-import { getValueFormat, stringToJsRegex } from '@grafana/ui';
+import { getValueFormat, stringToJsRegex, ValueFormatter, DecimalCount } from '@grafana/ui';
-function matchSeriesOverride(aliasOrRegex, seriesAlias) {
+function matchSeriesOverride(aliasOrRegex: string, seriesAlias: string) {
if (!aliasOrRegex) {
return false;
}
@@ -15,7 +15,7 @@ function matchSeriesOverride(aliasOrRegex, seriesAlias) {
return aliasOrRegex === seriesAlias;
}
-function translateFillOption(fill) {
+function translateFillOption(fill: number) {
return fill === 0 ? 0.001 : fill / 10;
}
@@ -25,7 +25,7 @@ function translateFillOption(fill) {
* @param panel
* @param height
*/
-export function updateLegendValues(data: TimeSeries[], panel, height) {
+export function updateLegendValues(data: TimeSeries[], panel: any, height: number) {
for (let i = 0; i < data.length; i++) {
const series = data[i];
const yaxes = panel.yaxes;
@@ -97,7 +97,7 @@ export default class TimeSeries {
flotpairs: any;
unit: any;
- constructor(opts) {
+ constructor(opts: any) {
this.datapoints = opts.datapoints;
this.label = opts.alias;
this.id = opts.alias;
@@ -112,7 +112,7 @@ export default class TimeSeries {
this.hasMsResolution = this.isMsResolutionNeeded();
}
- applySeriesOverrides(overrides) {
+ applySeriesOverrides(overrides: any[]) {
this.lines = {};
this.dashes = {
dashLength: [],
@@ -192,7 +192,7 @@ export default class TimeSeries {
}
}
- getFlotPairs(fillStyle) {
+ getFlotPairs(fillStyle: string) {
const result = [];
this.stats.total = 0;
@@ -314,13 +314,13 @@ export default class TimeSeries {
return result;
}
- updateLegendValues(formater, decimals, scaledDecimals) {
+ updateLegendValues(formater: ValueFormatter, decimals: DecimalCount, scaledDecimals: DecimalCount) {
this.valueFormater = formater;
this.decimals = decimals;
this.scaledDecimals = scaledDecimals;
}
- formatValue(value) {
+ formatValue(value: number) {
if (!_.isFinite(value)) {
value = null; // Prevent NaN formatting
}
@@ -329,7 +329,7 @@ export default class TimeSeries {
isMsResolutionNeeded() {
for (let i = 0; i < this.datapoints.length; i++) {
- if (this.datapoints[i][1] !== null) {
+ if (this.datapoints[i][1] !== null && this.datapoints[i][1] !== undefined) {
const timestamp = this.datapoints[i][1].toString();
if (timestamp.length === 13 && timestamp % 1000 !== 0) {
return true;
@@ -339,7 +339,7 @@ export default class TimeSeries {
return false;
}
- hideFromLegend(options) {
+ hideFromLegend(options: any) {
if (options.hideEmpty && this.allIsNull) {
return true;
}
diff --git a/public/app/core/utils/connectWithReduxStore.tsx b/public/app/core/utils/connectWithReduxStore.tsx
index be91958f8cd1..19493681c0d7 100644
--- a/public/app/core/utils/connectWithReduxStore.tsx
+++ b/public/app/core/utils/connectWithReduxStore.tsx
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { store } from '../../store/store';
export function connectWithStore(WrappedComponent, ...args) {
- const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
+ const ConnectedWrappedComponent = (connect as any)(...args)(WrappedComponent);
return props => {
return ;
diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts
index 3a0752d2a5ed..344de3803202 100644
--- a/public/app/core/utils/explore.test.ts
+++ b/public/app/core/utils/explore.test.ts
@@ -12,8 +12,7 @@ import {
} from './explore';
import { ExploreUrlState } from 'app/types/explore';
import store from 'app/core/store';
-import { LogsDedupStrategy } from 'app/core/logs_model';
-import { DataQueryError } from '@grafana/ui';
+import { DataQueryError, LogsDedupStrategy } from '@grafana/ui';
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
datasource: null,
@@ -182,11 +181,11 @@ describe('updateHistory()', () => {
describe('hasNonEmptyQuery', () => {
test('should return true if one query is non-empty', () => {
- expect(hasNonEmptyQuery([{ refId: '1', key: '2', expr: 'foo' }])).toBeTruthy();
+ expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'explore', expr: 'foo' }])).toBeTruthy();
});
test('should return false if query is empty', () => {
- expect(hasNonEmptyQuery([{ refId: '1', key: '2' }])).toBeFalsy();
+ expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel' }])).toBeFalsy();
});
test('should return false if no queries exist', () => {
diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts
index 7a19fd5a822d..811950a9251f 100644
--- a/public/app/core/utils/explore.ts
+++ b/public/app/core/utils/explore.ts
@@ -1,40 +1,35 @@
// Libraries
import _ from 'lodash';
+import { from } from 'rxjs';
+import { toUtc } from '@grafana/ui/src/utils/moment_wrapper';
+import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
// Services & Utils
import * as dateMath from '@grafana/ui/src/utils/datemath';
import { renderUrl } from 'app/core/utils/url';
import kbn from 'app/core/utils/kbn';
import store from 'app/core/store';
-import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { getNextRefIdChar } from './query';
// Types
import {
- colors,
TimeRange,
RawTimeRange,
TimeZone,
IntervalValues,
DataQuery,
DataSourceApi,
- toSeriesData,
- guessFieldTypes,
TimeFragment,
DataQueryError,
+ LogRowModel,
+ LogsModel,
+ LogsDedupStrategy,
+ DataSourceJsonData,
+ DataQueryRequest,
+ DataStreamObserver,
} from '@grafana/ui';
-import TimeSeries from 'app/core/time_series2';
-import {
- ExploreUrlState,
- HistoryItem,
- QueryTransaction,
- ResultType,
- QueryIntervals,
- QueryOptions,
- ResultGetter,
-} from 'app/types/explore';
-import { LogsDedupStrategy, seriesDataToLogsModel } from 'app/core/logs_model';
-import { toUtc } from '@grafana/ui/src/utils/moment_wrapper';
+import { ExploreUrlState, HistoryItem, QueryTransaction, QueryIntervals, QueryOptions } from 'app/types/explore';
+import { config } from '../config';
export const DEFAULT_RANGE = {
from: 'now-6h',
@@ -112,7 +107,6 @@ export async function getExploreUrl(
export function buildQueryTransaction(
queries: DataQuery[],
- resultType: ResultType,
queryOptions: QueryOptions,
range: TimeRange,
queryIntervals: QueryIntervals,
@@ -133,7 +127,7 @@ export function buildQueryTransaction(
// Using `format` here because it relates to the view panel that the request is for.
// However, some datasources don't use `panelId + query.refId`, but only `panelId`.
// Therefore panel id has to be unique.
- const panelId = `${queryOptions.format}-${key}`;
+ const panelId = `${key}`;
const options = {
interval,
@@ -152,7 +146,6 @@ export function buildQueryTransaction(
return {
queries,
options,
- resultType,
scanning,
id: generateKey(), // reusing for unique ID
done: false,
@@ -310,42 +303,22 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
}
/**
- * A target is non-empty when it has keys (with non-empty values) other than refId and key.
+ * A target is non-empty when it has keys (with non-empty values) other than refId, key and context.
*/
+const validKeys = ['refId', 'key', 'context'];
export function hasNonEmptyQuery(queries: TQuery[]): boolean {
return (
queries &&
- queries.some(
- query =>
- Object.keys(query)
- .map(k => query[k])
- .filter(v => v).length > 2
- )
+ queries.some(query => {
+ const keys = Object.keys(query)
+ .filter(key => validKeys.indexOf(key) === -1)
+ .map(k => query[k])
+ .filter(v => v);
+ return keys.length > 0;
+ })
);
}
-export function calculateResultsFromQueryTransactions(result: any, resultType: ResultType, graphInterval: number) {
- const flattenedResult: any[] = _.flatten(result);
- const graphResult = resultType === 'Graph' && result ? result : null;
- const tableResult =
- resultType === 'Table' && result
- ? mergeTablesIntoModel(
- new TableModel(),
- ...flattenedResult.filter((r: any) => r.columns && r.rows).map((r: any) => r as TableModel)
- )
- : mergeTablesIntoModel(new TableModel());
- const logsResult =
- resultType === 'Logs' && result
- ? seriesDataToLogsModel(flattenedResult.map(r => guessFieldTypes(toSeriesData(r))), graphInterval)
- : null;
-
- return {
- graphResult,
- tableResult,
- logsResult,
- };
-}
-
export function getIntervals(range: TimeRange, lowLimit: string, resolution: number): IntervalValues {
if (!resolution) {
return { interval: '1s', intervalMs: 1000 };
@@ -354,37 +327,6 @@ export function getIntervals(range: TimeRange, lowLimit: string, resolution: num
return kbn.calculateInterval(range, resolution, lowLimit);
}
-export const makeTimeSeriesList: ResultGetter = (dataList, transaction, allTransactions) => {
- // Prevent multiple Graph transactions to have the same colors
- let colorIndexOffset = 0;
- for (const other of allTransactions) {
- // Only need to consider transactions that came before the current one
- if (other === transaction) {
- break;
- }
- // Count timeseries of previous query results
- if (other.resultType === 'Graph' && other.done) {
- colorIndexOffset += other.result.length;
- }
- }
-
- return dataList.map((seriesData, index: number) => {
- const datapoints = seriesData.datapoints || [];
- const alias = seriesData.target;
- const colorIndex = (colorIndexOffset + index) % colors.length;
- const color = colors[colorIndex];
-
- const series = new TimeSeries({
- datapoints,
- alias,
- color,
- unit: seriesData.unit,
- });
-
- return series;
- });
-};
-
/**
* Update the query history. Side-effect: store history in local storage
*/
@@ -529,3 +471,53 @@ export const getRefIds = (value: any): string[] => {
return _.uniq(_.flatten(refIds));
};
+
+const sortInAscendingOrder = (a: LogRowModel, b: LogRowModel) => {
+ if (a.timeEpochMs < b.timeEpochMs) {
+ return -1;
+ }
+
+ if (a.timeEpochMs > b.timeEpochMs) {
+ return 1;
+ }
+
+ return 0;
+};
+
+const sortInDescendingOrder = (a: LogRowModel, b: LogRowModel) => {
+ if (a.timeEpochMs > b.timeEpochMs) {
+ return -1;
+ }
+
+ if (a.timeEpochMs < b.timeEpochMs) {
+ return 1;
+ }
+
+ return 0;
+};
+
+export const sortLogsResult = (logsResult: LogsModel, refreshInterval: string) => {
+ const rows = logsResult ? logsResult.rows : [];
+ const live = isLive(refreshInterval);
+ live ? rows.sort(sortInAscendingOrder) : rows.sort(sortInDescendingOrder);
+ const result: LogsModel = logsResult ? { ...logsResult, rows } : { hasUniqueLabels: false, rows };
+
+ return result;
+};
+
+export const convertToWebSocketUrl = (url: string) => {
+ const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
+ let backend = `${protocol}${window.location.host}${config.appSubUrl}`;
+ if (backend.endsWith('/')) {
+ backend = backend.slice(0, backend.length - 1);
+ }
+ return `${backend}${url}`;
+};
+
+export const getQueryResponse = (
+ datasourceInstance: DataSourceApi,
+ options: DataQueryRequest,
+ observer?: DataStreamObserver
+) => {
+ return from(datasourceInstance.query(options, observer));
+};
diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts
index 6d341b5582e2..ae8d0ad06dea 100644
--- a/public/app/core/utils/file_export.ts
+++ b/public/app/core/utils/file_export.ts
@@ -17,7 +17,11 @@ function csvEscaped(text) {
return text;
}
- return text.split(QUOTE).join(QUOTE + QUOTE);
+ return text
+ .split(QUOTE)
+ .join(QUOTE + QUOTE)
+ .replace(/^([-+=@])/, "'$1")
+ .replace(/\s+$/, '');
}
const domParser = new DOMParser();
diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts
index d747fa37f579..1a1cf6f56ba1 100644
--- a/public/app/core/utils/kbn.ts
+++ b/public/app/core/utils/kbn.ts
@@ -1,4 +1,4 @@
-import _ from 'lodash';
+import { has } from 'lodash';
import { getValueFormat, getValueFormatterIndex, getValueFormats, stringToJsRegex } from '@grafana/ui';
import deprecationWarning from '@grafana/ui/src/utils/deprecationWarning';
@@ -133,7 +133,7 @@ kbn.secondsToHms = seconds => {
};
kbn.secondsToHhmmss = seconds => {
- const strings = [];
+ const strings: string[] = [];
const numhours = Math.floor(seconds / 3600);
const numminutes = Math.floor((seconds % 3600) / 60);
const numseconds = Math.floor((seconds % 3600) % 60);
@@ -193,7 +193,7 @@ kbn.calculateInterval = (range, resolution, lowLimitInterval) => {
kbn.describe_interval = str => {
const matches = str.match(kbn.interval_regex);
- if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
+ if (!matches || !has(kbn.intervals_in_seconds, matches[2])) {
throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"');
} else {
return {
diff --git a/public/app/core/utils/text.ts b/public/app/core/utils/text.ts
index faf801b45be8..6db844e736ef 100644
--- a/public/app/core/utils/text.ts
+++ b/public/app/core/utils/text.ts
@@ -6,7 +6,7 @@ import xss from 'xss';
* See https://github.com/bvaughn/react-highlight-words#props
*/
export function findHighlightChunksInText({ searchWords, textToHighlight }) {
- return findMatchesInText(textToHighlight, searchWords.join(' '));
+ return searchWords.reduce((acc, term) => [...acc, ...findMatchesInText(textToHighlight, term)], []);
}
const cleanNeedle = (needle: string): string => {
diff --git a/public/app/core/utils/url.ts b/public/app/core/utils/url.ts
index b0994eb01034..878904bc7a64 100644
--- a/public/app/core/utils/url.ts
+++ b/public/app/core/utils/url.ts
@@ -2,7 +2,7 @@
* @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT
*/
-import { UrlQueryMap } from 'app/types';
+import { UrlQueryMap } from '@grafana/runtime';
export function renderUrl(path: string, query: UrlQueryMap | undefined): string {
if (query && Object.keys(query).length > 0) {
@@ -11,7 +11,7 @@ export function renderUrl(path: string, query: UrlQueryMap | undefined): string
return path;
}
-export function encodeURIComponentAsAngularJS(val, pctEncodeSpaces) {
+export function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) {
return encodeURIComponent(val)
.replace(/%40/gi, '@')
.replace(/%3A/gi, ':')
@@ -21,15 +21,15 @@ export function encodeURIComponentAsAngularJS(val, pctEncodeSpaces) {
.replace(/%20/g, pctEncodeSpaces ? '%20' : '+');
}
-export function toUrlParams(a) {
+export function toUrlParams(a: any) {
const s = [];
const rbracket = /\[\]$/;
- const isArray = obj => {
+ const isArray = (obj: any) => {
return Object.prototype.toString.call(obj) === '[object Array]';
};
- const add = (k, v) => {
+ const add = (k: string, v: any) => {
v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v;
if (typeof v !== 'boolean') {
s[s.length] = encodeURIComponentAsAngularJS(k, true) + '=' + encodeURIComponentAsAngularJS(v, true);
@@ -38,7 +38,7 @@ export function toUrlParams(a) {
}
};
- const buildParams = (prefix, obj) => {
+ const buildParams = (prefix: string, obj: any) => {
let i, len, key;
if (prefix) {
diff --git a/public/app/features/admin/AdminEditOrgCtrl.ts b/public/app/features/admin/AdminEditOrgCtrl.ts
index 4ce0e2366f63..a10067b3dbab 100644
--- a/public/app/features/admin/AdminEditOrgCtrl.ts
+++ b/public/app/features/admin/AdminEditOrgCtrl.ts
@@ -1,8 +1,11 @@
+import { BackendSrv } from 'app/core/services/backend_srv';
+import { NavModelSrv } from 'app/core/core';
+
export default class AdminEditOrgCtrl {
/** @ngInject */
- constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
+ constructor($scope: any, $routeParams: any, backendSrv: BackendSrv, $location: any, navModelSrv: NavModelSrv) {
$scope.init = () => {
- $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
+ $scope.navModel = navModelSrv.getNav('admin', 'global-orgs');
if ($routeParams.id) {
$scope.getOrg($routeParams.id);
@@ -10,14 +13,14 @@ export default class AdminEditOrgCtrl {
}
};
- $scope.getOrg = id => {
- backendSrv.get('/api/orgs/' + id).then(org => {
+ $scope.getOrg = (id: number) => {
+ backendSrv.get('/api/orgs/' + id).then((org: any) => {
$scope.org = org;
});
};
- $scope.getOrgUsers = id => {
- backendSrv.get('/api/orgs/' + id + '/users').then(orgUsers => {
+ $scope.getOrgUsers = (id: number) => {
+ backendSrv.get('/api/orgs/' + id + '/users').then((orgUsers: any) => {
$scope.orgUsers = orgUsers;
});
};
@@ -32,11 +35,11 @@ export default class AdminEditOrgCtrl {
});
};
- $scope.updateOrgUser = orgUser => {
+ $scope.updateOrgUser = (orgUser: any) => {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId, orgUser);
};
- $scope.removeOrgUser = orgUser => {
+ $scope.removeOrgUser = (orgUser: any) => {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId).then(() => {
$scope.getOrgUsers($scope.org.id);
});
diff --git a/public/app/features/admin/AdminEditUserCtrl.ts b/public/app/features/admin/AdminEditUserCtrl.ts
index 4b7dc9d21e59..f64f5005dfa2 100644
--- a/public/app/features/admin/AdminEditUserCtrl.ts
+++ b/public/app/features/admin/AdminEditUserCtrl.ts
@@ -1,12 +1,15 @@
import _ from 'lodash';
+import { BackendSrv } from 'app/core/services/backend_srv';
+import { NavModelSrv } from 'app/core/core';
+import { User } from 'app/core/services/context_srv';
export default class AdminEditUserCtrl {
/** @ngInject */
- constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
+ constructor($scope: any, $routeParams: any, backendSrv: BackendSrv, $location: any, navModelSrv: NavModelSrv) {
$scope.user = {};
$scope.newOrg = { name: '', role: 'Editor' };
$scope.permissions = {};
- $scope.navModel = navModelSrv.getNav('admin', 'global-users', 0);
+ $scope.navModel = navModelSrv.getNav('admin', 'global-users');
$scope.init = () => {
if ($routeParams.id) {
@@ -15,8 +18,8 @@ export default class AdminEditUserCtrl {
}
};
- $scope.getUser = id => {
- backendSrv.get('/api/users/' + id).then(user => {
+ $scope.getUser = (id: number) => {
+ backendSrv.get('/api/users/' + id).then((user: User) => {
$scope.user = user;
$scope.user_id = id;
$scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin;
@@ -52,8 +55,8 @@ export default class AdminEditUserCtrl {
});
};
- $scope.getUserOrgs = id => {
- backendSrv.get('/api/users/' + id + '/orgs').then(orgs => {
+ $scope.getUserOrgs = (id: number) => {
+ backendSrv.get('/api/users/' + id + '/orgs').then((orgs: any) => {
$scope.orgs = orgs;
});
};
@@ -68,11 +71,11 @@ export default class AdminEditUserCtrl {
});
};
- $scope.updateOrgUser = orgUser => {
+ $scope.updateOrgUser = (orgUser: { orgId: string }) => {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(() => {});
};
- $scope.removeOrgUser = orgUser => {
+ $scope.removeOrgUser = (orgUser: { orgId: string }) => {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(() => {
$scope.getUser($scope.user_id);
$scope.getUserOrgs($scope.user_id);
@@ -81,13 +84,13 @@ export default class AdminEditUserCtrl {
$scope.orgsSearchCache = [];
- $scope.searchOrgs = (queryStr, callback) => {
+ $scope.searchOrgs = (queryStr: any, callback: any) => {
if ($scope.orgsSearchCache.length > 0) {
callback(_.map($scope.orgsSearchCache, 'name'));
return;
}
- backendSrv.get('/api/orgs', { query: '' }).then(result => {
+ backendSrv.get('/api/orgs', { query: '' }).then((result: any) => {
$scope.orgsSearchCache = result;
callback(_.map(result, 'name'));
});
@@ -101,6 +104,7 @@ export default class AdminEditUserCtrl {
const orgInfo: any = _.find($scope.orgsSearchCache, {
name: $scope.newOrg.name,
});
+
if (!orgInfo) {
return;
}
diff --git a/public/app/features/admin/AdminListOrgsCtrl.ts b/public/app/features/admin/AdminListOrgsCtrl.ts
index d6ee4b33dbbe..439811edbf28 100644
--- a/public/app/features/admin/AdminListOrgsCtrl.ts
+++ b/public/app/features/admin/AdminListOrgsCtrl.ts
@@ -1,18 +1,21 @@
+import { BackendSrv } from 'app/core/services/backend_srv';
+import { NavModelSrv } from 'app/core/core';
+
export default class AdminListOrgsCtrl {
/** @ngInject */
- constructor($scope, backendSrv, navModelSrv) {
+ constructor($scope: any, backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
$scope.init = () => {
- $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
+ $scope.navModel = navModelSrv.getNav('admin', 'global-orgs');
$scope.getOrgs();
};
$scope.getOrgs = () => {
- backendSrv.get('/api/orgs').then(orgs => {
+ backendSrv.get('/api/orgs').then((orgs: any) => {
$scope.orgs = orgs;
});
};
- $scope.deleteOrg = org => {
+ $scope.deleteOrg = (org: any) => {
$scope.appEvent('confirm-modal', {
title: 'Delete',
text: 'Do you want to delete organization ' + org.name + '?',
diff --git a/public/app/features/admin/AdminListUsersCtrl.ts b/public/app/features/admin/AdminListUsersCtrl.ts
index 5b5321d13cea..33a43adf0598 100644
--- a/public/app/features/admin/AdminListUsersCtrl.ts
+++ b/public/app/features/admin/AdminListUsersCtrl.ts
@@ -1,6 +1,9 @@
+import { BackendSrv } from 'app/core/services/backend_srv';
+import { NavModelSrv } from 'app/core/core';
+
export default class AdminListUsersCtrl {
users: any;
- pages = [];
+ pages: any[] = [];
perPage = 50;
page = 1;
totalPages: number;
@@ -9,8 +12,8 @@ export default class AdminListUsersCtrl {
navModel: any;
/** @ngInject */
- constructor(private $scope, private backendSrv, navModelSrv) {
- this.navModel = navModelSrv.getNav('admin', 'global-users', 0);
+ constructor(private $scope: any, private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
+ this.navModel = navModelSrv.getNav('admin', 'global-users');
this.query = '';
this.getUsers();
}
@@ -18,7 +21,7 @@ export default class AdminListUsersCtrl {
getUsers() {
this.backendSrv
.get(`/api/users/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
- .then(result => {
+ .then((result: any) => {
this.users = result.users;
this.page = result.page;
this.perPage = result.perPage;
@@ -32,12 +35,12 @@ export default class AdminListUsersCtrl {
});
}
- navigateToPage(page) {
+ navigateToPage(page: any) {
this.page = page.page;
this.getUsers();
}
- deleteUser(user) {
+ deleteUser(user: any) {
this.$scope.appEvent('confirm-modal', {
title: 'Delete',
text: 'Do you want to delete ' + user.login + '?',
diff --git a/public/app/features/admin/ServerStats.test.tsx b/public/app/features/admin/ServerStats.test.tsx
index cbcc580f6120..c33a5f4fbb21 100644
--- a/public/app/features/admin/ServerStats.test.tsx
+++ b/public/app/features/admin/ServerStats.test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+// @ts-ignore
import renderer from 'react-test-renderer';
import { ServerStats } from './ServerStats';
import { createNavModel } from 'test/mocks/common';
diff --git a/public/app/features/admin/StyleGuideCtrl.ts b/public/app/features/admin/StyleGuideCtrl.ts
index 6548aa091989..44ef3c487760 100644
--- a/public/app/features/admin/StyleGuideCtrl.ts
+++ b/public/app/features/admin/StyleGuideCtrl.ts
@@ -1,4 +1,6 @@
import config from 'app/core/config';
+import { BackendSrv } from 'app/core/services/backend_srv';
+import { NavModelSrv } from 'app/core/core';
export default class StyleGuideCtrl {
theme: string;
@@ -8,8 +10,8 @@ export default class StyleGuideCtrl {
navModel: any;
/** @ngInject */
- constructor(private $routeParams, private backendSrv, navModelSrv) {
- this.navModel = navModelSrv.getNav('admin', 'styleguide', 0);
+ constructor(private $routeParams: any, private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
+ this.navModel = navModelSrv.getNav('admin', 'styleguide');
this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
}
diff --git a/public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap
index 23d4d714559d..bb1ba9c9388f 100644
--- a/public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap
+++ b/public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap
@@ -212,7 +212,7 @@ exports[`ServerStats Should render table with stats 1`] = `