Procházet zdrojové kódy

Merge branch 'master' of github.com:grafana/grafana

Torkel Ödegaard před 9 roky
rodič
revize
2da2d5df56
38 změnil soubory, kde provedl 344 přidání a 158 odebrání
  1. 2 0
      docs/sources/datasources/cloudwatch.md
  2. 6 1
      docs/sources/plugins/installation.md
  3. 1 1
      pkg/api/alerting.go
  4. 11 0
      pkg/cmd/grafana-cli/services/services.go
  5. 2 0
      pkg/metrics/metrics.go
  6. 7 6
      pkg/models/alert.go
  7. 13 6
      pkg/services/alerting/conditions/query.go
  8. 20 20
      pkg/services/alerting/conditions/query_test.go
  9. 10 2
      pkg/services/alerting/eval_handler.go
  10. 6 5
      pkg/services/alerting/eval_handler_test.go
  11. 4 1
      pkg/services/alerting/extractor.go
  12. 7 1
      pkg/services/alerting/interfaces.go
  13. 1 1
      pkg/services/alerting/notifier.go
  14. 12 2
      pkg/services/alerting/notifiers/slack.go
  15. 8 1
      pkg/services/alerting/result_handler.go
  16. 3 1
      pkg/services/alerting/rule_test.go
  17. 1 1
      pkg/services/notifications/mailer.go
  18. 2 2
      pkg/services/sqlstore/alert.go
  19. 2 2
      pkg/services/sqlstore/alert_test.go
  20. 18 12
      pkg/tsdb/graphite/graphite.go
  21. 60 0
      pkg/tsdb/graphite/graphite_test.go
  22. 29 0
      pkg/tsdb/http.go
  23. 1 10
      pkg/tsdb/influxdb/influxdb.go
  24. 49 58
      pkg/tsdb/opentsdb/opentsdb.go
  25. 6 13
      pkg/tsdb/prometheus/prometheus.go
  26. 14 0
      pkg/tsdb/prometheus/prometheus_test.go
  27. 1 1
      public/app/features/admin/partials/settings.html
  28. 7 0
      public/app/features/alerting/alert_def.ts
  29. 12 1
      public/app/features/alerting/partials/notification_edit.html
  30. 1 0
      public/app/features/templating/constant_variable.ts
  31. 5 0
      public/app/features/templating/specs/template_srv_specs.ts
  32. 14 0
      public/app/features/templating/templateSrv.js
  33. 1 1
      public/app/plugins/datasource/opentsdb/datasource.js
  34. 0 8
      public/app/plugins/panel/alertlist/editor.html
  35. 1 1
      public/app/plugins/panel/alertlist/module.html
  36. 3 0
      public/app/plugins/panel/alertlist/module.ts
  37. 1 0
      public/sass/_grafana.scss
  38. 3 0
      public/sass/components/_panel_alertlist.scss

+ 2 - 0
docs/sources/datasources/cloudwatch.md

@@ -25,6 +25,7 @@ be ready to build dashboards for you CloudWatch metrics.
 
 3. Click the `Add new` link in the top header.
 4. Select `CloudWatch` from the dropdown.
+    > NOTE: If at any moment you have issues with getting this datasource to work and grafana is giving you undescriptive errors then dont forget to check your log file (try looking in /var/log/grafana/).
 
 Name | Description
 ------------ | -------------
@@ -47,6 +48,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
 ### AWS credentials file
 
 Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.
+    > NOTE: If you think you have the credentials file in the right place but it is still not working then you might try moving your .aws file to '/usr/share/grafana/' and make sure your credentials file has at most 0644 permissions.
 
 Example content:
 

+ 6 - 1
docs/sources/plugins/installation.md

@@ -34,11 +34,16 @@ List available plugins
 grafana-cli plugins list-remote
 ```
 
-Install a plugin type
+Install the latest version of a plugin
 ```
 grafana-cli plugins install <plugin-id>
 ```
 
+Install a specific version of a plugin
+```
+grafana-cli plugins install <plugin-id> <version>
+```
+
 List installed plugins
 ```
 grafana-cli plugins ls

+ 1 - 1
pkg/api/alerting.go

@@ -264,7 +264,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
 		return ApiError(500, "", err)
 	}
 
-	var response models.AlertStateType = models.AlertStateNoData
+	var response models.AlertStateType = models.AlertStatePending
 	pausedState := "un paused"
 	if cmd.Paused {
 		response = models.AlertStatePaused

+ 11 - 0
pkg/cmd/grafana-cli/services/services.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
+	"net"
 	"net/http"
 	"net/url"
 	"path"
@@ -25,6 +26,16 @@ func Init(version string) {
 	grafanaVersion = version
 
 	tr := &http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+		DialContext: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: 30 * time.Second,
+		}).DialContext,
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+
 		TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
 	}
 

+ 2 - 0
pkg/metrics/metrics.go

@@ -41,6 +41,7 @@ var (
 	M_Alerting_Result_State_Paused       Counter
 	M_Alerting_Result_State_NoData       Counter
 	M_Alerting_Result_State_ExecError    Counter
+	M_Alerting_Result_State_Pending			 Counter
 	M_Alerting_Active_Alerts             Counter
 	M_Alerting_Notification_Sent_Slack   Counter
 	M_Alerting_Notification_Sent_Email   Counter
@@ -102,6 +103,7 @@ func initMetricVars(settings *MetricSettings) {
 	M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
 	M_Alerting_Result_State_NoData = RegCounter("alerting.result", "state", "no_data")
 	M_Alerting_Result_State_ExecError = RegCounter("alerting.result", "state", "exec_error")
+	M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending")
 
 	M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
 	M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")

+ 7 - 6
pkg/models/alert.go

@@ -11,11 +11,12 @@ type AlertSeverityType string
 type NoDataOption string
 
 const (
-	AlertStateNoData    AlertStateType = "no_data"
-	AlertStateExecError AlertStateType = "execution_error"
-	AlertStatePaused    AlertStateType = "paused"
-	AlertStateAlerting  AlertStateType = "alerting"
-	AlertStateOK        AlertStateType = "ok"
+	AlertStateNoData    	 AlertStateType = "no_data"
+	AlertStateExecError 	 AlertStateType = "execution_error"
+	AlertStatePaused    	 AlertStateType = "paused"
+	AlertStateAlerting     AlertStateType = "alerting"
+	AlertStateOK           AlertStateType = "ok"
+	AlertStatePending      AlertStateType = "pending"
 )
 
 const (
@@ -26,7 +27,7 @@ const (
 )
 
 func (s AlertStateType) IsValid() bool {
-	return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused
+	return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused || s == AlertStatePending
 }
 
 func (s NoDataOption) IsValid() bool {

+ 13 - 6
pkg/services/alerting/conditions/query.go

@@ -33,15 +33,17 @@ type AlertQuery struct {
 	To           string
 }
 
-func (c *QueryCondition) Eval(context *alerting.EvalContext) {
+func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.ConditionResult, error) {
 	timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
+
 	seriesList, err := c.executeQuery(context, timeRange)
 	if err != nil {
-		context.Error = err
-		return
+		return nil, err
 	}
 
 	emptySerieCount := 0
+	evalMatchCount := 0
+	var matches []*alerting.EvalMatch
 	for _, series := range seriesList {
 		reducedValue := c.Reducer.Reduce(series)
 		evalMatch := c.Evaluator.Eval(reducedValue)
@@ -58,15 +60,20 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
 		}
 
 		if evalMatch {
-			context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
+			evalMatchCount++
+
+			matches = append(matches, &alerting.EvalMatch{
 				Metric: series.Name,
 				Value:  reducedValue.Float64,
 			})
 		}
 	}
 
-	context.NoDataFound = emptySerieCount == len(seriesList)
-	context.Firing = len(context.EvalMatches) > 0
+	return &alerting.ConditionResult{
+		Firing:      evalMatchCount > 0,
+		NoDataFound: emptySerieCount == len(seriesList),
+		EvalMatches: matches,
+	}, nil
 }
 
 func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {

+ 20 - 20
pkg/services/alerting/conditions/query_test.go

@@ -46,19 +46,19 @@ func TestQueryCondition(t *testing.T) {
 			Convey("should fire when avg is above 100", func() {
 				points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
 				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
-				ctx.exec()
+				cr, err := ctx.exec()
 
-				So(ctx.result.Error, ShouldBeNil)
-				So(ctx.result.Firing, ShouldBeTrue)
+				So(err, ShouldBeNil)
+				So(cr.Firing, ShouldBeTrue)
 			})
 
 			Convey("Should not fire when avg is below 100", func() {
 				points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
 				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
-				ctx.exec()
+				cr, err := ctx.exec()
 
-				So(ctx.result.Error, ShouldBeNil)
-				So(ctx.result.Firing, ShouldBeFalse)
+				So(err, ShouldBeNil)
+				So(cr.Firing, ShouldBeFalse)
 			})
 
 			Convey("Should fire if only first serie matches", func() {
@@ -66,10 +66,10 @@ func TestQueryCondition(t *testing.T) {
 					tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
 					tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
 				}
-				ctx.exec()
+				cr, err := ctx.exec()
 
-				So(ctx.result.Error, ShouldBeNil)
-				So(ctx.result.Firing, ShouldBeTrue)
+				So(err, ShouldBeNil)
+				So(cr.Firing, ShouldBeTrue)
 			})
 
 			Convey("Empty series", func() {
@@ -78,10 +78,10 @@ func TestQueryCondition(t *testing.T) {
 						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
 						tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
 					}
-					ctx.exec()
+					cr, err := ctx.exec()
 
-					So(ctx.result.Error, ShouldBeNil)
-					So(ctx.result.NoDataFound, ShouldBeTrue)
+					So(err, ShouldBeNil)
+					So(cr.NoDataFound, ShouldBeTrue)
 				})
 
 				Convey("Should set NoDataFound both series contains null", func() {
@@ -89,10 +89,10 @@ func TestQueryCondition(t *testing.T) {
 						tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
 						tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
 					}
-					ctx.exec()
+					cr, err := ctx.exec()
 
-					So(ctx.result.Error, ShouldBeNil)
-					So(ctx.result.NoDataFound, ShouldBeTrue)
+					So(err, ShouldBeNil)
+					So(cr.NoDataFound, ShouldBeTrue)
 				})
 
 				Convey("Should not set NoDataFound if one serie is empty", func() {
@@ -100,10 +100,10 @@ func TestQueryCondition(t *testing.T) {
 						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
 						tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
 					}
-					ctx.exec()
+					cr, err := ctx.exec()
 
-					So(ctx.result.Error, ShouldBeNil)
-					So(ctx.result.NoDataFound, ShouldBeFalse)
+					So(err, ShouldBeNil)
+					So(cr.NoDataFound, ShouldBeFalse)
 				})
 			})
 		})
@@ -120,7 +120,7 @@ type queryConditionTestContext struct {
 
 type queryConditionScenarioFunc func(c *queryConditionTestContext)
 
-func (ctx *queryConditionTestContext) exec() {
+func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) {
 	jsonModel, err := simplejson.NewJson([]byte(`{
             "type": "query",
             "query":  {
@@ -146,7 +146,7 @@ func (ctx *queryConditionTestContext) exec() {
 		}, nil
 	}
 
-	condition.Eval(ctx.result)
+	return condition.Eval(ctx.result)
 }
 
 func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {

+ 10 - 2
pkg/services/alerting/eval_handler.go

@@ -20,8 +20,12 @@ func NewEvalHandler() *DefaultEvalHandler {
 }
 
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
+	firing := true
 	for _, condition := range context.Rule.Conditions {
-		condition.Eval(context)
+		cr, err := condition.Eval(context)
+		if err != nil {
+			context.Error = err
+		}
 
 		// break if condition could not be evaluated
 		if context.Error != nil {
@@ -29,11 +33,15 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 		}
 
 		// break if result has not triggered yet
-		if context.Firing == false {
+		if cr.Firing == false {
+			firing = false
 			break
 		}
+
+		context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
 	}
 
+	context.Firing = firing
 	context.EndTime = time.Now()
 	elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
 	metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)

+ 6 - 5
pkg/services/alerting/eval_handler_test.go

@@ -8,11 +8,12 @@ import (
 )
 
 type conditionStub struct {
-	firing bool
+	firing  bool
+	matches []*EvalMatch
 }
 
-func (c *conditionStub) Eval(context *EvalContext) {
-	context.Firing = c.firing
+func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
+	return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil
 }
 
 func TestAlertingExecutor(t *testing.T) {
@@ -30,10 +31,10 @@ func TestAlertingExecutor(t *testing.T) {
 			So(context.Firing, ShouldEqual, true)
 		})
 
-		Convey("Show return false with not passing condition", func() {
+		Convey("Show return false with not passing asdf", func() {
 			context := NewEvalContext(context.TODO(), &Rule{
 				Conditions: []Condition{
-					&conditionStub{firing: true},
+					&conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
 					&conditionStub{firing: false},
 				},
 			})

+ 4 - 1
pkg/services/alerting/extractor.go

@@ -3,6 +3,8 @@ package alerting
 import (
 	"errors"
 
+	"fmt"
+
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
@@ -104,7 +106,8 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
 				panelQuery := findPanelQueryByRefId(panel, queryRefId)
 
 				if panelQuery == nil {
-					return nil, ValidationError{Reason: "Alert refes to query that cannot be found"}
+					reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
+					return nil, ValidationError{Reason: reason}
 				}
 
 				dsName := ""

+ 7 - 1
pkg/services/alerting/interfaces.go

@@ -21,6 +21,12 @@ type Notifier interface {
 	GetIsDefault() bool
 }
 
+type ConditionResult struct {
+	Firing      bool
+	NoDataFound bool
+	EvalMatches []*EvalMatch
+}
+
 type Condition interface {
-	Eval(result *EvalContext)
+	Eval(result *EvalContext) (*ConditionResult, error)
 }

+ 1 - 1
pkg/services/alerting/notifier.go

@@ -49,7 +49,7 @@ func (n *RootNotifier) Notify(context *EvalContext) error {
 		return err
 	}
 
-	n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "Amount to send", len(notifiers))
+	n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
 
 	if len(notifiers) == 0 {
 		return nil

+ 12 - 2
pkg/services/alerting/notifiers/slack.go

@@ -22,17 +22,21 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 		return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
 	}
 
+	recipient := model.Settings.Get("recipient").MustString()
+
 	return &SlackNotifier{
 		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
 		Url:          url,
+		Recipient:    recipient,
 		log:          log.New("alerting.notifier.slack"),
 	}, nil
 }
 
 type SlackNotifier struct {
 	NotifierBase
-	Url string
-	log log.Logger
+	Url       string
+	Recipient string
+	log       log.Logger
 }
 
 func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
@@ -85,6 +89,12 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
 				"ts":          time.Now().Unix(),
 			},
 		},
+		"parse": "full", // to linkify urls, users and channels in alert message.
+	}
+
+	//recipient override
+	if this.Recipient != "" {
+		body["channel"] = this.Recipient
 	}
 
 	data, _ := json.Marshal(&body)

+ 8 - 1
pkg/services/alerting/result_handler.go

@@ -86,7 +86,12 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
 			handler.log.Error("Failed to save annotation for new alert state", "error", err)
 		}
 
-		handler.notifier.Notify(evalContext)
+		if (oldState == m.AlertStatePending) && (evalContext.Rule.State == m.AlertStateOK) {
+			handler.log.Info("Notfication not sent", "oldState", oldState, "newState", evalContext.Rule.State)
+		} else {
+			handler.notifier.Notify(evalContext)
+		}
+
 	}
 
 	return nil
@@ -98,6 +103,8 @@ func (handler *DefaultResultHandler) shouldUpdateAlertState(evalContext *EvalCon
 
 func countStateResult(state m.AlertStateType) {
 	switch state {
+	case m.AlertStatePending:
+		metrics.M_Alerting_Result_State_Pending.Inc(1)
 	case m.AlertStateAlerting:
 		metrics.M_Alerting_Result_State_Alerting.Inc(1)
 	case m.AlertStateOK:

+ 3 - 1
pkg/services/alerting/rule_test.go

@@ -10,7 +10,9 @@ import (
 
 type FakeCondition struct{}
 
-func (f *FakeCondition) Eval(context *EvalContext) {}
+func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
+	return &ConditionResult{}, nil
+}
 
 func TestAlertRuleModel(t *testing.T) {
 	Convey("Testing alert rule", t, func() {

+ 1 - 1
pkg/services/notifications/mailer.go

@@ -99,7 +99,7 @@ func createDialer() (*gomail.Dialer, error) {
 		tlsconfig.Certificates = []tls.Certificate{cert}
 	}
 
-	d := gomail.NewPlainDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
+	d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
 	d.TLSConfig = tlsconfig
 	return d, nil
 }

+ 2 - 2
pkg/services/sqlstore/alert.go

@@ -173,7 +173,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor
 		} else {
 			alert.Updated = time.Now()
 			alert.Created = time.Now()
-			alert.State = m.AlertStateNoData
+			alert.State = m.AlertStatePending
 			alert.NewStateDate = time.Now()
 
 			_, err := sess.Insert(alert)
@@ -260,7 +260,7 @@ func PauseAlertRule(cmd *m.PauseAlertCommand) error {
 		if cmd.Paused {
 			newState = m.AlertStatePaused
 		} else {
-			newState = m.AlertStateNoData
+			newState = m.AlertStatePending
 		}
 		alert.State = newState
 

+ 2 - 2
pkg/services/sqlstore/alert_test.go

@@ -47,7 +47,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			So(err2, ShouldBeNil)
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.Message, ShouldEqual, "Alerting message")
-			So(alert.State, ShouldEqual, "no_data")
+			So(alert.State, ShouldEqual, "pending")
 			So(alert.Frequency, ShouldEqual, 1)
 		})
 
@@ -77,7 +77,7 @@ func TestAlertingDataAccess(t *testing.T) {
 				So(query.Result[0].Name, ShouldEqual, "Name")
 
 				Convey("Alert state should not be updated", func() {
-					So(query.Result[0].State, ShouldEqual, "no_data")
+					So(query.Result[0].State, ShouldEqual, "pending")
 				})
 			})
 

+ 18 - 12
pkg/tsdb/graphite/graphite.go

@@ -2,15 +2,14 @@ package graphite
 
 import (
 	"context"
-	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/url"
 	"path"
+	"regexp"
 	"strings"
-	"time"
 
 	"golang.org/x/net/context/ctxhttp"
 
@@ -36,14 +35,7 @@ func init() {
 	glog = log.New("tsdb.graphite")
 	tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
 
-	tr := &http.Transport{
-		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-	}
-
-	HttpClient = &http.Client{
-		Timeout:   time.Duration(15 * time.Second),
-		Transport: tr,
-	}
+	HttpClient = tsdb.GetDefaultClient()
 }
 
 func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
@@ -58,9 +50,9 @@ func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
 
 	for _, query := range queries {
 		if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
-			formData["target"] = []string{fullTarget}
+			formData["target"] = []string{fixIntervalFormat(fullTarget)}
 		} else {
-			formData["target"] = []string{query.Model.Get("target").MustString()}
+			formData["target"] = []string{fixIntervalFormat(query.Model.Get("target").MustString())}
 		}
 	}
 
@@ -150,3 +142,17 @@ func formatTimeRange(input string) string {
 	}
 	return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
 }
+
+func fixIntervalFormat(target string) string {
+	rMinute := regexp.MustCompile(`'(\d+)m'`)
+	rMin := regexp.MustCompile("m")
+	target = rMinute.ReplaceAllStringFunc(target, func(m string) string {
+		return rMin.ReplaceAllString(m, "min")
+	})
+	rMonth := regexp.MustCompile(`'(\d+)M'`)
+	rMon := regexp.MustCompile("M")
+	target = rMonth.ReplaceAllStringFunc(target, func(M string) string {
+		return rMon.ReplaceAllString(M, "mon")
+	})
+	return target
+}

+ 60 - 0
pkg/tsdb/graphite/graphite_test.go

@@ -1 +1,61 @@
 package graphite
+
+import (
+	. "github.com/smartystreets/goconvey/convey"
+	"testing"
+)
+
+func TestGraphiteFunctions(t *testing.T) {
+	Convey("Testing Graphite Functions", t, func() {
+
+		Convey("formatting time range for now", func() {
+
+			timeRange := formatTimeRange("now")
+			So(timeRange, ShouldEqual, "now")
+
+		})
+
+		Convey("formatting time range for now-1m", func() {
+
+			timeRange := formatTimeRange("now-1m")
+			So(timeRange, ShouldEqual, "now-1min")
+
+		})
+
+		Convey("formatting time range for now-1M", func() {
+
+			timeRange := formatTimeRange("now-1M")
+			So(timeRange, ShouldEqual, "now-1mon")
+
+		})
+
+		Convey("fix interval format in query for 1m", func() {
+
+			timeRange := fixIntervalFormat("aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1m'), 4)")
+			So(timeRange, ShouldEqual, "aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1min'), 4)")
+
+		})
+
+		Convey("fix interval format in query for 1M", func() {
+
+			timeRange := fixIntervalFormat("aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1M'), 4)")
+			So(timeRange, ShouldEqual, "aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1mon'), 4)")
+
+		})
+
+		Convey("should not override query for 1M", func() {
+
+			timeRange := fixIntervalFormat("app.grafana.*.dashboards.views.1M.count")
+			So(timeRange, ShouldEqual, "app.grafana.*.dashboards.views.1M.count")
+
+		})
+
+		Convey("should not override query for 1m", func() {
+
+			timeRange := fixIntervalFormat("app.grafana.*.dashboards.views.1m.count")
+			So(timeRange, ShouldEqual, "app.grafana.*.dashboards.views.1m.count")
+
+		})
+
+	})
+}

+ 29 - 0
pkg/tsdb/http.go

@@ -0,0 +1,29 @@
+package tsdb
+
+import (
+	"crypto/tls"
+	"net"
+	"net/http"
+	"time"
+)
+
+func GetDefaultClient() *http.Client {
+	tr := &http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+		DialContext: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: 30 * time.Second,
+		}).DialContext,
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	}
+
+	return &http.Client{
+		Timeout:   time.Duration(30 * time.Second),
+		Transport: tr,
+	}
+}

+ 1 - 10
pkg/tsdb/influxdb/influxdb.go

@@ -2,13 +2,11 @@ package influxdb
 
 import (
 	"context"
-	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
 	"path"
-	"time"
 
 	"golang.org/x/net/context/ctxhttp"
 
@@ -41,14 +39,7 @@ func init() {
 	glog = log.New("tsdb.influxdb")
 	tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor)
 
-	tr := &http.Transport{
-		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-	}
-
-	HttpClient = &http.Client{
-		Timeout:   time.Duration(15 * time.Second),
-		Transport: tr,
-	}
+	HttpClient = tsdb.GetDefaultClient()
 }
 
 func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {

+ 49 - 58
pkg/tsdb/opentsdb/opentsdb.go

@@ -2,19 +2,17 @@ package opentsdb
 
 import (
 	"context"
-	"crypto/tls"
 	"fmt"
 	"path"
 	"strconv"
 	"strings"
-	"time"
 
 	"golang.org/x/net/context/ctxhttp"
 
+	"encoding/json"
 	"io/ioutil"
 	"net/http"
 	"net/url"
-	"encoding/json"
 
 	"gopkg.in/guregu/null.v3"
 
@@ -40,14 +38,7 @@ func init() {
 	plog = log.New("tsdb.opentsdb")
 	tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor)
 
-	tr := &http.Transport{
-		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-	}
-
-	HttpClient = &http.Client{
-		Timeout:   time.Duration(15 * time.Second),
-		Transport: tr,
-	}
+	HttpClient = tsdb.GetDefaultClient()
 }
 
 func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
@@ -58,9 +49,9 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
 	tsdbQuery.Start = queryContext.TimeRange.GetFromAsMsEpoch()
 	tsdbQuery.End = queryContext.TimeRange.GetToAsMsEpoch()
 
-  for _ , query := range queries {
-  	metric := e.buildMetric(query)
-  	tsdbQuery.Queries = append(tsdbQuery.Queries, metric)
+	for _, query := range queries {
+		metric := e.buildMetric(query)
+		tsdbQuery.Queries = append(tsdbQuery.Queries, metric)
 	}
 
 	if setting.Env == setting.DEV {
@@ -104,7 +95,7 @@ func (e *OpenTsdbExecutor) createRequest(data OpenTsdbQuery) (*http.Request, err
 	if e.BasicAuth {
 		req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
 	}
-	
+
 	return req, err
 }
 
@@ -152,61 +143,61 @@ func (e *OpenTsdbExecutor) parseResponse(query OpenTsdbQuery, res *http.Response
 	return queryResults, nil
 }
 
-func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) (map[string]interface{}) {
+func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) map[string]interface{} {
 
 	metric := make(map[string]interface{})
 
-		// Setting metric and aggregator		
-		metric["metric"] = query.Model.Get("metric").MustString()
-		metric["aggregator"] = query.Model.Get("aggregator").MustString()
+	// Setting metric and aggregator
+	metric["metric"] = query.Model.Get("metric").MustString()
+	metric["aggregator"] = query.Model.Get("aggregator").MustString()
 
-		// Setting downsampling options
-		disableDownsampling := query.Model.Get("disableDownsampling").MustBool()
-		if !disableDownsampling {
-			downsampleInterval := query.Model.Get("downsampleInterval").MustString()
-			if downsampleInterval == "" {
-				downsampleInterval = "1m"  //default value for blank
-			}
-			downsample :=  downsampleInterval + "-" + query.Model.Get("downsampleAggregator").MustString()
-			if query.Model.Get("downsampleFillPolicy").MustString() != "none" {
-				metric["downsample"] = downsample + "-" + query.Model.Get("downsampleFillPolicy").MustString()
-			} else {
-				metric["downsample"] = downsample
-			}
+	// Setting downsampling options
+	disableDownsampling := query.Model.Get("disableDownsampling").MustBool()
+	if !disableDownsampling {
+		downsampleInterval := query.Model.Get("downsampleInterval").MustString()
+		if downsampleInterval == "" {
+			downsampleInterval = "1m" //default value for blank
+		}
+		downsample := downsampleInterval + "-" + query.Model.Get("downsampleAggregator").MustString()
+		if query.Model.Get("downsampleFillPolicy").MustString() != "none" {
+			metric["downsample"] = downsample + "-" + query.Model.Get("downsampleFillPolicy").MustString()
+		} else {
+			metric["downsample"] = downsample
 		}
+	}
 
-		// Setting rate options
-		if query.Model.Get("shouldComputeRate").MustBool() {
-			
-			metric["rate"] = true
-			rateOptions := make(map[string]interface{})
-			rateOptions["counter"] = query.Model.Get("isCounter").MustBool()
+	// Setting rate options
+	if query.Model.Get("shouldComputeRate").MustBool() {
 
-			counterMax, counterMaxCheck := query.Model.CheckGet("counterMax")
-			if counterMaxCheck {
-				rateOptions["counterMax"] = counterMax.MustFloat64()
-			}
-			
-			resetValue, resetValueCheck := query.Model.CheckGet("counterResetValue")
-			if resetValueCheck {
-				rateOptions["resetValue"] = resetValue.MustFloat64()
-			}
+		metric["rate"] = true
+		rateOptions := make(map[string]interface{})
+		rateOptions["counter"] = query.Model.Get("isCounter").MustBool()
 
-			metric["rateOptions"] = rateOptions
+		counterMax, counterMaxCheck := query.Model.CheckGet("counterMax")
+		if counterMaxCheck {
+			rateOptions["counterMax"] = counterMax.MustFloat64()
 		}
 
-		// Setting tags
-		tags, tagsCheck := query.Model.CheckGet("tags")
-		if tagsCheck && len(tags.MustMap()) > 0 {
-			metric["tags"] = tags.MustMap()
+		resetValue, resetValueCheck := query.Model.CheckGet("counterResetValue")
+		if resetValueCheck {
+			rateOptions["resetValue"] = resetValue.MustFloat64()
 		}
 
-		// Setting filters
-		filters, filtersCheck := query.Model.CheckGet("filters")
-		if filtersCheck && len(filters.MustArray()) > 0 {
-			metric["filters"] = filters.MustArray()
-		}
+		metric["rateOptions"] = rateOptions
+	}
+
+	// Setting tags
+	tags, tagsCheck := query.Model.CheckGet("tags")
+	if tagsCheck && len(tags.MustMap()) > 0 {
+		metric["tags"] = tags.MustMap()
+	}
+
+	// Setting filters
+	filters, filtersCheck := query.Model.CheckGet("filters")
+	if filtersCheck && len(filters.MustArray()) > 0 {
+		metric["filters"] = filters.MustArray()
+	}
 
-		return metric
+	return metric
 
 }

+ 6 - 13
pkg/tsdb/prometheus/prometheus.go

@@ -3,7 +3,6 @@ package prometheus
 import (
 	"context"
 	"fmt"
-	"net/http"
 	"regexp"
 	"strings"
 	"time"
@@ -25,8 +24,7 @@ func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
 }
 
 var (
-	plog       log.Logger
-	HttpClient http.Client
+	plog log.Logger
 )
 
 func init() {
@@ -83,6 +81,10 @@ func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlic
 func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
 	reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
 
+	if query.LegendFormat == "" {
+		return metric.String()
+	}
+
 	result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
 		labelName := strings.Replace(string(in), "{{", "", 1)
 		labelName = strings.Replace(labelName, "}}", "", 1)
@@ -110,10 +112,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
 		return nil, err
 	}
 
-	format, err := queryModel.Model.Get("legendFormat").String()
-	if err != nil {
-		return nil, err
-	}
+	format := queryModel.Model.Get("legendFormat").MustString("")
 
 	start, err := queryContext.TimeRange.ParseFrom()
 	if err != nil {
@@ -158,9 +157,3 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
 	queryResults["A"] = queryRes
 	return queryResults, nil
 }
-
-/*
-func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult {
-	result.Error = err
-	return result
-}*/

+ 14 - 0
pkg/tsdb/prometheus/prometheus_test.go

@@ -22,5 +22,19 @@ func TestPrometheus(t *testing.T) {
 
 			So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}")
 		})
+
+		Convey("build full serie name", func() {
+			metric := map[p.LabelName]p.LabelValue{
+				p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"),
+				p.LabelName("app"):             p.LabelValue("backend"),
+				p.LabelName("device"):          p.LabelValue("mobile"),
+			}
+
+			query := &PrometheusQuery{
+				LegendFormat: "",
+			}
+
+			So(formatLegend(metric, query), ShouldEqual, `http_request_total{app="backend", device="mobile"}`)
+		})
 	})
 }

+ 1 - 1
public/app/features/admin/partials/settings.html

@@ -7,7 +7,7 @@
 	</div>
 
 		<div class="grafana-info-box span8" style="margin: 20px 0 25px 0">
-			These system settings are defined in grafana.ini or grafana.custom.ini (or overriden in ENV variables).
+			These system settings are defined in grafana.ini or custom.ini (or overriden in ENV variables).
 			To change these you currently need to restart grafana.
 		</div>
 

+ 7 - 0
public/app/features/alerting/alert_def.ts

@@ -87,6 +87,13 @@ function getStateDisplayModel(state) {
         stateClass: 'alert-state-paused'
       };
     }
+    case 'pending': {
+      return {
+        text: 'PENDING',
+        iconClass: "fa fa-exclamation",
+        stateClass: 'alert-state-warning'
+      };
+    }
   }
 }
 

+ 12 - 1
public/app/features/alerting/partials/notification_edit.html

@@ -59,10 +59,21 @@
 
     <div class="gf-form-group" ng-if="ctrl.model.type === 'slack'">
       <h3 class="page-heading">Slack settings</h3>
-      <div class="gf-form">
+      <div class="gf-form max-width-30">
         <span class="gf-form-label width-6">Url</span>
         <input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
       </div>
+      <div class="gf-form max-width-30">
+        <span class="gf-form-label width-6">Recipient</span>
+        <input type="text"
+          class="gf-form-input max-width-30"
+          ng-model="ctrl.model.settings.recipient"
+          data-placement="right">
+        </input>
+        <info-popover mode="right-absolute">
+          Override default channel or user, use #channel-name or @username
+        </info-popover>
+      </div>
     </div>
 
     <div class="gf-form-group section" ng-if="ctrl.model.type === 'email'">

+ 1 - 0
public/app/features/templating/constant_variable.ts

@@ -16,6 +16,7 @@ export class ConstantVariable implements Variable {
     label: '',
     query: '',
     current: {},
+    options: [],
   };
 
   /** @ngInject **/

+ 5 - 0
public/app/features/templating/specs/template_srv_specs.ts

@@ -145,6 +145,11 @@ describe('templateSrv', function() {
       expect(result).to.be('test|test2');
     });
 
+    it('multi value and distributed should render distributed string', function() {
+      var result = _templateSrv.formatValue(['test','test2'], 'distributed', { name: 'build' });
+      expect(result).to.be('test,build=test2');
+    });
+
     it('slash should be properly escaped in regex format', function() {
       var result = _templateSrv.formatValue('Gi3/14', 'regex');
       expect(result).to.be('Gi3\\/14');

+ 14 - 0
public/app/features/templating/templateSrv.js

@@ -95,6 +95,9 @@ function (angular, _, kbn) {
           }
           return value.join('|');
         }
+        case "distributed": {
+          return this.distributeVariable(value, variable.name);
+        }
         default:  {
           if (typeof value === 'string') {
             return value;
@@ -210,6 +213,17 @@ function (angular, _, kbn) {
       });
     };
 
+    this.distributeVariable = function(value, variable) {
+      value = _.map(value, function(val, index) {
+        if (index !== 0) {
+          return variable + "=" + val;
+        } else {
+          return val;
+        }
+      });
+      return value.join(',');
+    };
+
   });
 
 });

+ 1 - 1
public/app/plugins/datasource/opentsdb/datasource.js

@@ -244,7 +244,7 @@ function (angular, _, dateMath) {
 
       var interpolated;
       try {
-        interpolated = templateSrv.replace(query);
+        interpolated = templateSrv.replace(query, {}, 'distributed');
       }
       catch (err) {
         return $q.reject(err);

+ 0 - 8
public/app/plugins/panel/alertlist/editor.html

@@ -20,12 +20,4 @@
     <gf-form-switch class="gf-form" label="Execution error" label-class="width-10" checked="ctrl.stateFilter['execution_error']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
     <gf-form-switch class="gf-form" label="Alerting" label-class="width-10" checked="ctrl.stateFilter['alerting']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
   </div>
-
-  <div class="section gf-form-group" ng-if="ctrl.panel.show == 'changes'">
-    <!-- <h5 class="section-heading">Current state</h5> -->
-	</div>
-
-  <div class="section gf-form-group" ng-if="ctrl.panel.show == 'current'">
-    <!-- <h5 class="section-heading">Current state</h5> -->
-  </div>
 </div>

+ 1 - 1
public/app/plugins/panel/alertlist/module.html

@@ -1,4 +1,4 @@
-<div class="panel-alert-list">
+<div class="panel-alert-list" style="{{ctrl.contentHeight}}">
   <section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
     <ol class="card-list">
       <li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">

+ 3 - 0
public/app/plugins/panel/alertlist/module.ts

@@ -17,6 +17,7 @@ class AlertListPanel extends PanelCtrl {
     {text: 'Recent state changes', value: 'changes'}
   ];
 
+  contentHeight: string;
   stateFilter: any = {};
   currentAlerts: any = [];
   alertHistory: any = [];
@@ -27,6 +28,7 @@ class AlertListPanel extends PanelCtrl {
     stateFilter: []
   };
 
+
   /** @ngInject */
   constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) {
     super($scope, $injector);
@@ -55,6 +57,7 @@ class AlertListPanel extends PanelCtrl {
   }
 
   onRender() {
+    this.contentHeight = "max-height: " + this.height + "px;";
     if (this.panel.show === 'current') {
       this.getCurrentAlertState();
     }

+ 1 - 0
public/sass/_grafana.scss

@@ -41,6 +41,7 @@
 @import "components/tags";
 @import "components/panel_graph";
 @import "components/submenu";
+@import "components/panel_alertlist";
 @import "components/panel_dashlist";
 @import "components/panel_pluginlist";
 @import "components/panel_singlestat";

+ 3 - 0
public/sass/components/_panel_alertlist.scss

@@ -0,0 +1,3 @@
+.panel-alert-list {
+  overflow-y: scroll;
+}