Просмотр исходного кода

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

Torkel Ödegaard 9 лет назад
Родитель
Сommit
2da2d5df56
38 измененных файлов с 344 добавлено и 158 удалено
  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.
 3. Click the `Add new` link in the top header.
 4. Select `CloudWatch` from the dropdown.
 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
 Name | Description
 ------------ | -------------
 ------------ | -------------
@@ -47,6 +48,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
 ### AWS credentials file
 ### AWS credentials file
 
 
 Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.
 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:
 Example content:
 
 

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

@@ -34,11 +34,16 @@ List available plugins
 grafana-cli plugins list-remote
 grafana-cli plugins list-remote
 ```
 ```
 
 
-Install a plugin type
+Install the latest version of a plugin
 ```
 ```
 grafana-cli plugins install <plugin-id>
 grafana-cli plugins install <plugin-id>
 ```
 ```
 
 
+Install a specific version of a plugin
+```
+grafana-cli plugins install <plugin-id> <version>
+```
+
 List installed plugins
 List installed plugins
 ```
 ```
 grafana-cli plugins ls
 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)
 		return ApiError(500, "", err)
 	}
 	}
 
 
-	var response models.AlertStateType = models.AlertStateNoData
+	var response models.AlertStateType = models.AlertStatePending
 	pausedState := "un paused"
 	pausedState := "un paused"
 	if cmd.Paused {
 	if cmd.Paused {
 		response = models.AlertStatePaused
 		response = models.AlertStatePaused

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

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
+	"net"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"path"
 	"path"
@@ -25,6 +26,16 @@ func Init(version string) {
 	grafanaVersion = version
 	grafanaVersion = version
 
 
 	tr := &http.Transport{
 	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},
 		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_Paused       Counter
 	M_Alerting_Result_State_NoData       Counter
 	M_Alerting_Result_State_NoData       Counter
 	M_Alerting_Result_State_ExecError    Counter
 	M_Alerting_Result_State_ExecError    Counter
+	M_Alerting_Result_State_Pending			 Counter
 	M_Alerting_Active_Alerts             Counter
 	M_Alerting_Active_Alerts             Counter
 	M_Alerting_Notification_Sent_Slack   Counter
 	M_Alerting_Notification_Sent_Slack   Counter
 	M_Alerting_Notification_Sent_Email   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_Paused = RegCounter("alerting.result", "state", "paused")
 	M_Alerting_Result_State_NoData = RegCounter("alerting.result", "state", "no_data")
 	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_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_Active_Alerts = RegCounter("alerting.active_alerts")
 	M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
 	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
 type NoDataOption string
 
 
 const (
 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 (
 const (
@@ -26,7 +27,7 @@ const (
 )
 )
 
 
 func (s AlertStateType) IsValid() bool {
 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 {
 func (s NoDataOption) IsValid() bool {

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

@@ -33,15 +33,17 @@ type AlertQuery struct {
 	To           string
 	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)
 	timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
+
 	seriesList, err := c.executeQuery(context, timeRange)
 	seriesList, err := c.executeQuery(context, timeRange)
 	if err != nil {
 	if err != nil {
-		context.Error = err
-		return
+		return nil, err
 	}
 	}
 
 
 	emptySerieCount := 0
 	emptySerieCount := 0
+	evalMatchCount := 0
+	var matches []*alerting.EvalMatch
 	for _, series := range seriesList {
 	for _, series := range seriesList {
 		reducedValue := c.Reducer.Reduce(series)
 		reducedValue := c.Reducer.Reduce(series)
 		evalMatch := c.Evaluator.Eval(reducedValue)
 		evalMatch := c.Evaluator.Eval(reducedValue)
@@ -58,15 +60,20 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
 		}
 		}
 
 
 		if evalMatch {
 		if evalMatch {
-			context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
+			evalMatchCount++
+
+			matches = append(matches, &alerting.EvalMatch{
 				Metric: series.Name,
 				Metric: series.Name,
 				Value:  reducedValue.Float64,
 				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) {
 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() {
 			Convey("should fire when avg is above 100", func() {
 				points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
 				points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
 				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
 				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() {
 			Convey("Should not fire when avg is below 100", func() {
 				points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
 				points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
 				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
 				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() {
 			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("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
 					tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 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() {
 			Convey("Empty series", func() {
@@ -78,10 +78,10 @@ func TestQueryCondition(t *testing.T) {
 						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
 						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
 						tsdb.NewTimeSeries("test2", 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() {
 				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("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
 						tsdb.NewTimeSeries("test2", 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() {
 				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("test1", tsdb.NewTimeSeriesPointsFromArgs()),
 						tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
 						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)
 type queryConditionScenarioFunc func(c *queryConditionTestContext)
 
 
-func (ctx *queryConditionTestContext) exec() {
+func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) {
 	jsonModel, err := simplejson.NewJson([]byte(`{
 	jsonModel, err := simplejson.NewJson([]byte(`{
             "type": "query",
             "type": "query",
             "query":  {
             "query":  {
@@ -146,7 +146,7 @@ func (ctx *queryConditionTestContext) exec() {
 		}, nil
 		}, nil
 	}
 	}
 
 
-	condition.Eval(ctx.result)
+	return condition.Eval(ctx.result)
 }
 }
 
 
 func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {
 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) {
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
+	firing := true
 	for _, condition := range context.Rule.Conditions {
 	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
 		// break if condition could not be evaluated
 		if context.Error != nil {
 		if context.Error != nil {
@@ -29,11 +33,15 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 		}
 		}
 
 
 		// break if result has not triggered yet
 		// break if result has not triggered yet
-		if context.Firing == false {
+		if cr.Firing == false {
+			firing = false
 			break
 			break
 		}
 		}
+
+		context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
 	}
 	}
 
 
+	context.Firing = firing
 	context.EndTime = time.Now()
 	context.EndTime = time.Now()
 	elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
 	elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
 	metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)
 	metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)

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

@@ -8,11 +8,12 @@ import (
 )
 )
 
 
 type conditionStub struct {
 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) {
 func TestAlertingExecutor(t *testing.T) {
@@ -30,10 +31,10 @@ func TestAlertingExecutor(t *testing.T) {
 			So(context.Firing, ShouldEqual, true)
 			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{
 			context := NewEvalContext(context.TODO(), &Rule{
 				Conditions: []Condition{
 				Conditions: []Condition{
-					&conditionStub{firing: true},
+					&conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
 					&conditionStub{firing: false},
 					&conditionStub{firing: false},
 				},
 				},
 			})
 			})

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

@@ -3,6 +3,8 @@ package alerting
 import (
 import (
 	"errors"
 	"errors"
 
 
+	"fmt"
+
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
@@ -104,7 +106,8 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
 				panelQuery := findPanelQueryByRefId(panel, queryRefId)
 				panelQuery := findPanelQueryByRefId(panel, queryRefId)
 
 
 				if panelQuery == nil {
 				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 := ""
 				dsName := ""

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

@@ -21,6 +21,12 @@ type Notifier interface {
 	GetIsDefault() bool
 	GetIsDefault() bool
 }
 }
 
 
+type ConditionResult struct {
+	Firing      bool
+	NoDataFound bool
+	EvalMatches []*EvalMatch
+}
+
 type Condition interface {
 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
 		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 {
 	if len(notifiers) == 0 {
 		return nil
 		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"}
 		return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
 	}
 	}
 
 
+	recipient := model.Settings.Get("recipient").MustString()
+
 	return &SlackNotifier{
 	return &SlackNotifier{
 		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
 		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
 		Url:          url,
 		Url:          url,
+		Recipient:    recipient,
 		log:          log.New("alerting.notifier.slack"),
 		log:          log.New("alerting.notifier.slack"),
 	}, nil
 	}, nil
 }
 }
 
 
 type SlackNotifier struct {
 type SlackNotifier struct {
 	NotifierBase
 	NotifierBase
-	Url string
-	log log.Logger
+	Url       string
+	Recipient string
+	log       log.Logger
 }
 }
 
 
 func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
@@ -85,6 +89,12 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
 				"ts":          time.Now().Unix(),
 				"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)
 	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.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
 	return nil
@@ -98,6 +103,8 @@ func (handler *DefaultResultHandler) shouldUpdateAlertState(evalContext *EvalCon
 
 
 func countStateResult(state m.AlertStateType) {
 func countStateResult(state m.AlertStateType) {
 	switch state {
 	switch state {
+	case m.AlertStatePending:
+		metrics.M_Alerting_Result_State_Pending.Inc(1)
 	case m.AlertStateAlerting:
 	case m.AlertStateAlerting:
 		metrics.M_Alerting_Result_State_Alerting.Inc(1)
 		metrics.M_Alerting_Result_State_Alerting.Inc(1)
 	case m.AlertStateOK:
 	case m.AlertStateOK:

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

@@ -10,7 +10,9 @@ import (
 
 
 type FakeCondition struct{}
 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) {
 func TestAlertRuleModel(t *testing.T) {
 	Convey("Testing alert rule", t, func() {
 	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}
 		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
 	d.TLSConfig = tlsconfig
 	return d, nil
 	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 {
 		} else {
 			alert.Updated = time.Now()
 			alert.Updated = time.Now()
 			alert.Created = time.Now()
 			alert.Created = time.Now()
-			alert.State = m.AlertStateNoData
+			alert.State = m.AlertStatePending
 			alert.NewStateDate = time.Now()
 			alert.NewStateDate = time.Now()
 
 
 			_, err := sess.Insert(alert)
 			_, err := sess.Insert(alert)
@@ -260,7 +260,7 @@ func PauseAlertRule(cmd *m.PauseAlertCommand) error {
 		if cmd.Paused {
 		if cmd.Paused {
 			newState = m.AlertStatePaused
 			newState = m.AlertStatePaused
 		} else {
 		} else {
-			newState = m.AlertStateNoData
+			newState = m.AlertStatePending
 		}
 		}
 		alert.State = newState
 		alert.State = newState
 
 

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

@@ -47,7 +47,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			So(err2, ShouldBeNil)
 			So(err2, ShouldBeNil)
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.Message, ShouldEqual, "Alerting message")
 			So(alert.Message, ShouldEqual, "Alerting message")
-			So(alert.State, ShouldEqual, "no_data")
+			So(alert.State, ShouldEqual, "pending")
 			So(alert.Frequency, ShouldEqual, 1)
 			So(alert.Frequency, ShouldEqual, 1)
 		})
 		})
 
 
@@ -77,7 +77,7 @@ func TestAlertingDataAccess(t *testing.T) {
 				So(query.Result[0].Name, ShouldEqual, "Name")
 				So(query.Result[0].Name, ShouldEqual, "Name")
 
 
 				Convey("Alert state should not be updated", func() {
 				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 (
 import (
 	"context"
 	"context"
-	"crypto/tls"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"path"
 	"path"
+	"regexp"
 	"strings"
 	"strings"
-	"time"
 
 
 	"golang.org/x/net/context/ctxhttp"
 	"golang.org/x/net/context/ctxhttp"
 
 
@@ -36,14 +35,7 @@ func init() {
 	glog = log.New("tsdb.graphite")
 	glog = log.New("tsdb.graphite")
 	tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
 	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 {
 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 {
 	for _, query := range queries {
 		if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
 		if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
-			formData["target"] = []string{fullTarget}
+			formData["target"] = []string{fixIntervalFormat(fullTarget)}
 		} else {
 		} 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)
 	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
 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 (
 import (
 	"context"
 	"context"
-	"crypto/tls"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"path"
 	"path"
-	"time"
 
 
 	"golang.org/x/net/context/ctxhttp"
 	"golang.org/x/net/context/ctxhttp"
 
 
@@ -41,14 +39,7 @@ func init() {
 	glog = log.New("tsdb.influxdb")
 	glog = log.New("tsdb.influxdb")
 	tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor)
 	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 {
 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 (
 import (
 	"context"
 	"context"
-	"crypto/tls"
 	"fmt"
 	"fmt"
 	"path"
 	"path"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
-	"time"
 
 
 	"golang.org/x/net/context/ctxhttp"
 	"golang.org/x/net/context/ctxhttp"
 
 
+	"encoding/json"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
-	"encoding/json"
 
 
 	"gopkg.in/guregu/null.v3"
 	"gopkg.in/guregu/null.v3"
 
 
@@ -40,14 +38,7 @@ func init() {
 	plog = log.New("tsdb.opentsdb")
 	plog = log.New("tsdb.opentsdb")
 	tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor)
 	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 {
 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.Start = queryContext.TimeRange.GetFromAsMsEpoch()
 	tsdbQuery.End = queryContext.TimeRange.GetToAsMsEpoch()
 	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 {
 	if setting.Env == setting.DEV {
@@ -104,7 +95,7 @@ func (e *OpenTsdbExecutor) createRequest(data OpenTsdbQuery) (*http.Request, err
 	if e.BasicAuth {
 	if e.BasicAuth {
 		req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
 		req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
 	}
 	}
-	
+
 	return req, err
 	return req, err
 }
 }
 
 
@@ -152,61 +143,61 @@ func (e *OpenTsdbExecutor) parseResponse(query OpenTsdbQuery, res *http.Response
 	return queryResults, nil
 	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{})
 	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 (
 import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
-	"net/http"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
 	"time"
 	"time"
@@ -25,8 +24,7 @@ func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
 }
 }
 
 
 var (
 var (
-	plog       log.Logger
-	HttpClient http.Client
+	plog log.Logger
 )
 )
 
 
 func init() {
 func init() {
@@ -83,6 +81,10 @@ func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlic
 func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
 func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
 	reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
 	reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
 
 
+	if query.LegendFormat == "" {
+		return metric.String()
+	}
+
 	result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
 	result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
 		labelName := strings.Replace(string(in), "{{", "", 1)
 		labelName := strings.Replace(string(in), "{{", "", 1)
 		labelName = strings.Replace(labelName, "}}", "", 1)
 		labelName = strings.Replace(labelName, "}}", "", 1)
@@ -110,10 +112,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
 		return nil, err
 		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()
 	start, err := queryContext.TimeRange.ParseFrom()
 	if err != nil {
 	if err != nil {
@@ -158,9 +157,3 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
 	queryResults["A"] = queryRes
 	queryResults["A"] = queryRes
 	return queryResults, nil
 	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}}")
 			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>
 
 
 		<div class="grafana-info-box span8" style="margin: 20px 0 25px 0">
 		<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.
 			To change these you currently need to restart grafana.
 		</div>
 		</div>
 
 

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

@@ -87,6 +87,13 @@ function getStateDisplayModel(state) {
         stateClass: 'alert-state-paused'
         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'">
     <div class="gf-form-group" ng-if="ctrl.model.type === 'slack'">
       <h3 class="page-heading">Slack settings</h3>
       <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>
         <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>
         <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>
+      <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>
 
 
     <div class="gf-form-group section" ng-if="ctrl.model.type === 'email'">
     <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: '',
     label: '',
     query: '',
     query: '',
     current: {},
     current: {},
+    options: [],
   };
   };
 
 
   /** @ngInject **/
   /** @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');
       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() {
     it('slash should be properly escaped in regex format', function() {
       var result = _templateSrv.formatValue('Gi3/14', 'regex');
       var result = _templateSrv.formatValue('Gi3/14', 'regex');
       expect(result).to.be('Gi3\\/14');
       expect(result).to.be('Gi3\\/14');

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

@@ -95,6 +95,9 @@ function (angular, _, kbn) {
           }
           }
           return value.join('|');
           return value.join('|');
         }
         }
+        case "distributed": {
+          return this.distributeVariable(value, variable.name);
+        }
         default:  {
         default:  {
           if (typeof value === 'string') {
           if (typeof value === 'string') {
             return value;
             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;
       var interpolated;
       try {
       try {
-        interpolated = templateSrv.replace(query);
+        interpolated = templateSrv.replace(query, {}, 'distributed');
       }
       }
       catch (err) {
       catch (err) {
         return $q.reject(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="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>
     <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>
-
-  <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>
 </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'">
   <section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
     <ol class="card-list">
     <ol class="card-list">
       <li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">
       <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'}
     {text: 'Recent state changes', value: 'changes'}
   ];
   ];
 
 
+  contentHeight: string;
   stateFilter: any = {};
   stateFilter: any = {};
   currentAlerts: any = [];
   currentAlerts: any = [];
   alertHistory: any = [];
   alertHistory: any = [];
@@ -27,6 +28,7 @@ class AlertListPanel extends PanelCtrl {
     stateFilter: []
     stateFilter: []
   };
   };
 
 
+
   /** @ngInject */
   /** @ngInject */
   constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) {
   constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) {
     super($scope, $injector);
     super($scope, $injector);
@@ -55,6 +57,7 @@ class AlertListPanel extends PanelCtrl {
   }
   }
 
 
   onRender() {
   onRender() {
+    this.contentHeight = "max-height: " + this.height + "px;";
     if (this.panel.show === 'current') {
     if (this.panel.show === 'current') {
       this.getCurrentAlertState();
       this.getCurrentAlertState();
     }
     }

+ 1 - 0
public/sass/_grafana.scss

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

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

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