Browse Source

Merge branch 'master' into alerting_opentsdb

bergquist 9 years ago
parent
commit
8368a4a88a
70 changed files with 954 additions and 511 deletions
  1. 1 1
      README.md
  2. 3 3
      docs/sources/guides/basic_concepts.md
  3. 1 1
      docs/sources/guides/gettingstarted.md
  4. 27 0
      docs/sources/installation/configuration.md
  5. 19 0
      pkg/api/alerting.go
  6. 1 0
      pkg/api/api.go
  7. 1 1
      pkg/api/common.go
  8. 5 0
      pkg/api/datasources.go
  9. 2 1
      pkg/api/metrics.go
  10. 40 0
      pkg/bus/bus.go
  11. 6 60
      pkg/cmd/grafana-server/main.go
  12. 150 0
      pkg/cmd/grafana-server/server.go
  13. 15 0
      pkg/models/alert.go
  14. 2 1
      pkg/models/datasource.go
  15. 11 0
      pkg/models/notifications.go
  16. 10 0
      pkg/models/server.go
  17. 1 1
      pkg/services/alerting/conditions/query.go
  18. 2 1
      pkg/services/alerting/conditions/query_test.go
  19. 35 55
      pkg/services/alerting/engine.go
  20. 21 5
      pkg/services/alerting/eval_context.go
  21. 1 45
      pkg/services/alerting/eval_handler.go
  22. 5 4
      pkg/services/alerting/eval_handler_test.go
  23. 2 2
      pkg/services/alerting/extractor.go
  24. 0 2
      pkg/services/alerting/extractor_test.go
  25. 3 5
      pkg/services/alerting/interfaces.go
  26. 13 7
      pkg/services/alerting/notifier.go
  27. 1 1
      pkg/services/alerting/notifier_test.go
  28. 23 17
      pkg/services/alerting/notifiers/email.go
  29. 16 19
      pkg/services/alerting/notifiers/slack.go
  30. 13 11
      pkg/services/alerting/notifiers/webhook.go
  31. 29 27
      pkg/services/alerting/result_handler.go
  32. 4 3
      pkg/services/alerting/test_notification.go
  33. 2 1
      pkg/services/alerting/test_rule.go
  34. 50 0
      pkg/services/notifications/mailer.go
  35. 27 33
      pkg/services/notifications/notifications.go
  36. 1 2
      pkg/services/notifications/notifications_test.go
  37. 16 12
      pkg/services/notifications/webhook.go
  38. 17 0
      pkg/services/sqlstore/alert.go
  39. 7 0
      pkg/services/sqlstore/datasource.go
  40. 7 2
      pkg/services/sqlstore/datasource_test.go
  41. 27 26
      pkg/services/sqlstore/migrator/migrator.go
  42. 8 5
      pkg/tsdb/batch.go
  43. 3 1
      pkg/tsdb/executor.go
  44. 3 1
      pkg/tsdb/fake_test.go
  45. 8 4
      pkg/tsdb/graphite/graphite.go
  46. 3 3
      pkg/tsdb/prometheus/prometheus.go
  47. 6 4
      pkg/tsdb/request.go
  48. 3 1
      pkg/tsdb/testdata/testdata.go
  49. 6 5
      pkg/tsdb/tsdb_test.go
  50. 1 1
      public/app/core/directives/metric_segment.js
  51. 1 1
      public/app/core/directives/value_select_dropdown.js
  52. 43 31
      public/app/features/alerting/alert_tab_ctrl.ts
  53. 9 5
      public/app/features/alerting/notification_edit_ctrl.ts
  54. 2 2
      public/app/features/alerting/partials/alert_tab.html
  55. 63 71
      public/app/features/alerting/partials/notification_edit.html
  56. 40 4
      public/app/features/annotations/annotations_srv.ts
  57. 1 1
      public/app/features/dashboard/dashnav/dashnav.ts
  58. 3 1
      public/app/features/panel/metrics_panel_ctrl.ts
  59. 21 1
      public/app/features/panel/panel_directive.ts
  60. 1 0
      public/app/features/panel/panel_menu.js
  61. 1 1
      public/app/features/plugins/ds_edit_ctrl.ts
  62. 2 2
      public/app/partials/panelgeneral.html
  63. 1 1
      public/app/partials/valueSelectDropdown.html
  64. 26 7
      public/app/plugins/datasource/cloudwatch/datasource.js
  65. 29 0
      public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts
  66. 1 1
      public/app/plugins/panel/graph/graph.ts
  67. 7 3
      public/app/plugins/panel/graph/module.ts
  68. 1 1
      public/app/plugins/panel/graph/thresholds_form.ts
  69. 1 4
      public/app/plugins/panel/table/editor.html
  70. 43 0
      public/sass/pages/_alerting.scss

+ 1 - 1
README.md

@@ -3,7 +3,7 @@
 [Website](http://grafana.org) |
 [Website](http://grafana.org) |
 [Twitter](https://twitter.com/grafana) |
 [Twitter](https://twitter.com/grafana) |
 [IRC](https://webchat.freenode.net/?channels=grafana) |
 [IRC](https://webchat.freenode.net/?channels=grafana) |
-![](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com)
+[![Slack](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com)](http://slack.raintank.io)
 [Slack](http://slack.raintank.io) |
 [Slack](http://slack.raintank.io) |
 [Email](mailto:contact@grafana.org)
 [Email](mailto:contact@grafana.org)
 
 

+ 3 - 3
docs/sources/guides/basic_concepts.md

@@ -12,7 +12,7 @@ This document is a “bottom up” introduction to basic concepts in Grafana, an
 ### ** Data Source **
 ### ** Data Source **
 Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. 
 Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. 
 
 
-The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), and [KairosDB](/datasources/kairosdb)
+The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), [Prometheus](/datasources/prometheus/), [Elasticsearch](/datasources/elasticsearch/), [CloudWatch](/datasources/cloudwatch/), and [KairosDB](/datasources/kairosdb)
 
 
 The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization.
 The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization.
 
 
@@ -58,7 +58,7 @@ There are a wide variety of styling and formatting options that each Panel expos
 
 
 Panels can be dragged and dropped and rearranged on the Dashboard. They can also be resized.
 Panels can be dragged and dropped and rearranged on the Dashboard. They can also be resized.
 
 
-There are currently four Panel types: [Graph](/reference/graph/), [Singlestat](/reference/singlestat/), [Dashlist](/reference/dashlist/), and [Text](/reference/text/).
+There are currently four Panel types: [Graph](/reference/graph/), [Singlestat](/reference/singlestat/), [Dashlist](/reference/dashlist/), [Table](/reference/table_panel/),and [Text](/reference/text/).
 
 
 Panels like the [Graph](/reference/graph/) panel allow you to graph as many metrics and series as you want. Other panels like [Singlestat](/reference/singlestat/) require a reduction of a single query into a single number. [Dashlist](/reference/dashlist/) and [Text](/reference/text/) are special panels that do not connect to any Data Source.
 Panels like the [Graph](/reference/graph/) panel allow you to graph as many metrics and series as you want. Other panels like [Singlestat](/reference/singlestat/) require a reduction of a single query into a single number. [Dashlist](/reference/dashlist/) and [Text](/reference/text/) are special panels that do not connect to any Data Source.
 
 
@@ -66,7 +66,7 @@ Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/t
 
 
 Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected.
 Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected.
 
 
-The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-override).
+The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift).
 
 
 Panels (or an entire Dashboard) can be [Shared](/reference/sharing/) easily in a variety of ways. You can send a link to someone who has a login to your Grafana. You can use the [Snapshot](/reference/sharing/#snapshots) feature to encode all the data currently being viewed into a static and interactive JSON document; it's so much better than emailing a screenshot!
 Panels (or an entire Dashboard) can be [Shared](/reference/sharing/) easily in a variety of ways. You can send a link to someone who has a login to your Grafana. You can use the [Snapshot](/reference/sharing/#snapshots) feature to encode all the data currently being viewed into a static and interactive JSON document; it's so much better than emailing a screenshot!
 
 

+ 1 - 1
docs/sources/guides/gettingstarted.md

@@ -29,7 +29,7 @@ The image above shows you the top header for a Dashboard.
 6. Settings: Manage Dashboard settings and features such as Templating and Annotations.
 6. Settings: Manage Dashboard settings and features such as Templating and Annotations.
 
 
 ## Dashboards, Panels, Rows, the building blocks of Grafana...
 ## Dashboards, Panels, Rows, the building blocks of Grafana...
-Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Core Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
+Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
 
 
 
 
 ## Adding & Editing Graphs and Panels
 ## Adding & Editing Graphs and Panels

+ 27 - 0
docs/sources/installation/configuration.md

@@ -492,6 +492,33 @@ Grafana backend index those json dashboards which will make them appear in regul
 ### path
 ### path
 The full path to a directory containing your json dashboards.
 The full path to a directory containing your json dashboards.
 
 
+## [smtp]
+Email server settings.
+
+### enabled
+defaults to false
+
+### host
+defaults to localhost:25
+
+### user
+In case of SMTP auth, defaults to `empty`
+
+### password
+In case of SMTP auth, defaults to `empty`
+
+### cert_file
+File path to a cert file, defaults to `empty`
+
+### key_file
+File path to a key file, defaults to `empty`
+
+### skip_verify
+Verify SSL for smtp server? defaults to `false`
+
+### from_address
+Address used when sending out emails, defaults to `admin@grafana.localhost`
+
 ## [log]
 ## [log]
 
 
 ### mode
 ### mode

+ 19 - 0
pkg/api/alerting.go

@@ -25,6 +25,25 @@ func ValidateOrgAlert(c *middleware.Context) {
 	}
 	}
 }
 }
 
 
+func GetAlertStatesForDashboard(c *middleware.Context) Response {
+	dashboardId := c.QueryInt64("dashboardId")
+
+	if dashboardId == 0 {
+		return ApiError(400, "Missing query parameter dashboardId", nil)
+	}
+
+	query := models.GetAlertStatesForDashboardQuery{
+		OrgId:       c.OrgId,
+		DashboardId: c.QueryInt64("dashboardId"),
+	}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return ApiError(500, "Failed to fetch alert states", err)
+	}
+
+	return Json(200, query.Result)
+}
+
 // GET /api/alerts
 // GET /api/alerts
 func GetAlerts(c *middleware.Context) Response {
 func GetAlerts(c *middleware.Context) Response {
 	query := models.GetAlertsQuery{
 	query := models.GetAlertsQuery{

+ 1 - 0
pkg/api/api.go

@@ -254,6 +254,7 @@ func Register(r *macaron.Macaron) {
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/", wrap(GetAlerts))
 			r.Get("/", wrap(GetAlerts))
+			r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
 		})
 		})
 
 
 		r.Get("/alert-notifications", wrap(GetAlertNotifications))
 		r.Get("/alert-notifications", wrap(GetAlertNotifications))

+ 1 - 1
pkg/api/common.go

@@ -79,7 +79,7 @@ func Json(status int, body interface{}) *NormalResponse {
 func ApiSuccess(message string) *NormalResponse {
 func ApiSuccess(message string) *NormalResponse {
 	resp := make(map[string]interface{})
 	resp := make(map[string]interface{})
 	resp["message"] = message
 	resp["message"] = message
-	return Respond(200, resp)
+	return Json(200, resp)
 }
 }
 
 
 func ApiError(status int, message string, err error) *NormalResponse {
 func ApiError(status int, message string, err error) *NormalResponse {

+ 5 - 0
pkg/api/datasources.go

@@ -92,6 +92,11 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
+		if err == m.ErrDataSourceNameExists {
+			c.JsonApiErr(409, err.Error(), err)
+			return
+		}
+
 		c.JsonApiErr(500, "Failed to add datasource", err)
 		c.JsonApiErr(500, "Failed to add datasource", err)
 		return
 		return
 	}
 	}

+ 2 - 1
pkg/api/metrics.go

@@ -1,6 +1,7 @@
 package api
 package api
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"net/http"
 	"net/http"
 
 
@@ -31,7 +32,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
 		})
 		})
 	}
 	}
 
 
-	resp, err := tsdb.HandleRequest(request)
+	resp, err := tsdb.HandleRequest(context.TODO(), request)
 	if err != nil {
 	if err != nil {
 		return ApiError(500, "Metric request error", err)
 		return ApiError(500, "Metric request error", err)
 	}
 	}

+ 40 - 0
pkg/bus/bus.go

@@ -1,18 +1,22 @@
 package bus
 package bus
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 	"reflect"
 	"reflect"
 )
 )
 
 
 type HandlerFunc interface{}
 type HandlerFunc interface{}
+type CtxHandlerFunc func() 
 type Msg interface{}
 type Msg interface{}
 
 
 type Bus interface {
 type Bus interface {
 	Dispatch(msg Msg) error
 	Dispatch(msg Msg) error
+	DispatchCtx(ctx context.Context, msg Msg) error
 	Publish(msg Msg) error
 	Publish(msg Msg) error
 
 
 	AddHandler(handler HandlerFunc)
 	AddHandler(handler HandlerFunc)
+	AddCtxHandler(handler HandlerFunc)
 	AddEventListener(handler HandlerFunc)
 	AddEventListener(handler HandlerFunc)
 	AddWildcardListener(handler HandlerFunc)
 	AddWildcardListener(handler HandlerFunc)
 }
 }
@@ -34,6 +38,27 @@ func New() Bus {
 	return bus
 	return bus
 }
 }
 
 
+func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
+	var msgName = reflect.TypeOf(msg).Elem().Name()
+
+	var handler = b.handlers[msgName]
+	if handler == nil {
+		return fmt.Errorf("handler not found for %s", msgName)
+	}
+
+	var params = make([]reflect.Value, 2)
+	params[0] = reflect.ValueOf(ctx)
+	params[1] = reflect.ValueOf(msg)
+
+	ret := reflect.ValueOf(handler).Call(params)
+	err := ret[0].Interface()
+	if err == nil {
+		return nil
+	} else {
+		return err.(error)
+	}
+}
+
 func (b *InProcBus) Dispatch(msg Msg) error {
 func (b *InProcBus) Dispatch(msg Msg) error {
 	var msgName = reflect.TypeOf(msg).Elem().Name()
 	var msgName = reflect.TypeOf(msg).Elem().Name()
 
 
@@ -90,6 +115,12 @@ func (b *InProcBus) AddHandler(handler HandlerFunc) {
 	b.handlers[queryTypeName] = handler
 	b.handlers[queryTypeName] = handler
 }
 }
 
 
+func (b *InProcBus) AddCtxHandler(handler HandlerFunc) {
+	handlerType := reflect.TypeOf(handler)
+	queryTypeName := handlerType.In(1).Elem().Name()
+	b.handlers[queryTypeName] = handler
+}
+
 func (b *InProcBus) AddEventListener(handler HandlerFunc) {
 func (b *InProcBus) AddEventListener(handler HandlerFunc) {
 	handlerType := reflect.TypeOf(handler)
 	handlerType := reflect.TypeOf(handler)
 	eventName := handlerType.In(0).Elem().Name()
 	eventName := handlerType.In(0).Elem().Name()
@@ -105,6 +136,11 @@ func AddHandler(implName string, handler HandlerFunc) {
 	globalBus.AddHandler(handler)
 	globalBus.AddHandler(handler)
 }
 }
 
 
+// Package level functions
+func AddCtxHandler(implName string, handler HandlerFunc) {
+	globalBus.AddCtxHandler(handler)
+}
+
 // Package level functions
 // Package level functions
 func AddEventListener(handler HandlerFunc) {
 func AddEventListener(handler HandlerFunc) {
 	globalBus.AddEventListener(handler)
 	globalBus.AddEventListener(handler)
@@ -118,6 +154,10 @@ func Dispatch(msg Msg) error {
 	return globalBus.Dispatch(msg)
 	return globalBus.Dispatch(msg)
 }
 }
 
 
+func DispatchCtx(ctx context.Context, msg Msg) error {
+	return globalBus.DispatchCtx(ctx, msg)
+}
+
 func Publish(msg Msg) error {
 func Publish(msg Msg) error {
 	return globalBus.Publish(msg)
 	return globalBus.Publish(msg)
 }
 }

+ 6 - 60
pkg/cmd/grafana-server/main.go

@@ -1,7 +1,6 @@
 package main
 package main
 
 
 import (
 import (
-	"context"
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
@@ -13,21 +12,11 @@ import (
 	"syscall"
 	"syscall"
 	"time"
 	"time"
 
 
-	"golang.org/x/sync/errgroup"
-
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
-	"github.com/grafana/grafana/pkg/login"
-	"github.com/grafana/grafana/pkg/metrics"
-	"github.com/grafana/grafana/pkg/plugins"
-	"github.com/grafana/grafana/pkg/services/cleanup"
-	"github.com/grafana/grafana/pkg/services/eventpublisher"
-	"github.com/grafana/grafana/pkg/services/notifications"
-	"github.com/grafana/grafana/pkg/services/search"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/sqlstore"
 	"github.com/grafana/grafana/pkg/services/sqlstore"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
-	"github.com/grafana/grafana/pkg/social"
 
 
-	"github.com/grafana/grafana/pkg/services/alerting"
 	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
 	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
 	_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
 	_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
 	_ "github.com/grafana/grafana/pkg/tsdb/graphite"
 	_ "github.com/grafana/grafana/pkg/tsdb/graphite"
@@ -66,41 +55,8 @@ func main() {
 	setting.BuildCommit = commit
 	setting.BuildCommit = commit
 	setting.BuildStamp = buildstampInt64
 	setting.BuildStamp = buildstampInt64
 
 
-	appContext, shutdownFn := context.WithCancel(context.Background())
-	grafanaGroup, appContext := errgroup.WithContext(appContext)
-
-	go listenToSystemSignals(shutdownFn, grafanaGroup)
-
-	flag.Parse()
-	writePIDFile()
-
-	initRuntime()
-	initSql()
-	metrics.Init()
-	search.Init()
-	login.Init()
-	social.NewOAuthService()
-	eventpublisher.Init()
-	plugins.Init()
-
-	// init alerting
-	if setting.AlertingEnabled {
-		engine := alerting.NewEngine()
-		grafanaGroup.Go(func() error { return engine.Run(appContext) })
-	}
-
-	// cleanup service
-	cleanUpService := cleanup.NewCleanUpService()
-	grafanaGroup.Go(func() error { return cleanUpService.Run(appContext) })
-
-	if err := notifications.Init(); err != nil {
-		log.Fatal(3, "Notification service failed to initialize", err)
-	}
-
-	exitCode := StartServer()
-
-	grafanaGroup.Wait()
-	exitChan <- exitCode
+	server := NewGrafanaServer()
+	server.Start()
 }
 }
 
 
 func initRuntime() {
 func initRuntime() {
@@ -143,7 +99,7 @@ func writePIDFile() {
 	}
 	}
 }
 }
 
 
-func listenToSystemSignals(cancel context.CancelFunc, grafanaGroup *errgroup.Group) {
+func listenToSystemSignals(server models.GrafanaServer) {
 	signalChan := make(chan os.Signal, 1)
 	signalChan := make(chan os.Signal, 1)
 	code := 0
 	code := 0
 
 
@@ -151,18 +107,8 @@ func listenToSystemSignals(cancel context.CancelFunc, grafanaGroup *errgroup.Gro
 
 
 	select {
 	select {
 	case sig := <-signalChan:
 	case sig := <-signalChan:
-		log.Info2("Received system signal. Shutting down", "signal", sig)
+		server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
 	case code = <-exitChan:
 	case code = <-exitChan:
-		switch code {
-		case 0:
-			log.Info("Shutting down")
-		default:
-			log.Warn("Shutting down")
-		}
+		server.Shutdown(code, "startup error")
 	}
 	}
-
-	cancel()
-	grafanaGroup.Wait()
-	log.Close()
-	os.Exit(code)
 }
 }

+ 150 - 0
pkg/cmd/grafana-server/server.go

@@ -0,0 +1,150 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"os"
+	"time"
+
+	"gopkg.in/macaron.v1"
+
+	"golang.org/x/sync/errgroup"
+
+	"github.com/grafana/grafana/pkg/api"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/login"
+	"github.com/grafana/grafana/pkg/metrics"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	"github.com/grafana/grafana/pkg/services/cleanup"
+	"github.com/grafana/grafana/pkg/services/eventpublisher"
+	"github.com/grafana/grafana/pkg/services/notifications"
+	"github.com/grafana/grafana/pkg/services/search"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/social"
+)
+
+func NewGrafanaServer() models.GrafanaServer {
+	rootCtx, shutdownFn := context.WithCancel(context.Background())
+	childRoutines, childCtx := errgroup.WithContext(rootCtx)
+
+	return &GrafanaServerImpl{
+		context:       childCtx,
+		shutdownFn:    shutdownFn,
+		childRoutines: childRoutines,
+		log:           log.New("server"),
+	}
+}
+
+type GrafanaServerImpl struct {
+	context       context.Context
+	shutdownFn    context.CancelFunc
+	childRoutines *errgroup.Group
+	log           log.Logger
+}
+
+func (g *GrafanaServerImpl) Start() {
+	go listenToSystemSignals(g)
+
+	writePIDFile()
+	initRuntime()
+	initSql()
+	metrics.Init()
+	search.Init()
+	login.Init()
+	social.NewOAuthService()
+	eventpublisher.Init()
+	plugins.Init()
+
+	// init alerting
+	if setting.AlertingEnabled {
+		engine := alerting.NewEngine()
+		g.childRoutines.Go(func() error { return engine.Run(g.context) })
+	}
+
+	// cleanup service
+	cleanUpService := cleanup.NewCleanUpService()
+	g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
+
+	if err := notifications.Init(); err != nil {
+		g.log.Error("Notification service failed to initialize", "erro", err)
+		g.Shutdown(1, "Startup failed")
+		return
+	}
+
+	g.startHttpServer()
+}
+
+func (g *GrafanaServerImpl) startHttpServer() {
+	logger = log.New("http.server")
+
+	var err error
+	m := newMacaron()
+	api.Register(m)
+
+	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
+	g.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
+
+	switch setting.Protocol {
+	case setting.HTTP:
+		err = http.ListenAndServe(listenAddr, m)
+	case setting.HTTPS:
+		err = ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
+	default:
+		g.log.Error("Invalid protocol", "protocol", setting.Protocol)
+		g.Shutdown(1, "Startup failed")
+	}
+
+	if err != nil {
+		g.log.Error("Fail to start server", "error", err)
+		g.Shutdown(1, "Startup failed")
+		return
+	}
+}
+
+func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
+	g.log.Info("Shutdown started", "code", code, "reason", reason)
+
+	g.shutdownFn()
+	err := g.childRoutines.Wait()
+
+	g.log.Info("Shutdown completed", "reason", err)
+	log.Close()
+	os.Exit(code)
+}
+
+func ListenAndServeTLS(listenAddr, certfile, keyfile string, m *macaron.Macaron) error {
+	if certfile == "" {
+		return fmt.Errorf("cert_file cannot be empty when using HTTPS")
+	}
+
+	if keyfile == "" {
+		return fmt.Errorf("cert_key cannot be empty when using HTTPS")
+	}
+
+	if _, err := os.Stat(setting.CertFile); os.IsNotExist(err) {
+		return fmt.Errorf(`Cannot find SSL cert_file at %v`, setting.CertFile)
+	}
+
+	if _, err := os.Stat(setting.KeyFile); os.IsNotExist(err) {
+		return fmt.Errorf(`Cannot find SSL key_file at %v`, setting.KeyFile)
+	}
+
+	return http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
+}
+
+// implement context.Context
+func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) {
+	return g.context.Deadline()
+}
+func (g *GrafanaServerImpl) Done() <-chan struct{} {
+	return g.context.Done()
+}
+func (g *GrafanaServerImpl) Err() error {
+	return g.context.Err()
+}
+func (g *GrafanaServerImpl) Value(key interface{}) interface{} {
+	return g.context.Value(key)
+}

+ 15 - 0
pkg/models/alert.go

@@ -135,3 +135,18 @@ type GetAlertByIdQuery struct {
 
 
 	Result *Alert
 	Result *Alert
 }
 }
+
+type GetAlertStatesForDashboardQuery struct {
+	OrgId       int64
+	DashboardId int64
+
+	Result []*AlertStateInfoDTO
+}
+
+type AlertStateInfoDTO struct {
+	Id           int64          `json:"id"`
+	DashboardId  int64          `json:"dashboardId"`
+	PanelId      int64          `json:"panelId"`
+	State        AlertStateType `json:"state"`
+	NewStateDate time.Time      `json:"newStateDate"`
+}

+ 2 - 1
pkg/models/datasource.go

@@ -22,7 +22,8 @@ const (
 
 
 // Typed errors
 // Typed errors
 var (
 var (
-	ErrDataSourceNotFound = errors.New("Data source not found")
+	ErrDataSourceNotFound   = errors.New("Data source not found")
+	ErrDataSourceNameExists = errors.New("Data source with same name already exists")
 )
 )
 
 
 type DsAccess string
 type DsAccess string

+ 11 - 0
pkg/models/notifications.go

@@ -12,6 +12,10 @@ type SendEmailCommand struct {
 	Info     string
 	Info     string
 }
 }
 
 
+type SendEmailCommandSync struct {
+	SendEmailCommand
+}
+
 type SendWebhook struct {
 type SendWebhook struct {
 	Url      string
 	Url      string
 	User     string
 	User     string
@@ -19,6 +23,13 @@ type SendWebhook struct {
 	Body     string
 	Body     string
 }
 }
 
 
+type SendWebhookSync struct {
+	Url      string
+	User     string
+	Password string
+	Body     string
+}
+
 type SendResetPasswordEmailCommand struct {
 type SendResetPasswordEmailCommand struct {
 	User *User
 	User *User
 }
 }

+ 10 - 0
pkg/models/server.go

@@ -0,0 +1,10 @@
+package models
+
+import "context"
+
+type GrafanaServer interface {
+	context.Context
+
+	Start()
+	Shutdown(code int, reason string)
+}

+ 1 - 1
pkg/services/alerting/conditions/query.go

@@ -82,7 +82,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *
 	req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
 	req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
 	result := make(tsdb.TimeSeriesSlice, 0)
 	result := make(tsdb.TimeSeriesSlice, 0)
 
 
-	resp, err := c.HandleRequest(req)
+	resp, err := c.HandleRequest(context.Context, req)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err)
 		return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err)
 	}
 	}

+ 2 - 1
pkg/services/alerting/conditions/query_test.go

@@ -1,6 +1,7 @@
 package conditions
 package conditions
 
 
 import (
 import (
+	"context"
 	"testing"
 	"testing"
 
 
 	null "gopkg.in/guregu/null.v3"
 	null "gopkg.in/guregu/null.v3"
@@ -137,7 +138,7 @@ func (ctx *queryConditionTestContext) exec() {
 
 
 	ctx.condition = condition
 	ctx.condition = condition
 
 
-	condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) {
+	condition.HandleRequest = func(context context.Context, req *tsdb.Request) (*tsdb.Response, error) {
 		return &tsdb.Response{
 		return &tsdb.Response{
 			Results: map[string]*tsdb.QueryResult{
 			Results: map[string]*tsdb.QueryResult{
 				"A": {Series: ctx.series},
 				"A": {Series: ctx.series},

+ 35 - 55
pkg/services/alerting/engine.go

@@ -11,7 +11,6 @@ import (
 
 
 type Engine struct {
 type Engine struct {
 	execQueue     chan *Job
 	execQueue     chan *Job
-	resultQueue   chan *EvalContext
 	clock         clock.Clock
 	clock         clock.Clock
 	ticker        *Ticker
 	ticker        *Ticker
 	scheduler     Scheduler
 	scheduler     Scheduler
@@ -25,7 +24,6 @@ func NewEngine() *Engine {
 	e := &Engine{
 	e := &Engine{
 		ticker:        NewTicker(time.Now(), time.Second*0, clock.New()),
 		ticker:        NewTicker(time.Now(), time.Second*0, clock.New()),
 		execQueue:     make(chan *Job, 1000),
 		execQueue:     make(chan *Job, 1000),
-		resultQueue:   make(chan *EvalContext, 1000),
 		scheduler:     NewScheduler(),
 		scheduler:     NewScheduler(),
 		evalHandler:   NewEvalHandler(),
 		evalHandler:   NewEvalHandler(),
 		ruleReader:    NewRuleReader(),
 		ruleReader:    NewRuleReader(),
@@ -39,23 +37,17 @@ func NewEngine() *Engine {
 func (e *Engine) Run(ctx context.Context) error {
 func (e *Engine) Run(ctx context.Context) error {
 	e.log.Info("Initializing Alerting")
 	e.log.Info("Initializing Alerting")
 
 
-	g, ctx := errgroup.WithContext(ctx)
+	alertGroup, ctx := errgroup.WithContext(ctx)
 
 
-	g.Go(func() error { return e.alertingTicker(ctx) })
-	g.Go(func() error { return e.execDispatcher(ctx) })
-	g.Go(func() error { return e.resultDispatcher(ctx) })
+	alertGroup.Go(func() error { return e.alertingTicker(ctx) })
+	alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
 
 
-	err := g.Wait()
+	err := alertGroup.Wait()
 
 
 	e.log.Info("Stopped Alerting", "reason", err)
 	e.log.Info("Stopped Alerting", "reason", err)
 	return err
 	return err
 }
 }
 
 
-func (e *Engine) Stop() {
-	close(e.execQueue)
-	close(e.resultQueue)
-}
-
 func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
 func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
 	defer func() {
 	defer func() {
 		if err := recover(); err != nil {
 		if err := recover(); err != nil {
@@ -81,70 +73,58 @@ func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
 	}
 	}
 }
 }
 
 
-func (e *Engine) execDispatcher(grafanaCtx context.Context) error {
+func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
+	dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
+
 	for {
 	for {
 		select {
 		select {
 		case <-grafanaCtx.Done():
 		case <-grafanaCtx.Done():
-			close(e.resultQueue)
-			return grafanaCtx.Err()
+			return dispatcherGroup.Wait()
 		case job := <-e.execQueue:
 		case job := <-e.execQueue:
-			go e.executeJob(grafanaCtx, job)
+			dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) })
 		}
 		}
 	}
 	}
 }
 }
 
 
-func (e *Engine) executeJob(grafanaCtx context.Context, job *Job) error {
+var (
+	unfinishedWorkTimeout time.Duration = time.Second * 5
+	alertTimeout          time.Duration = time.Second * 30
+)
+
+func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
 	defer func() {
 	defer func() {
 		if err := recover(); err != nil {
 		if err := recover(); err != nil {
-			e.log.Error("Execute Alert Panic", "error", err, "stack", log.Stack(1))
+			e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
 		}
 		}
 	}()
 	}()
 
 
-	done := make(chan *EvalContext, 1)
+	alertCtx, cancelFn := context.WithTimeout(context.TODO(), alertTimeout)
+
+	job.Running = true
+	evalContext := NewEvalContext(alertCtx, job.Rule)
+
+	done := make(chan struct{})
+
 	go func() {
 	go func() {
-		job.Running = true
-		context := NewEvalContext(job.Rule)
-		e.evalHandler.Eval(context)
-		job.Running = false
-		done <- context
+		e.evalHandler.Eval(evalContext)
+		e.resultHandler.Handle(evalContext)
 		close(done)
 		close(done)
 	}()
 	}()
 
 
+	var err error = nil
 	select {
 	select {
-
 	case <-grafanaCtx.Done():
 	case <-grafanaCtx.Done():
-		return grafanaCtx.Err()
-	case evalContext := <-done:
-		e.resultQueue <- evalContext
-	}
-
-	return nil
-}
-
-func (e *Engine) resultDispatcher(grafanaCtx context.Context) error {
-	for {
 		select {
 		select {
-		case <-grafanaCtx.Done():
-			//handle all responses before shutting down.
-			for result := range e.resultQueue {
-				e.handleResponse(result)
-			}
-
-			return grafanaCtx.Err()
-		case result := <-e.resultQueue:
-			e.handleResponse(result)
+		case <-time.After(unfinishedWorkTimeout):
+			cancelFn()
+			err = grafanaCtx.Err()
+		case <-done:
 		}
 		}
+	case <-done:
 	}
 	}
-}
 
 
-func (e *Engine) handleResponse(result *EvalContext) {
-	defer func() {
-		if err := recover(); err != nil {
-			e.log.Error("Panic in resultDispatcher", "error", err, "stack", log.Stack(1))
-		}
-	}()
-
-	e.log.Info("rule", "nil", result.Rule == nil)
-	e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing)
-	e.resultHandler.Handle(result)
+	e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing)
+	job.Running = false
+	cancelFn()
+	return err
 }
 }

+ 21 - 5
pkg/services/alerting/eval_context.go

@@ -1,6 +1,7 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 	"time"
 	"time"
 
 
@@ -20,14 +21,30 @@ type EvalContext struct {
 	StartTime       time.Time
 	StartTime       time.Time
 	EndTime         time.Time
 	EndTime         time.Time
 	Rule            *Rule
 	Rule            *Rule
-	DoneChan        chan bool
-	CancelChan      chan bool
 	log             log.Logger
 	log             log.Logger
 	dashboardSlug   string
 	dashboardSlug   string
 	ImagePublicUrl  string
 	ImagePublicUrl  string
 	ImageOnDiskPath string
 	ImageOnDiskPath string
 	NoDataFound     bool
 	NoDataFound     bool
 	RetryCount      int
 	RetryCount      int
+
+	Context context.Context
+}
+
+func (evalContext *EvalContext) Deadline() (deadline time.Time, ok bool) {
+	return evalContext.Deadline()
+}
+
+func (evalContext *EvalContext) Done() <-chan struct{} {
+	return evalContext.Context.Done()
+}
+
+func (evalContext *EvalContext) Err() error {
+	return evalContext.Context.Err()
+}
+
+func (evalContext *EvalContext) Value(key interface{}) interface{} {
+	return evalContext.Context.Value(key)
 }
 }
 
 
 type StateDescription struct {
 type StateDescription struct {
@@ -94,14 +111,13 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
 	}
 	}
 }
 }
 
 
-func NewEvalContext(rule *Rule) *EvalContext {
+func NewEvalContext(alertCtx context.Context, rule *Rule) *EvalContext {
 	return &EvalContext{
 	return &EvalContext{
+		Context:     alertCtx,
 		StartTime:   time.Now(),
 		StartTime:   time.Now(),
 		Rule:        rule,
 		Rule:        rule,
 		Logs:        make([]*ResultLogEntry, 0),
 		Logs:        make([]*ResultLogEntry, 0),
 		EvalMatches: make([]*EvalMatch, 0),
 		EvalMatches: make([]*EvalMatch, 0),
-		DoneChan:    make(chan bool, 1),
-		CancelChan:  make(chan bool, 1),
 		log:         log.New("alerting.evalContext"),
 		log:         log.New("alerting.evalContext"),
 		RetryCount:  0,
 		RetryCount:  0,
 	}
 	}

+ 1 - 45
pkg/services/alerting/eval_handler.go

@@ -1,17 +1,12 @@
 package alerting
 package alerting
 
 
 import (
 import (
-	"fmt"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 )
 )
 
 
-var (
-	MaxRetries int = 1
-)
-
 type DefaultEvalHandler struct {
 type DefaultEvalHandler struct {
 	log             log.Logger
 	log             log.Logger
 	alertJobTimeout time.Duration
 	alertJobTimeout time.Duration
@@ -20,49 +15,11 @@ type DefaultEvalHandler struct {
 func NewEvalHandler() *DefaultEvalHandler {
 func NewEvalHandler() *DefaultEvalHandler {
 	return &DefaultEvalHandler{
 	return &DefaultEvalHandler{
 		log:             log.New("alerting.evalHandler"),
 		log:             log.New("alerting.evalHandler"),
-		alertJobTimeout: time.Second * 15,
+		alertJobTimeout: time.Second * 5,
 	}
 	}
 }
 }
 
 
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
-	go e.eval(context)
-
-	select {
-	case <-time.After(e.alertJobTimeout):
-		context.Error = fmt.Errorf("Execution timed out after %v", e.alertJobTimeout)
-		context.EndTime = time.Now()
-		e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id, "timeout setting", e.alertJobTimeout)
-		e.retry(context)
-	case <-context.DoneChan:
-		e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
-
-		if context.Error != nil {
-			e.retry(context)
-		}
-	}
-}
-
-func (e *DefaultEvalHandler) retry(context *EvalContext) {
-	e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id)
-
-	if context.RetryCount < MaxRetries {
-		context.DoneChan = make(chan bool, 1)
-		context.CancelChan = make(chan bool, 1)
-		context.RetryCount++
-		e.Eval(context)
-	}
-}
-
-func (e *DefaultEvalHandler) eval(context *EvalContext) {
-	defer func() {
-		if err := recover(); err != nil {
-			e.log.Error("Alerting rule eval panic", "error", err, "stack", log.Stack(1))
-			if panicErr, ok := err.(error); ok {
-				context.Error = panicErr
-			}
-		}
-	}()
-
 	for _, condition := range context.Rule.Conditions {
 	for _, condition := range context.Rule.Conditions {
 		condition.Eval(context)
 		condition.Eval(context)
 
 
@@ -80,5 +37,4 @@ func (e *DefaultEvalHandler) eval(context *EvalContext) {
 	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)
-	context.DoneChan <- true
 }
 }

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

@@ -1,6 +1,7 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"context"
 	"testing"
 	"testing"
 
 
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
@@ -19,25 +20,25 @@ func TestAlertingExecutor(t *testing.T) {
 		handler := NewEvalHandler()
 		handler := NewEvalHandler()
 
 
 		Convey("Show return triggered with single passing condition", func() {
 		Convey("Show return triggered with single passing condition", func() {
-			context := NewEvalContext(&Rule{
+			context := NewEvalContext(context.TODO(), &Rule{
 				Conditions: []Condition{&conditionStub{
 				Conditions: []Condition{&conditionStub{
 					firing: true,
 					firing: true,
 				}},
 				}},
 			})
 			})
 
 
-			handler.eval(context)
+			handler.Eval(context)
 			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 condition", func() {
-			context := NewEvalContext(&Rule{
+			context := NewEvalContext(context.TODO(), &Rule{
 				Conditions: []Condition{
 				Conditions: []Condition{
 					&conditionStub{firing: true},
 					&conditionStub{firing: true},
 					&conditionStub{firing: false},
 					&conditionStub{firing: false},
 				},
 				},
 			})
 			})
 
 
-			handler.eval(context)
+			handler.Eval(context)
 			So(context.Firing, ShouldEqual, false)
 			So(context.Firing, ShouldEqual, false)
 		})
 		})
 	})
 	})

+ 2 - 2
pkg/services/alerting/extractor.go

@@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
 				continue
 				continue
 			}
 			}
 
 
+			// backward compatability check, can be removed later
 			enabled, hasEnabled := jsonAlert.CheckGet("enabled")
 			enabled, hasEnabled := jsonAlert.CheckGet("enabled")
-
-			if !hasEnabled || !enabled.MustBool() {
+			if hasEnabled && enabled.MustBool() == false {
 				continue
 				continue
 			}
 			}
 
 

+ 0 - 2
pkg/services/alerting/extractor_test.go

@@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) {
               "name": "name1",
               "name": "name1",
               "message": "desc1",
               "message": "desc1",
               "handler": 1,
               "handler": 1,
-              "enabled": true,
               "frequency": "60s",
               "frequency": "60s",
               "conditions": [
               "conditions": [
               {
               {
@@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) {
               "name": "name2",
               "name": "name2",
               "message": "desc2",
               "message": "desc2",
               "handler": 0,
               "handler": 0,
-              "enabled": true,
               "frequency": "60s",
               "frequency": "60s",
               "severity": "warning",
               "severity": "warning",
               "conditions": [
               "conditions": [

+ 3 - 5
pkg/services/alerting/interfaces.go

@@ -1,11 +1,9 @@
 package alerting
 package alerting
 
 
-import (
-	"time"
-)
+import "time"
 
 
 type EvalHandler interface {
 type EvalHandler interface {
-	Eval(context *EvalContext)
+	Eval(evalContext *EvalContext)
 }
 }
 
 
 type Scheduler interface {
 type Scheduler interface {
@@ -14,7 +12,7 @@ type Scheduler interface {
 }
 }
 
 
 type Notifier interface {
 type Notifier interface {
-	Notify(alertResult *EvalContext)
+	Notify(evalContext *EvalContext) error
 	GetType() string
 	GetType() string
 	NeedsImage() bool
 	NeedsImage() bool
 	PassesFilter(rule *Rule) bool
 	PassesFilter(rule *Rule) bool

+ 13 - 7
pkg/services/alerting/notifier.go

@@ -4,6 +4,8 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 
 
+	"golang.org/x/sync/errgroup"
+
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/imguploader"
 	"github.com/grafana/grafana/pkg/components/imguploader"
 	"github.com/grafana/grafana/pkg/components/renderer"
 	"github.com/grafana/grafana/pkg/components/renderer"
@@ -33,32 +35,36 @@ func (n *RootNotifier) PassesFilter(rule *Rule) bool {
 	return false
 	return false
 }
 }
 
 
-func (n *RootNotifier) Notify(context *EvalContext) {
+func (n *RootNotifier) Notify(context *EvalContext) error {
 	n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
 	n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
 
 
 	notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
 	notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
 	if err != nil {
 	if err != nil {
-		n.log.Error("Failed to read notifications", "error", err)
-		return
+		return err
 	}
 	}
 
 
 	if len(notifiers) == 0 {
 	if len(notifiers) == 0 {
-		return
+		return nil
 	}
 	}
 
 
 	err = n.uploadImage(context)
 	err = n.uploadImage(context)
 	if err != nil {
 	if err != nil {
 		n.log.Error("Failed to upload alert panel image", "error", err)
 		n.log.Error("Failed to upload alert panel image", "error", err)
+		return err
 	}
 	}
 
 
-	n.sendNotifications(notifiers, context)
+	return n.sendNotifications(context, notifiers)
 }
 }
 
 
-func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalContext) {
+func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notifier) error {
+	g, _ := errgroup.WithContext(context.Context)
+
 	for _, notifier := range notifiers {
 	for _, notifier := range notifiers {
 		n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
 		n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
-		go notifier.Notify(context)
+		g.Go(func() error { return notifier.Notify(context) })
 	}
 	}
+
+	return g.Wait()
 }
 }
 
 
 func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
 func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {

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

@@ -22,7 +22,7 @@ func (fn *FakeNotifier) NeedsImage() bool {
 	return true
 	return true
 }
 }
 
 
-func (fn *FakeNotifier) Notify(alertResult *EvalContext) {}
+func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
 
 
 func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
 func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
 	return fn.FakeMatchResult
 	return fn.FakeMatchResult

+ 23 - 17
pkg/services/alerting/notifiers/email.go

@@ -35,33 +35,39 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 	}, nil
 	}, nil
 }
 }
 
 
-func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
+func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Sending alert notification to", "addresses", this.Addresses)
 	this.log.Info("Sending alert notification to", "addresses", this.Addresses)
 	metrics.M_Alerting_Notification_Sent_Email.Inc(1)
 	metrics.M_Alerting_Notification_Sent_Email.Inc(1)
 
 
-	ruleUrl, err := context.GetRuleUrl()
+	ruleUrl, err := evalContext.GetRuleUrl()
 	if err != nil {
 	if err != nil {
 		this.log.Error("Failed get rule link", "error", err)
 		this.log.Error("Failed get rule link", "error", err)
-		return
+		return err
 	}
 	}
 
 
-	cmd := &m.SendEmailCommand{
-		Data: map[string]interface{}{
-			"Title":        context.GetNotificationTitle(),
-			"State":        context.Rule.State,
-			"Name":         context.Rule.Name,
-			"StateModel":   context.GetStateModel(),
-			"Message":      context.Rule.Message,
-			"RuleUrl":      ruleUrl,
-			"ImageLink":    context.ImagePublicUrl,
-			"AlertPageUrl": setting.AppUrl + "alerting",
-			"EvalMatches":  context.EvalMatches,
+	cmd := &m.SendEmailCommandSync{
+		SendEmailCommand: m.SendEmailCommand{
+			Data: map[string]interface{}{
+				"Title":        evalContext.GetNotificationTitle(),
+				"State":        evalContext.Rule.State,
+				"Name":         evalContext.Rule.Name,
+				"StateModel":   evalContext.GetStateModel(),
+				"Message":      evalContext.Rule.Message,
+				"RuleUrl":      ruleUrl,
+				"ImageLink":    evalContext.ImagePublicUrl,
+				"AlertPageUrl": setting.AppUrl + "alerting",
+				"EvalMatches":  evalContext.EvalMatches,
+			},
+			To:       this.Addresses,
+			Template: "alert_notification.html",
 		},
 		},
-		To:       this.Addresses,
-		Template: "alert_notification.html",
 	}
 	}
 
 
-	if err := bus.Dispatch(cmd); err != nil {
+	err = bus.DispatchCtx(evalContext, cmd)
+
+	if err != nil {
 		this.log.Error("Failed to send alert notification email", "error", err)
 		this.log.Error("Failed to send alert notification email", "error", err)
 	}
 	}
+	return nil
+
 }
 }

+ 16 - 19
pkg/services/alerting/notifiers/slack.go

@@ -35,19 +35,19 @@ type SlackNotifier struct {
 	log log.Logger
 	log log.Logger
 }
 }
 
 
-func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
-	this.log.Info("Executing slack notification", "ruleId", context.Rule.Id, "notification", this.Name)
+func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
+	this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
 	metrics.M_Alerting_Notification_Sent_Slack.Inc(1)
 	metrics.M_Alerting_Notification_Sent_Slack.Inc(1)
 
 
-	ruleUrl, err := context.GetRuleUrl()
+	ruleUrl, err := evalContext.GetRuleUrl()
 	if err != nil {
 	if err != nil {
 		this.log.Error("Failed get rule link", "error", err)
 		this.log.Error("Failed get rule link", "error", err)
-		return
+		return err
 	}
 	}
 
 
 	fields := make([]map[string]interface{}, 0)
 	fields := make([]map[string]interface{}, 0)
 	fieldLimitCount := 4
 	fieldLimitCount := 4
-	for index, evt := range context.EvalMatches {
+	for index, evt := range evalContext.EvalMatches {
 		fields = append(fields, map[string]interface{}{
 		fields = append(fields, map[string]interface{}{
 			"title": evt.Metric,
 			"title": evt.Metric,
 			"value": evt.Value,
 			"value": evt.Value,
@@ -58,44 +58,41 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
 		}
 		}
 	}
 	}
 
 
-	if context.Error != nil {
+	if evalContext.Error != nil {
 		fields = append(fields, map[string]interface{}{
 		fields = append(fields, map[string]interface{}{
 			"title": "Error message",
 			"title": "Error message",
-			"value": context.Error.Error(),
+			"value": evalContext.Error.Error(),
 			"short": false,
 			"short": false,
 		})
 		})
 	}
 	}
 
 
 	message := ""
 	message := ""
-	if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
-		message = context.Rule.Message
+	if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
+		message = evalContext.Rule.Message
 	}
 	}
 
 
 	body := map[string]interface{}{
 	body := map[string]interface{}{
 		"attachments": []map[string]interface{}{
 		"attachments": []map[string]interface{}{
 			{
 			{
-				"color":       context.GetStateModel().Color,
-				"title":       context.GetNotificationTitle(),
+				"color":       evalContext.GetStateModel().Color,
+				"title":       evalContext.GetNotificationTitle(),
 				"title_link":  ruleUrl,
 				"title_link":  ruleUrl,
 				"text":        message,
 				"text":        message,
 				"fields":      fields,
 				"fields":      fields,
-				"image_url":   context.ImagePublicUrl,
+				"image_url":   evalContext.ImagePublicUrl,
 				"footer":      "Grafana v" + setting.BuildVersion,
 				"footer":      "Grafana v" + setting.BuildVersion,
 				"footer_icon": "http://grafana.org/assets/img/fav32.png",
 				"footer_icon": "http://grafana.org/assets/img/fav32.png",
 				"ts":          time.Now().Unix(),
 				"ts":          time.Now().Unix(),
-				//"pretext":     "Optional text that appears above the attachment block",
-				// "author_name": "Bobby Tables",
-				// "author_link": "http://flickr.com/bobby/",
-				// "author_icon": "http://flickr.com/icons/bobby.jpg",
-				// "thumb_url":   "http://example.com/path/to/thumb.png",
 			},
 			},
 		},
 		},
 	}
 	}
 
 
 	data, _ := json.Marshal(&body)
 	data, _ := json.Marshal(&body)
-	cmd := &m.SendWebhook{Url: this.Url, Body: string(data)}
+	cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)}
 
 
-	if err := bus.Dispatch(cmd); err != nil {
+	if err := bus.DispatchCtx(evalContext, cmd); err != nil {
 		this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name)
 		this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name)
 	}
 	}
+
+	return nil
 }
 }

+ 13 - 11
pkg/services/alerting/notifiers/webhook.go

@@ -36,36 +36,38 @@ type WebhookNotifier struct {
 	log      log.Logger
 	log      log.Logger
 }
 }
 
 
-func (this *WebhookNotifier) Notify(context *alerting.EvalContext) {
+func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
 	this.log.Info("Sending webhook")
 	this.log.Info("Sending webhook")
 	metrics.M_Alerting_Notification_Sent_Webhook.Inc(1)
 	metrics.M_Alerting_Notification_Sent_Webhook.Inc(1)
 
 
 	bodyJSON := simplejson.New()
 	bodyJSON := simplejson.New()
-	bodyJSON.Set("title", context.GetNotificationTitle())
-	bodyJSON.Set("ruleId", context.Rule.Id)
-	bodyJSON.Set("ruleName", context.Rule.Name)
-	bodyJSON.Set("state", context.Rule.State)
-	bodyJSON.Set("evalMatches", context.EvalMatches)
+	bodyJSON.Set("title", evalContext.GetNotificationTitle())
+	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 := context.GetRuleUrl()
+	ruleUrl, err := evalContext.GetRuleUrl()
 	if err == nil {
 	if err == nil {
 		bodyJSON.Set("rule_url", ruleUrl)
 		bodyJSON.Set("rule_url", ruleUrl)
 	}
 	}
 
 
-	if context.ImagePublicUrl != "" {
-		bodyJSON.Set("image_url", context.ImagePublicUrl)
+	if evalContext.ImagePublicUrl != "" {
+		bodyJSON.Set("image_url", evalContext.ImagePublicUrl)
 	}
 	}
 
 
 	body, _ := bodyJSON.MarshalJSON()
 	body, _ := bodyJSON.MarshalJSON()
 
 
-	cmd := &m.SendWebhook{
+	cmd := &m.SendWebhookSync{
 		Url:      this.Url,
 		Url:      this.Url,
 		User:     this.User,
 		User:     this.User,
 		Password: this.Password,
 		Password: this.Password,
 		Body:     string(body),
 		Body:     string(body),
 	}
 	}
 
 
-	if err := bus.Dispatch(cmd); err != nil {
+	if err := bus.DispatchCtx(evalContext, cmd); err != nil {
 		this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
 		this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
 	}
 	}
+
+	return nil
 }
 }

+ 29 - 27
pkg/services/alerting/result_handler.go

@@ -12,7 +12,7 @@ import (
 )
 )
 
 
 type ResultHandler interface {
 type ResultHandler interface {
-	Handle(ctx *EvalContext)
+	Handle(evalContext *EvalContext) error
 }
 }
 
 
 type DefaultResultHandler struct {
 type DefaultResultHandler struct {
@@ -27,36 +27,36 @@ func NewResultHandler() *DefaultResultHandler {
 	}
 	}
 }
 }
 
 
-func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
-	oldState := ctx.Rule.State
+func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
+	oldState := evalContext.Rule.State
 
 
 	exeuctionError := ""
 	exeuctionError := ""
 	annotationData := simplejson.New()
 	annotationData := simplejson.New()
-	if ctx.Error != nil {
-		handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
-		ctx.Rule.State = m.AlertStateExecError
-		exeuctionError = ctx.Error.Error()
+	if evalContext.Error != nil {
+		handler.log.Error("Alert Rule Result Error", "ruleId", evalContext.Rule.Id, "error", evalContext.Error)
+		evalContext.Rule.State = m.AlertStateExecError
+		exeuctionError = evalContext.Error.Error()
 		annotationData.Set("errorMessage", exeuctionError)
 		annotationData.Set("errorMessage", exeuctionError)
-	} else if ctx.Firing {
-		ctx.Rule.State = m.AlertStateAlerting
-		annotationData = simplejson.NewFromAny(ctx.EvalMatches)
+	} else if evalContext.Firing {
+		evalContext.Rule.State = m.AlertStateAlerting
+		annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
 	} else {
 	} else {
 		// handle no data case
 		// handle no data case
-		if ctx.NoDataFound {
-			ctx.Rule.State = ctx.Rule.NoDataState
+		if evalContext.NoDataFound {
+			evalContext.Rule.State = evalContext.Rule.NoDataState
 		} else {
 		} else {
-			ctx.Rule.State = m.AlertStateOK
+			evalContext.Rule.State = m.AlertStateOK
 		}
 		}
 	}
 	}
 
 
-	countStateResult(ctx.Rule.State)
-	if ctx.Rule.State != oldState {
-		handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
+	countStateResult(evalContext.Rule.State)
+	if evalContext.Rule.State != oldState {
+		handler.log.Info("New state change", "alertId", evalContext.Rule.Id, "newState", evalContext.Rule.State, "oldState", oldState)
 
 
 		cmd := &m.SetAlertStateCommand{
 		cmd := &m.SetAlertStateCommand{
-			AlertId:  ctx.Rule.Id,
-			OrgId:    ctx.Rule.OrgId,
-			State:    ctx.Rule.State,
+			AlertId:  evalContext.Rule.Id,
+			OrgId:    evalContext.Rule.OrgId,
+			State:    evalContext.Rule.State,
 			Error:    exeuctionError,
 			Error:    exeuctionError,
 			EvalData: annotationData,
 			EvalData: annotationData,
 		}
 		}
@@ -67,14 +67,14 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
 
 
 		// save annotation
 		// save annotation
 		item := annotations.Item{
 		item := annotations.Item{
-			OrgId:       ctx.Rule.OrgId,
-			DashboardId: ctx.Rule.DashboardId,
-			PanelId:     ctx.Rule.PanelId,
+			OrgId:       evalContext.Rule.OrgId,
+			DashboardId: evalContext.Rule.DashboardId,
+			PanelId:     evalContext.Rule.PanelId,
 			Type:        annotations.AlertType,
 			Type:        annotations.AlertType,
-			AlertId:     ctx.Rule.Id,
-			Title:       ctx.Rule.Name,
-			Text:        ctx.GetStateModel().Text,
-			NewState:    string(ctx.Rule.State),
+			AlertId:     evalContext.Rule.Id,
+			Title:       evalContext.Rule.Name,
+			Text:        evalContext.GetStateModel().Text,
+			NewState:    string(evalContext.Rule.State),
 			PrevState:   string(oldState),
 			PrevState:   string(oldState),
 			Epoch:       time.Now().Unix(),
 			Epoch:       time.Now().Unix(),
 			Data:        annotationData,
 			Data:        annotationData,
@@ -85,8 +85,10 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
 			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(ctx)
+		handler.notifier.Notify(evalContext)
 	}
 	}
+
+	return nil
 }
 }
 
 
 func countStateResult(state m.AlertStateType) {
 func countStateResult(state m.AlertStateType) {

+ 4 - 3
pkg/services/alerting/test_notification.go

@@ -1,6 +1,8 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"context"
+
 	"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"
@@ -35,13 +37,12 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
 		return err
 		return err
 	}
 	}
 
 
-	notifier.sendNotifications([]Notifier{notifiers}, createTestEvalContext())
+	notifier.sendNotifications(createTestEvalContext(), []Notifier{notifiers})
 
 
 	return nil
 	return nil
 }
 }
 
 
 func createTestEvalContext() *EvalContext {
 func createTestEvalContext() *EvalContext {
-
 	testRule := &Rule{
 	testRule := &Rule{
 		DashboardId: 1,
 		DashboardId: 1,
 		PanelId:     1,
 		PanelId:     1,
@@ -50,7 +51,7 @@ func createTestEvalContext() *EvalContext {
 		State:       m.AlertStateAlerting,
 		State:       m.AlertStateAlerting,
 	}
 	}
 
 
-	ctx := NewEvalContext(testRule)
+	ctx := NewEvalContext(context.TODO(), testRule)
 	ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
 	ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
 
 
 	ctx.IsTestRun = true
 	ctx.IsTestRun = true

+ 2 - 1
pkg/services/alerting/test_rule.go

@@ -1,6 +1,7 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
@@ -48,7 +49,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
 func testAlertRule(rule *Rule) *EvalContext {
 func testAlertRule(rule *Rule) *EvalContext {
 	handler := NewEvalHandler()
 	handler := NewEvalHandler()
 
 
-	context := NewEvalContext(rule)
+	context := NewEvalContext(context.TODO(), rule)
 	context.IsTestRun = true
 	context.IsTestRun = true
 
 
 	handler.Eval(context)
 	handler.Eval(context)

+ 50 - 0
pkg/services/notifications/mailer.go

@@ -5,8 +5,11 @@
 package notifications
 package notifications
 
 
 import (
 import (
+	"bytes"
 	"crypto/tls"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"fmt"
+	"html/template"
 	"net"
 	"net"
 	"net/mail"
 	"net/mail"
 	"net/smtp"
 	"net/smtp"
@@ -15,6 +18,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
@@ -185,3 +189,49 @@ func buildAndSend(msg *Message) (int, error) {
 		}
 		}
 	}
 	}
 }
 }
+
+func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
+	if !setting.Smtp.Enabled {
+		return nil, errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin")
+	}
+
+	var buffer bytes.Buffer
+	var err error
+	var subjectText interface{}
+
+	data := cmd.Data
+	if data == nil {
+		data = make(map[string]interface{}, 10)
+	}
+
+	setDefaultTemplateData(data, nil)
+	err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data)
+	if err != nil {
+		return nil, err
+	}
+
+	subjectData := data["Subject"].(map[string]interface{})
+	subjectText, hasSubject := subjectData["value"]
+
+	if !hasSubject {
+		return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
+	}
+
+	subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
+	if err != nil {
+		return nil, err
+	}
+
+	var subjectBuffer bytes.Buffer
+	err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Message{
+		To:      cmd.To,
+		From:    setting.Smtp.FromAddress,
+		Subject: subjectBuffer.String(),
+		Body:    buffer.String(),
+	}, nil
+}

+ 27 - 33
pkg/services/notifications/notifications.go

@@ -1,7 +1,7 @@
 package notifications
 package notifications
 
 
 import (
 import (
-	"bytes"
+	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"html/template"
 	"html/template"
@@ -29,7 +29,10 @@ func Init() error {
 	bus.AddHandler("email", validateResetPasswordCode)
 	bus.AddHandler("email", validateResetPasswordCode)
 	bus.AddHandler("email", sendEmailCommandHandler)
 	bus.AddHandler("email", sendEmailCommandHandler)
 
 
+	bus.AddCtxHandler("email", sendEmailCommandHandlerSync)
+
 	bus.AddHandler("webhook", sendWebhook)
 	bus.AddHandler("webhook", sendWebhook)
+	bus.AddCtxHandler("webhook", SendWebhookSync)
 
 
 	bus.AddEventListener(signUpStartedHandler)
 	bus.AddEventListener(signUpStartedHandler)
 	bus.AddEventListener(signUpCompletedHandler)
 	bus.AddEventListener(signUpCompletedHandler)
@@ -56,6 +59,15 @@ func Init() error {
 	return nil
 	return nil
 }
 }
 
 
+func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
+	return sendWebRequestSync(ctx, &Webhook{
+		Url:      cmd.Url,
+		User:     cmd.User,
+		Password: cmd.Password,
+		Body:     cmd.Body,
+	})
+}
+
 func sendWebhook(cmd *m.SendWebhook) error {
 func sendWebhook(cmd *m.SendWebhook) error {
 	addToWebhookQueue(&Webhook{
 	addToWebhookQueue(&Webhook{
 		Url:      cmd.Url,
 		Url:      cmd.Url,
@@ -72,50 +84,32 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string {
 	return ""
 	return ""
 }
 }
 
 
-func sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
-	if !setting.Smtp.Enabled {
-		return errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin")
-	}
-
-	var buffer bytes.Buffer
-	var err error
-	var subjectText interface{}
-
-	data := cmd.Data
-	if data == nil {
-		data = make(map[string]interface{}, 10)
-	}
+func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error {
+	message, err := buildEmailMessage(&m.SendEmailCommand{
+		Data:     cmd.Data,
+		Info:     cmd.Info,
+		Massive:  cmd.Massive,
+		Template: cmd.Template,
+		To:       cmd.To,
+	})
 
 
-	setDefaultTemplateData(data, nil)
-	err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	subjectData := data["Subject"].(map[string]interface{})
-	subjectText, hasSubject := subjectData["value"]
+	_, err = buildAndSend(message)
 
 
-	if !hasSubject {
-		return errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
-	}
+	return err
+}
 
 
-	subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
-	if err != nil {
-		return err
-	}
+func sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
+	message, err := buildEmailMessage(cmd)
 
 
-	var subjectBuffer bytes.Buffer
-	err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	addToMailQueue(&Message{
-		To:      cmd.To,
-		From:    setting.Smtp.FromAddress,
-		Subject: subjectBuffer.String(),
-		Body:    buffer.String(),
-	})
+	addToMailQueue(message)
 
 
 	return nil
 	return nil
 }
 }

+ 1 - 2
pkg/services/notifications/notifications_test.go

@@ -3,7 +3,6 @@ package notifications
 import (
 import (
 	"testing"
 	"testing"
 
 
-	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
@@ -18,7 +17,7 @@ type testTriggeredAlert struct {
 func TestNotifications(t *testing.T) {
 func TestNotifications(t *testing.T) {
 
 
 	Convey("Given the notifications service", t, func() {
 	Convey("Given the notifications service", t, func() {
-		bus.ClearBusHandlers()
+		//bus.ClearBusHandlers()
 
 
 		setting.StaticRootPath = "../../../public/"
 		setting.StaticRootPath = "../../../public/"
 		setting.Smtp.Enabled = true
 		setting.Smtp.Enabled = true

+ 16 - 12
pkg/services/notifications/webhook.go

@@ -2,11 +2,14 @@ package notifications
 
 
 import (
 import (
 	"bytes"
 	"bytes"
+	"context"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"time"
 	"time"
 
 
+	"golang.org/x/net/context/ctxhttp"
+
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
@@ -31,7 +34,7 @@ func processWebhookQueue() {
 	for {
 	for {
 		select {
 		select {
 		case webhook := <-webhookQueue:
 		case webhook := <-webhookQueue:
-			err := sendWebRequest(webhook)
+			err := sendWebRequestSync(context.TODO(), webhook)
 
 
 			if err != nil {
 			if err != nil {
 				webhookLog.Error("Failed to send webrequest ", "error", err)
 				webhookLog.Error("Failed to send webrequest ", "error", err)
@@ -40,14 +43,14 @@ func processWebhookQueue() {
 	}
 	}
 }
 }
 
 
-func sendWebRequest(webhook *Webhook) error {
+func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
 	webhookLog.Debug("Sending webhook", "url", webhook.Url)
 	webhookLog.Debug("Sending webhook", "url", webhook.Url)
 
 
-	client := http.Client{
+	client := &http.Client{
 		Timeout: time.Duration(10 * time.Second),
 		Timeout: time.Duration(10 * time.Second),
 	}
 	}
 
 
-	request, err := http.NewRequest("POST", webhook.Url, bytes.NewReader([]byte(webhook.Body)))
+	request, err := http.NewRequest(http.MethodPost, webhook.Url, bytes.NewReader([]byte(webhook.Body)))
 	if webhook.User != "" && webhook.Password != "" {
 	if webhook.User != "" && webhook.Password != "" {
 		request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
 		request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
 	}
 	}
@@ -56,22 +59,23 @@ func sendWebRequest(webhook *Webhook) error {
 		return err
 		return err
 	}
 	}
 
 
-	resp, err := client.Do(request)
+	resp, err := ctxhttp.Do(ctx, client, request)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	_, err = ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return err
+	if resp.StatusCode/100 == 2 {
+		return nil
 	}
 	}
 
 
-	if resp.StatusCode != 200 {
-		return fmt.Errorf("Webhook response code %v", resp.StatusCode)
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
 	}
 	}
-
 	defer resp.Body.Close()
 	defer resp.Body.Close()
-	return nil
+
+	webhookLog.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body))
+	return fmt.Errorf("Webhook response status %v", resp.Status)
 }
 }
 
 
 var addToWebhookQueue = func(msg *Webhook) {
 var addToWebhookQueue = func(msg *Webhook) {

+ 17 - 0
pkg/services/sqlstore/alert.go

@@ -17,6 +17,7 @@ func init() {
 	bus.AddHandler("sql", DeleteAlertById)
 	bus.AddHandler("sql", DeleteAlertById)
 	bus.AddHandler("sql", GetAllAlertQueryHandler)
 	bus.AddHandler("sql", GetAllAlertQueryHandler)
 	bus.AddHandler("sql", SetAlertState)
 	bus.AddHandler("sql", SetAlertState)
+	bus.AddHandler("sql", GetAlertStatesForDashboard)
 }
 }
 
 
 func GetAlertById(query *m.GetAlertByIdQuery) error {
 func GetAlertById(query *m.GetAlertByIdQuery) error {
@@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
 		return nil
 		return nil
 	})
 	})
 }
 }
+
+func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error {
+	var rawSql = `SELECT
+	                id,
+	                dashboard_id,
+	                panel_id,
+	                state,
+	                new_state_date
+	                FROM alert
+	                WHERE org_id = ? AND dashboard_id = ?`
+
+	query.Result = make([]*m.AlertStateInfoDTO, 0)
+	err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
+
+	return err
+}

+ 7 - 0
pkg/services/sqlstore/datasource.go

@@ -60,6 +60,13 @@ func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error {
 func AddDataSource(cmd *m.AddDataSourceCommand) error {
 func AddDataSource(cmd *m.AddDataSourceCommand) error {
 
 
 	return inTransaction(func(sess *xorm.Session) error {
 	return inTransaction(func(sess *xorm.Session) error {
+		existing := m.DataSource{OrgId: cmd.OrgId, Name: cmd.Name}
+		has, _ := sess.Get(&existing)
+
+		if has {
+			return m.ErrDataSourceNameExists
+		}
+
 		ds := &m.DataSource{
 		ds := &m.DataSource{
 			OrgId:             cmd.OrgId,
 			OrgId:             cmd.OrgId,
 			Name:              cmd.Name,
 			Name:              cmd.Name,

+ 7 - 2
pkg/services/sqlstore/datasource_test.go

@@ -41,6 +41,7 @@ func TestDataAccess(t *testing.T) {
 
 
 			err := AddDataSource(&m.AddDataSourceCommand{
 			err := AddDataSource(&m.AddDataSourceCommand{
 				OrgId:    10,
 				OrgId:    10,
+				Name:     "laban",
 				Type:     m.DS_INFLUXDB,
 				Type:     m.DS_INFLUXDB,
 				Access:   m.DS_ACCESS_DIRECT,
 				Access:   m.DS_ACCESS_DIRECT,
 				Url:      "http://test",
 				Url:      "http://test",
@@ -63,15 +64,19 @@ func TestDataAccess(t *testing.T) {
 
 
 		Convey("Given a datasource", func() {
 		Convey("Given a datasource", func() {
 
 
-			AddDataSource(&m.AddDataSourceCommand{
+			err := AddDataSource(&m.AddDataSourceCommand{
 				OrgId:  10,
 				OrgId:  10,
+				Name:   "nisse",
 				Type:   m.DS_GRAPHITE,
 				Type:   m.DS_GRAPHITE,
 				Access: m.DS_ACCESS_DIRECT,
 				Access: m.DS_ACCESS_DIRECT,
 				Url:    "http://test",
 				Url:    "http://test",
 			})
 			})
+			So(err, ShouldBeNil)
 
 
 			query := m.GetDataSourcesQuery{OrgId: 10}
 			query := m.GetDataSourcesQuery{OrgId: 10}
-			GetDataSources(&query)
+			err = GetDataSources(&query)
+			So(err, ShouldBeNil)
+
 			ds := query.Result[0]
 			ds := query.Result[0]
 
 
 			Convey("Can delete datasource", func() {
 			Convey("Can delete datasource", func() {

+ 27 - 26
pkg/services/sqlstore/migrator/migrator.go

@@ -92,44 +92,45 @@ func (mg *Migrator) Start() error {
 
 
 		mg.Logger.Debug("Executing", "sql", sql)
 		mg.Logger.Debug("Executing", "sql", sql)
 
 
-		if err := mg.exec(m); err != nil {
-			mg.Logger.Error("Exec failed", "error", err, "sql", sql)
-			record.Error = err.Error()
-			mg.x.Insert(&record)
+		err := mg.inTransaction(func(sess *xorm.Session) error {
+
+			if err := mg.exec(m, sess); err != nil {
+				mg.Logger.Error("Exec failed", "error", err, "sql", sql)
+				record.Error = err.Error()
+				sess.Insert(&record)
+				return err
+			} else {
+				record.Success = true
+				sess.Insert(&record)
+			}
+
+			return nil
+		})
+
+		if err != nil {
 			return err
 			return err
-		} else {
-			record.Success = true
-			mg.x.Insert(&record)
 		}
 		}
 	}
 	}
 
 
 	return nil
 	return nil
 }
 }
 
 
-func (mg *Migrator) exec(m Migration) error {
+func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
 	mg.Logger.Info("Executing migration", "id", m.Id())
 	mg.Logger.Info("Executing migration", "id", m.Id())
 
 
-	err := mg.inTransaction(func(sess *xorm.Session) error {
-
-		condition := m.GetCondition()
-		if condition != nil {
-			sql, args := condition.Sql(mg.dialect)
-			results, err := sess.Query(sql, args...)
-			if err != nil || len(results) == 0 {
-				mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
-				return sess.Rollback()
-			}
+	condition := m.GetCondition()
+	if condition != nil {
+		sql, args := condition.Sql(mg.dialect)
+		results, err := sess.Query(sql, args...)
+		if err != nil || len(results) == 0 {
+			mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
+			return sess.Rollback()
 		}
 		}
+	}
 
 
-		_, err := sess.Exec(m.Sql(mg.dialect))
-		if err != nil {
-			mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
-			return err
-		}
-		return nil
-	})
-
+	_, err := sess.Exec(m.Sql(mg.dialect))
 	if err != nil {
 	if err != nil {
+		mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
 		return err
 		return err
 	}
 	}
 
 

+ 8 - 5
pkg/tsdb/batch.go

@@ -1,6 +1,9 @@
 package tsdb
 package tsdb
 
 
-import "errors"
+import (
+	"context"
+	"errors"
+)
 
 
 type Batch struct {
 type Batch struct {
 	DataSourceId int64
 	DataSourceId int64
@@ -20,7 +23,7 @@ func newBatch(dsId int64, queries QuerySlice) *Batch {
 	}
 	}
 }
 }
 
 
-func (bg *Batch) process(context *QueryContext) {
+func (bg *Batch) process(ctx context.Context, queryContext *QueryContext) {
 	executor := getExecutorFor(bg.Queries[0].DataSource)
 	executor := getExecutorFor(bg.Queries[0].DataSource)
 
 
 	if executor == nil {
 	if executor == nil {
@@ -32,13 +35,13 @@ func (bg *Batch) process(context *QueryContext) {
 		for _, query := range bg.Queries {
 		for _, query := range bg.Queries {
 			result.QueryResults[query.RefId] = &QueryResult{Error: result.Error}
 			result.QueryResults[query.RefId] = &QueryResult{Error: result.Error}
 		}
 		}
-		context.ResultsChan <- result
+		queryContext.ResultsChan <- result
 		return
 		return
 	}
 	}
 
 
-	res := executor.Execute(bg.Queries, context)
+	res := executor.Execute(ctx, bg.Queries, queryContext)
 	bg.Done = true
 	bg.Done = true
-	context.ResultsChan <- res
+	queryContext.ResultsChan <- res
 }
 }
 
 
 func (bg *Batch) addQuery(query *Query) {
 func (bg *Batch) addQuery(query *Query) {

+ 3 - 1
pkg/tsdb/executor.go

@@ -1,7 +1,9 @@
 package tsdb
 package tsdb
 
 
+import "context"
+
 type Executor interface {
 type Executor interface {
-	Execute(queries QuerySlice, context *QueryContext) *BatchResult
+	Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult
 }
 }
 
 
 var registry map[string]GetExecutorFn
 var registry map[string]GetExecutorFn

+ 3 - 1
pkg/tsdb/fake_test.go

@@ -1,5 +1,7 @@
 package tsdb
 package tsdb
 
 
+import "context"
+
 type FakeExecutor struct {
 type FakeExecutor struct {
 	results   map[string]*QueryResult
 	results   map[string]*QueryResult
 	resultsFn map[string]ResultsFn
 	resultsFn map[string]ResultsFn
@@ -14,7 +16,7 @@ func NewFakeExecutor(dsInfo *DataSourceInfo) *FakeExecutor {
 	}
 	}
 }
 }
 
 
-func (e *FakeExecutor) Execute(queries QuerySlice, context *QueryContext) *BatchResult {
+func (e *FakeExecutor) Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult {
 	result := &BatchResult{QueryResults: make(map[string]*QueryResult)}
 	result := &BatchResult{QueryResults: make(map[string]*QueryResult)}
 	for _, query := range queries {
 	for _, query := range queries {
 		if results, has := e.results[query.RefId]; has {
 		if results, has := e.results[query.RefId]; has {

+ 8 - 4
pkg/tsdb/graphite/graphite.go

@@ -1,6 +1,7 @@
 package graphite
 package graphite
 
 
 import (
 import (
+	"context"
 	"crypto/tls"
 	"crypto/tls"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
@@ -11,6 +12,8 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
+	"golang.org/x/net/context/ctxhttp"
+
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
@@ -26,7 +29,7 @@ func NewGraphiteExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
 
 
 var (
 var (
 	glog       log.Logger
 	glog       log.Logger
-	HttpClient http.Client
+	HttpClient *http.Client
 )
 )
 
 
 func init() {
 func init() {
@@ -37,13 +40,13 @@ func init() {
 		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
 		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
 	}
 	}
 
 
-	HttpClient = http.Client{
+	HttpClient = &http.Client{
 		Timeout:   time.Duration(15 * time.Second),
 		Timeout:   time.Duration(15 * time.Second),
 		Transport: tr,
 		Transport: tr,
 	}
 	}
 }
 }
 
 
-func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
+func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
 	result := &tsdb.BatchResult{}
 	result := &tsdb.BatchResult{}
 
 
 	formData := url.Values{
 	formData := url.Values{
@@ -66,7 +69,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
 		result.Error = err
 		result.Error = err
 		return result
 		return result
 	}
 	}
-	res, err := HttpClient.Do(req)
+
+	res, err := ctxhttp.Do(ctx, HttpClient, req)
 	if err != nil {
 	if err != nil {
 		result.Error = err
 		result.Error = err
 		return result
 		return result

+ 3 - 3
pkg/tsdb/prometheus/prometheus.go

@@ -1,6 +1,7 @@
 package prometheus
 package prometheus
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"regexp"
 	"regexp"
@@ -11,7 +12,6 @@ import (
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/prometheus/client_golang/api/prometheus"
 	"github.com/prometheus/client_golang/api/prometheus"
 	pmodel "github.com/prometheus/common/model"
 	pmodel "github.com/prometheus/common/model"
-	"golang.org/x/net/context"
 )
 )
 
 
 type PrometheusExecutor struct {
 type PrometheusExecutor struct {
@@ -45,7 +45,7 @@ func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
 	return prometheus.NewQueryAPI(client), nil
 	return prometheus.NewQueryAPI(client), nil
 }
 }
 
 
-func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
+func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
 	result := &tsdb.BatchResult{}
 	result := &tsdb.BatchResult{}
 
 
 	client, err := e.getClient()
 	client, err := e.getClient()
@@ -64,7 +64,7 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb
 		Step:  query.Step,
 		Step:  query.Step,
 	}
 	}
 
 
-	value, err := client.QueryRange(context.Background(), query.Expr, timeRange)
+	value, err := client.QueryRange(ctx, query.Expr, timeRange)
 
 
 	if err != nil {
 	if err != nil {
 		return resultWithError(result, err)
 		return resultWithError(result, err)

+ 6 - 4
pkg/tsdb/request.go

@@ -1,8 +1,10 @@
 package tsdb
 package tsdb
 
 
-type HandleRequestFunc func(req *Request) (*Response, error)
+import "context"
 
 
-func HandleRequest(req *Request) (*Response, error) {
+type HandleRequestFunc func(ctx context.Context, req *Request) (*Response, error)
+
+func HandleRequest(ctx context.Context, req *Request) (*Response, error) {
 	context := NewQueryContext(req.Queries, req.TimeRange)
 	context := NewQueryContext(req.Queries, req.TimeRange)
 
 
 	batches, err := getBatches(req)
 	batches, err := getBatches(req)
@@ -16,7 +18,7 @@ func HandleRequest(req *Request) (*Response, error) {
 		if len(batch.Depends) == 0 {
 		if len(batch.Depends) == 0 {
 			currentlyExecuting += 1
 			currentlyExecuting += 1
 			batch.Started = true
 			batch.Started = true
-			go batch.process(context)
+			go batch.process(ctx, context)
 		}
 		}
 	}
 	}
 
 
@@ -46,7 +48,7 @@ func HandleRequest(req *Request) (*Response, error) {
 				if batch.allDependenciesAreIn(context) {
 				if batch.allDependenciesAreIn(context) {
 					currentlyExecuting += 1
 					currentlyExecuting += 1
 					batch.Started = true
 					batch.Started = true
-					go batch.process(context)
+					go batch.process(ctx, context)
 				}
 				}
 			}
 			}
 		}
 		}

+ 3 - 1
pkg/tsdb/testdata/testdata.go

@@ -1,6 +1,8 @@
 package testdata
 package testdata
 
 
 import (
 import (
+	"context"
+
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 )
@@ -21,7 +23,7 @@ func init() {
 	tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor)
 	tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor)
 }
 }
 
 
-func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
+func (e *TestDataExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
 	result := &tsdb.BatchResult{}
 	result := &tsdb.BatchResult{}
 	result.QueryResults = make(map[string]*tsdb.QueryResult)
 	result.QueryResults = make(map[string]*tsdb.QueryResult)
 
 

+ 6 - 5
pkg/tsdb/tsdb_test.go

@@ -1,6 +1,7 @@
 package tsdb
 package tsdb
 
 
 import (
 import (
+	"context"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -62,7 +63,7 @@ func TestMetricQuery(t *testing.T) {
 		fakeExecutor := registerFakeExecutor()
 		fakeExecutor := registerFakeExecutor()
 		fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
 		fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
 
 
-		res, err := HandleRequest(req)
+		res, err := HandleRequest(context.TODO(), req)
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 
 
 		Convey("Should return query results", func() {
 		Convey("Should return query results", func() {
@@ -83,7 +84,7 @@ func TestMetricQuery(t *testing.T) {
 		fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
 		fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
 		fakeExecutor.Return("B", TimeSeriesSlice{&TimeSeries{Name: "barg"}})
 		fakeExecutor.Return("B", TimeSeriesSlice{&TimeSeries{Name: "barg"}})
 
 
-		res, err := HandleRequest(req)
+		res, err := HandleRequest(context.TODO(), req)
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 
 
 		Convey("Should return query results", func() {
 		Convey("Should return query results", func() {
@@ -106,7 +107,7 @@ func TestMetricQuery(t *testing.T) {
 			},
 			},
 		}
 		}
 
 
-		res, err := HandleRequest(req)
+		res, err := HandleRequest(context.TODO(), req)
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 
 
 		Convey("Should have been batched in two requests", func() {
 		Convey("Should have been batched in two requests", func() {
@@ -121,7 +122,7 @@ func TestMetricQuery(t *testing.T) {
 			},
 			},
 		}
 		}
 
 
-		_, err := HandleRequest(req)
+		_, err := HandleRequest(context.TODO(), req)
 		So(err, ShouldNotBeNil)
 		So(err, ShouldNotBeNil)
 	})
 	})
 
 
@@ -152,7 +153,7 @@ func TestMetricQuery(t *testing.T) {
 				}}
 				}}
 		})
 		})
 
 
-		res, err := HandleRequest(req)
+		res, err := HandleRequest(context.TODO(), req)
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 
 
 		Convey("Should have been batched in two requests", func() {
 		Convey("Should have been batched in two requests", func() {

+ 1 - 1
public/app/core/directives/metric_segment.js

@@ -136,7 +136,7 @@ function (_, $, coreModule) {
 
 
         $button.click(function() {
         $button.click(function() {
           options = null;
           options = null;
-          $input.css('width', ($button.width() + 16) + 'px');
+          $input.css('width', (Math.max($button.width(), 80) + 16) + 'px');
 
 
           $button.hide();
           $button.hide();
           $input.show();
           $input.show();

+ 1 - 1
public/app/core/directives/value_select_dropdown.js

@@ -236,7 +236,7 @@ function (angular, _, coreModule) {
         var inputEl = elem.find('input');
         var inputEl = elem.find('input');
 
 
         function openDropdown() {
         function openDropdown() {
-          inputEl.css('width', Math.max(linkEl.width(), 30) + 'px');
+          inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
 
 
           inputEl.show();
           inputEl.show();
           linkEl.hide();
           linkEl.hide();

+ 43 - 31
public/app/features/alerting/alert_tab_ctrl.ts

@@ -6,6 +6,7 @@ import {QueryPart} from 'app/core/components/query_part/query_part';
 import alertDef from './alert_def';
 import alertDef from './alert_def';
 import config from 'app/core/config';
 import config from 'app/core/config';
 import moment from 'moment';
 import moment from 'moment';
+import appEvents from 'app/core/app_events';
 
 
 export class AlertTabCtrl {
 export class AlertTabCtrl {
   panel: any;
   panel: any;
@@ -47,19 +48,18 @@ export class AlertTabCtrl {
   $onInit() {
   $onInit() {
     this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
     this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
 
 
-    this.initModel();
-    this.validateModel();
+    // subscribe to graph threshold handle changes
+    var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
+    this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler);
 
 
-    // set panel alert edit mode
+   // set panel alert edit mode
     this.$scope.$on("$destroy", () => {
     this.$scope.$on("$destroy", () => {
+      this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler);
       this.panelCtrl.editingThresholds = false;
       this.panelCtrl.editingThresholds = false;
       this.panelCtrl.render();
       this.panelCtrl.render();
     });
     });
 
 
-    // subscribe to graph threshold handle changes
-    this.panelCtrl.events.on('threshold-changed', this.graphThresholdChanged.bind(this));
-
-    // build notification model
+       // build notification model
     this.notifications = [];
     this.notifications = [];
     this.alertNotifications = [];
     this.alertNotifications = [];
     this.alertHistory = [];
     this.alertHistory = [];
@@ -67,21 +67,8 @@ export class AlertTabCtrl {
     return this.backendSrv.get('/api/alert-notifications').then(res => {
     return this.backendSrv.get('/api/alert-notifications').then(res => {
       this.notifications = res;
       this.notifications = res;
 
 
-      _.each(this.alert.notifications, item => {
-        var model = _.find(this.notifications, {id: item.id});
-        if (model) {
-          model.iconClass = this.getNotificationIcon(model.type);
-          this.alertNotifications.push(model);
-        }
-      });
-
-      _.each(this.notifications, item => {
-        if (item.isDefault) {
-          item.iconClass = this.getNotificationIcon(item.type);
-          item.bgColor = "#00678b";
-          this.alertNotifications.push(item);
-        }
-      });
+      this.initModel();
+      this.validateModel();
     });
     });
   }
   }
 
 
@@ -142,9 +129,8 @@ export class AlertTabCtrl {
   }
   }
 
 
   initModel() {
   initModel() {
-    var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
-
-    if (!this.alert.enabled) {
+    var alert = this.alert = this.panel.alert;
+    if (!alert) {
       return;
       return;
     }
     }
 
 
@@ -168,6 +154,22 @@ export class AlertTabCtrl {
 
 
     ThresholdMapper.alertToGraphThresholds(this.panel);
     ThresholdMapper.alertToGraphThresholds(this.panel);
 
 
+    for (let addedNotification of alert.notifications) {
+      var model = _.find(this.notifications, {id: addedNotification.id});
+      if (model) {
+        model.iconClass = this.getNotificationIcon(model.type);
+        this.alertNotifications.push(model);
+      }
+    }
+
+    for (let notification of this.notifications) {
+      if (notification.isDefault) {
+        notification.iconClass = this.getNotificationIcon(notification.type);
+        notification.bgColor = "#00678b";
+        this.alertNotifications.push(notification);
+      }
+    }
+
     this.panelCtrl.editingThresholds = true;
     this.panelCtrl.editingThresholds = true;
     this.panelCtrl.render();
     this.panelCtrl.render();
   }
   }
@@ -192,7 +194,7 @@ export class AlertTabCtrl {
   }
   }
 
 
   validateModel() {
   validateModel() {
-    if (!this.alert.enabled) {
+    if (!this.alert) {
       return;
       return;
     }
     }
 
 
@@ -302,14 +304,24 @@ export class AlertTabCtrl {
   }
   }
 
 
   delete() {
   delete() {
-    this.alert = this.panel.alert = {enabled: false};
-    this.panel.thresholds = [];
-    this.conditionModels = [];
-    this.panelCtrl.render();
+    appEvents.emit('confirm-modal', {
+      title: 'Delete Alert',
+      text: 'Are you sure you want to delete this alert rule?',
+      text2: 'You need to save dashboard for the delete to take effect',
+      icon: 'fa-trash',
+      yesText: 'Delete',
+      onConfirm: () => {
+        delete this.panel.alert;
+        this.alert = null;
+        this.panel.thresholds = [];
+        this.conditionModels = [];
+        this.panelCtrl.render();
+      }
+    });
   }
   }
 
 
   enable() {
   enable() {
-    this.alert.enabled = true;
+    this.panel.alert = {};
     this.initModel();
     this.initModel();
   }
   }
 
 

+ 9 - 5
public/app/features/alerting/notification_edit_ctrl.ts

@@ -7,7 +7,7 @@ import config from 'app/core/config';
 
 
 export class AlertNotificationEditCtrl {
 export class AlertNotificationEditCtrl {
   model: any;
   model: any;
-  showTest: boolean = false;
+  theForm: any;
   testSeverity: string = "critical";
   testSeverity: string = "critical";
 
 
   /** @ngInject */
   /** @ngInject */
@@ -36,6 +36,10 @@ export class AlertNotificationEditCtrl {
   }
   }
 
 
   save() {
   save() {
+    if (!this.theForm.$valid) {
+      return;
+    }
+
     if (this.model.id) {
     if (this.model.id) {
       this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => {
       this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => {
         this.model = res;
         this.model = res;
@@ -53,11 +57,11 @@ export class AlertNotificationEditCtrl {
     this.model.settings = {};
     this.model.settings = {};
   }
   }
 
 
-  toggleTest() {
-    this.showTest = !this.showTest;
-  }
-
   testNotification() {
   testNotification() {
+    if (!this.theForm.$valid) {
+      return;
+    }
+
     var payload = {
     var payload = {
       name: this.model.name,
       name: this.model.name,
       type: this.model.type,
       type: this.model.type,

+ 2 - 2
public/app/features/alerting/partials/alert_tab.html

@@ -1,4 +1,4 @@
-<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert.enabled">
+<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert">
 	<aside class="edit-sidemenu-aside">
 	<aside class="edit-sidemenu-aside">
 		<ul class="edit-sidemenu">
 		<ul class="edit-sidemenu">
 			<li ng-class="{active: ctrl.subTabIndex === 0}">
 			<li ng-class="{active: ctrl.subTabIndex === 0}">
@@ -151,7 +151,7 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class="gf-form-group" ng-if="!ctrl.alert.enabled">
+<div class="gf-form-group" ng-if="!ctrl.alert">
 	<div class="gf-form-button-row">
 	<div class="gf-form-button-row">
 		<button class="btn btn-inverse" ng-click="ctrl.enable()">
 		<button class="btn btn-inverse" ng-click="ctrl.enable()">
 			<i class="icon-gf icon-gf-alert"></i>
 			<i class="icon-gf icon-gf-alert"></i>

+ 63 - 71
public/app/features/alerting/partials/notification_edit.html

@@ -6,81 +6,73 @@
 </navbar>
 </navbar>
 
 
 <div class="page-container" >
 <div class="page-container" >
-	<div class="page-header">
-		<h1>Alert notification</h1>
+  <div class="page-header">
+    <h1>Alert notification</h1>
   </div>
   </div>
 
 
-	<div class="gf-form-group">
-		<div class="gf-form">
-			<span class="gf-form-label width-12">Name</span>
-			<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
-		</div>
-		<div class="gf-form">
-			<span class="gf-form-label width-12">Type</span>
-			<div class="gf-form-select-wrapper width-15">
-				<select class="gf-form-input"
-					ng-model="ctrl.model.type"
-					ng-options="t for t in ['webhook', 'email', 'slack']"
-					ng-change="ctrl.typeChanged(notification, $index)">
-				</select>
-			</div>
-		</div>
-		<div class="gf-form">
-			<gf-form-switch
-				class="gf-form"
-				label="Send on all alerts"
-				label-class="width-12"
-				checked="ctrl.model.isDefault"
-				tooltip="Use this notification for all alerts">
-			</gf-form-switch>
-		</div>
-	</div>
+  <form name="ctrl.theForm">
+    <div class="gf-form-group">
+      <div class="gf-form">
+        <span class="gf-form-label width-12">Name</span>
+        <input type="text" required class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-12">Type</span>
+        <div class="gf-form-select-wrapper width-15">
+          <select class="gf-form-input" ng-model="ctrl.model.type" ng-options="t for t in ['webhook', 'email', 'slack']" ng-change="ctrl.typeChanged(notification, $index)">
+          </select>
+        </div>
+      </div>
+      <div class="gf-form">
+        <gf-form-switch class="gf-form" label="Send on all alerts" label-class="width-12" checked="ctrl.model.isDefault" tooltip="Use this notification for all alerts">
+        </gf-form-switch>
+      </div>
+    </div>
 
 
-	<div class="gf-form-group" ng-show="ctrl.model.type === 'webhook'">
-    <h3 class="page-heading">Webhook settings</h3>
-		<div class="gf-form">
-			<span class="gf-form-label width-6">Url</span>
-			<input type="text" class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
-		</div>
-		<div class="gf-form-inline">
-			<div class="gf-form">
-				<span class="gf-form-label width-6">Username</span>
-				<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.username"></input>
-			</div>
-			<div class="gf-form">
-				<span class="gf-form-label width-6">Password</span>
-				<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.password"></input>
-			</div>
-		</div>
-	</div>
+    <div class="gf-form-group" ng-if="ctrl.model.type === 'webhook'">
+      <h3 class="page-heading">Webhook settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-6">Url</span>
+        <input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
+      </div>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <span class="gf-form-label width-6">Username</span>
+          <input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.username"></input>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label width-6">Password</span>
+          <input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.password"></input>
+        </div>
+      </div>
+    </div>
 
 
-  <div class="gf-form-group" ng-show="ctrl.model.type === 'slack'">
-    <h3 class="page-heading">Slack settings</h3>
-		<div class="gf-form">
-			<span class="gf-form-label width-6">Url</span>
-			<input type="text" 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-group" ng-if="ctrl.model.type === 'slack'">
+      <h3 class="page-heading">Slack settings</h3>
+      <div class="gf-form">
+        <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>
 
 
-	<div class="gf-form-group section" ng-show="ctrl.model.type === 'email'">
-    <h3 class="page-heading">Email addresses</h3>
-		<div class="gf-form">
-      <textarea rows="7" class="gf-form-input width-25" ng-model="ctrl.model.settings.addresses"></textarea>
-		</div>
-	</div>
+    <div class="gf-form-group section" ng-if="ctrl.model.type === 'email'">
+      <h3 class="page-heading">Email addresses</h3>
+      <div class="gf-form">
+        <textarea rows="7" class="gf-form-input width-25" required ng-model="ctrl.model.settings.addresses"></textarea>
+      </div>
+    </div>
 
 
-  <div class="gf-form-group">
-		<div class="gf-form-inline">
-			<div class="gf-form width-6">
-				<button ng-click="ctrl.save()" class="btn btn-success">Save</button>
-			</div>
-			<div class="gf-form width-8">
-				<button ng-click="ctrl.toggleTest()" class="btn btn-secondary">Test</button>
-			</div>
-			<div class="gf-form width-20" ng-show="ctrl.showTest">
-			<div class="gf-form" ng-show="ctrl.showTest">
-				<button ng-click="ctrl.testNotification()" class="btn btn-secondary">Send</button>
-			</div>
-		</div>
-	</div>
+    <div class="gf-form-group">
+      <div class="gf-form-inline">
+        <div class="gf-form width-6">
+          <button type="submit" ng-click="ctrl.save()" class="btn btn-success">Save</button>
+        </div>
+        <div class="gf-form width-20">
+          <div class="gf-form">
+            <button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary">Send Test</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </form>
 </div>
 </div>

+ 40 - 4
public/app/features/annotations/annotations_srv.ts

@@ -9,6 +9,7 @@ import coreModule from 'app/core/core_module';
 
 
 export class AnnotationsSrv {
 export class AnnotationsSrv {
   globalAnnotationsPromise: any;
   globalAnnotationsPromise: any;
+  alertStatesPromise: any;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private $rootScope,
   constructor(private $rootScope,
@@ -22,14 +23,27 @@ export class AnnotationsSrv {
 
 
   clearCache() {
   clearCache() {
     this.globalAnnotationsPromise = null;
     this.globalAnnotationsPromise = null;
+    this.alertStatesPromise = null;
   }
   }
 
 
   getAnnotations(options) {
   getAnnotations(options) {
     return this.$q.all([
     return this.$q.all([
       this.getGlobalAnnotations(options),
       this.getGlobalAnnotations(options),
-      this.getPanelAnnotations(options)
-    ]).then(allResults => {
-      return _.flattenDeep(allResults);
+      this.getPanelAnnotations(options),
+      this.getAlertStates(options)
+    ]).then(results => {
+
+      // combine the annotations and flatten results
+      var annotations = _.flattenDeep([results[0], results[1]]);
+
+      // look for alert state for this panel
+      var alertState = _.find(results[2], {panelId: options.panel.id});
+
+      return {
+        annotations: annotations,
+        alertState: alertState,
+      };
+
     }).catch(err => {
     }).catch(err => {
       this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
       this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
     });
     });
@@ -39,7 +53,7 @@ export class AnnotationsSrv {
     var panel = options.panel;
     var panel = options.panel;
     var dashboard = options.dashboard;
     var dashboard = options.dashboard;
 
 
-    if (panel && panel.alert && panel.alert.enabled) {
+    if (panel && panel.alert) {
       return this.backendSrv.get('/api/annotations', {
       return this.backendSrv.get('/api/annotations', {
         from: options.range.from.valueOf(),
         from: options.range.from.valueOf(),
         to: options.range.to.valueOf(),
         to: options.range.to.valueOf(),
@@ -54,6 +68,28 @@ export class AnnotationsSrv {
     return this.$q.when([]);
     return this.$q.when([]);
   }
   }
 
 
+  getAlertStates(options) {
+    if (!options.dashboard.id) {
+      return this.$q.when([]);
+    }
+
+    // ignore if no alerts
+    if (options.panel && !options.panel.alert) {
+      return this.$q.when([]);
+    }
+
+    if (options.range.raw.to !== 'now') {
+      return this.$q.when([]);
+    }
+
+    if (this.alertStatesPromise) {
+      return this.alertStatesPromise;
+    }
+
+    this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id});
+    return this.alertStatesPromise;
+  }
+
   getGlobalAnnotations(options) {
   getGlobalAnnotations(options) {
     var dashboard = options.dashboard;
     var dashboard = options.dashboard;
 
 

+ 1 - 1
public/app/features/dashboard/dashnav/dashnav.ts

@@ -159,7 +159,7 @@ export class DashNavCtrl {
       var confirmText = "";
       var confirmText = "";
       var text2 = $scope.dashboard.title;
       var text2 = $scope.dashboard.title;
       var alerts = $scope.dashboard.rows.reduce((memo, row) => {
       var alerts = $scope.dashboard.rows.reduce((memo, row) => {
-        memo += row.panels.filter(panel => panel.alert && panel.alert.enabled).length;
+        memo += row.panels.filter(panel => panel.alert).length;
         return memo;
         return memo;
       }, 0);
       }, 0);
 
 

+ 3 - 1
public/app/features/panel/metrics_panel_ctrl.ts

@@ -131,7 +131,9 @@ class MetricsPanelCtrl extends PanelCtrl {
     var intervalOverride = this.panel.interval;
     var intervalOverride = this.panel.interval;
 
 
     // if no panel interval check datasource
     // if no panel interval check datasource
-    if (!intervalOverride && this.datasource && this.datasource.interval) {
+    if (intervalOverride) {
+      intervalOverride = this.templateSrv.replace(intervalOverride, this.panel.scopedVars);
+    } else if (this.datasource && this.datasource.interval) {
       intervalOverride = this.datasource.interval;
       intervalOverride = this.datasource.interval;
     }
     }
 
 

+ 21 - 1
public/app/features/panel/panel_directive.ts

@@ -6,7 +6,7 @@ import $ from 'jquery';
 var module = angular.module('grafana.directives');
 var module = angular.module('grafana.directives');
 
 
 var panelTemplate = `
 var panelTemplate = `
-  <div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
+  <div class="panel-container">
     <div class="panel-header">
     <div class="panel-header">
       <span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
       <span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
         <span data-placement="top" bs-tooltip="ctrl.error">
         <span data-placement="top" bs-tooltip="ctrl.error">
@@ -65,6 +65,26 @@ module.directive('grafanaPanel', function() {
     link: function(scope, elem) {
     link: function(scope, elem) {
       var panelContainer = elem.find('.panel-container');
       var panelContainer = elem.find('.panel-container');
       var ctrl = scope.ctrl;
       var ctrl = scope.ctrl;
+
+      // the reason for handling these classes this way is for performance
+      // limit the watchers on panels etc
+
+      ctrl.events.on('render', () => {
+        panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true);
+        panelContainer.toggleClass('panel-has-alert', ctrl.panel.alert !== undefined);
+
+        if (panelContainer.hasClass('panel-has-alert')) {
+          panelContainer.removeClass('panel-alert-state--ok panel-alert-state--alerting');
+        }
+
+        // set special class for ok, or alerting states
+        if (ctrl.alertState) {
+          if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') {
+            panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state);
+          }
+        }
+      });
+
       scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() {
       scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() {
         panelContainer.css({minHeight: ctrl.containerHeight});
         panelContainer.css({minHeight: ctrl.containerHeight});
         elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
         elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);

+ 1 - 0
public/app/features/panel/panel_menu.js

@@ -12,6 +12,7 @@ function (angular, $, _, Tether) {
     .directive('panelMenu', function($compile, linkSrv) {
     .directive('panelMenu', function($compile, linkSrv) {
       var linkTemplate =
       var linkTemplate =
           '<span class="panel-title drag-handle pointer">' +
           '<span class="panel-title drag-handle pointer">' +
+            '<span class="icon-gf panel-alert-icon"></span>' +
             '<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>' +
             '<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>' +
             '<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
             '<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
             '<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>' +
             '<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>' +

+ 1 - 1
public/app/features/plugins/ds_edit_ctrl.ts

@@ -122,7 +122,7 @@ export class DataSourceEditCtrl {
       });
       });
     }
     }
 
 
-    saveChanges(test) {
+    saveChanges() {
       if (!this.editForm.$valid) {
       if (!this.editForm.$valid) {
         return;
         return;
       }
       }

+ 2 - 2
public/app/partials/panelgeneral.html

@@ -8,11 +8,11 @@
 			<span class="gf-form-label width-6">Span</span>
 			<span class="gf-form-label width-6">Span</span>
 			<select class="gf-form-input gf-size-auto" ng-model="ctrl.panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
 			<select class="gf-form-input gf-size-auto" ng-model="ctrl.panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
 		</div>
 		</div>
-		<div class="gf-form max-width-26">
+		<div class="gf-form">
 			<span class="gf-form-label width-8">Height</span>
 			<span class="gf-form-label width-8">Height</span>
 			<input type="text" class="gf-form-input max-width-6" ng-model='ctrl.panel.height' placeholder="100px"></input>
 			<input type="text" class="gf-form-input max-width-6" ng-model='ctrl.panel.height' placeholder="100px"></input>
-			<editor-checkbox text="Transparent" model="ctrl.panel.transparent"></editor-checkbox>
 		</div>
 		</div>
+		<gf-form-switch class="gf-form" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
 	</div>
 	</div>
 
 
 	<div class="gf-form-inline">
 	<div class="gf-form-inline">

+ 1 - 1
public/app/partials/valueSelectDropdown.html

@@ -10,7 +10,7 @@
 		<i class="fa fa-caret-down"></i>
 		<i class="fa fa-caret-down"></i>
 	</a>
 	</a>
 
 
-	<input type="text" class="hidden-input input-small gf-form-input" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
+	<input type="text" class="gf-form-input" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
 
 
 	<div class="variable-value-dropdown" ng-if="vm.dropdownVisible" ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}">
 	<div class="variable-value-dropdown" ng-if="vm.dropdownVisible" ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}">
 		<div class="variable-options-wrapper">
 		<div class="variable-options-wrapper">

+ 26 - 7
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -3,9 +3,10 @@ define([
   'lodash',
   'lodash',
   'moment',
   'moment',
   'app/core/utils/datemath',
   'app/core/utils/datemath',
+  'app/core/utils/kbn',
   './annotation_query',
   './annotation_query',
 ],
 ],
-function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
+function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
   'use strict';
   'use strict';
 
 
   /** @ngInject */
   /** @ngInject */
@@ -36,12 +37,9 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
         query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
         query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
         query.statistics = target.statistics;
         query.statistics = target.statistics;
 
 
-        var range = end - start;
-        query.period = parseInt(target.period, 10) || (query.namespace === 'AWS/EC2' ? 300 : 60);
-        if (range / query.period >= 1440) {
-          query.period = Math.ceil(range / 1440 / 60) * 60;
-        }
-        target.period = query.period;
+        var period = this._getPeriod(target, query, options, start, end);
+        target.period = period;
+        query.period = period;
 
 
         queries.push(query);
         queries.push(query);
       }.bind(this));
       }.bind(this));
@@ -69,6 +67,27 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
       });
       });
     };
     };
 
 
+    this._getPeriod = function(target, query, options, start, end) {
+      var period;
+      var range = end - start;
+
+      if (!target.period) {
+        period = (query.namespace === 'AWS/EC2') ? 300 : 60;
+      } else if (/^\d+$/.test(target.period)) {
+        period = parseInt(target.period, 10);
+      } else {
+        period = kbn.interval_to_seconds(templateSrv.replace(target.period, options.scopedVars));
+      }
+      if (query.period < 60) {
+        period = 60;
+      }
+      if (range / query.period >= 1440) {
+        period = Math.ceil(range / 1440 / 60) * 60;
+      }
+
+      return period;
+    };
+
     this.performTimeSeriesQuery = function(query, start, end) {
     this.performTimeSeriesQuery = function(query, start, end) {
       return this.awsRequest({
       return this.awsRequest({
         region: query.region,
         region: query.region,

+ 29 - 0
public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts

@@ -82,6 +82,35 @@ describe('CloudWatchDatasource', function() {
       ctx.$rootScope.$apply();
       ctx.$rootScope.$apply();
     });
     });
 
 
+    it('should generate the correct query with interval variable', function(done) {
+      ctx.templateSrv.data = {
+        period: '10m'
+      };
+
+      var query = {
+        range: { from: 'now-1h', to: 'now' },
+        targets: [
+          {
+            region: 'us-east-1',
+            namespace: 'AWS/EC2',
+            metricName: 'CPUUtilization',
+            dimensions: {
+              InstanceId: 'i-12345678'
+            },
+            statistics: ['Average'],
+            period: '[[period]]'
+          }
+        ]
+      };
+
+      ctx.ds.query(query).then(function() {
+        var params = requestParams.data.parameters;
+        expect(params.period).to.be(600);
+        done();
+      });
+      ctx.$rootScope.$apply();
+    });
+
     it('should return series list', function(done) {
     it('should return series list', function(done) {
       ctx.ds.query(query).then(function(result) {
       ctx.ds.query(query).then(function(result) {
         expect(result.data[0].target).to.be('CPUUtilization_Average');
         expect(result.data[0].target).to.be('CPUUtilization_Average');

+ 1 - 1
public/app/plugins/panel/graph/graph.ts

@@ -62,7 +62,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
         if (!data) {
         if (!data) {
           return;
           return;
         }
         }
-        annotations = data.annotations || annotations;
+        annotations = ctrl.annotations;
         render_panel();
         render_panel();
       });
       });
 
 

+ 7 - 3
public/app/plugins/panel/graph/module.ts

@@ -22,6 +22,9 @@ class GraphCtrl extends MetricsPanelCtrl {
   hiddenSeries: any = {};
   hiddenSeries: any = {};
   seriesList: any = [];
   seriesList: any = [];
   dataList: any = [];
   dataList: any = [];
+  annotations: any = [];
+  alertState: any;
+
   annotationsPromise: any;
   annotationsPromise: any;
   datapointsCount: number;
   datapointsCount: number;
   datapointsOutside: boolean;
   datapointsOutside: boolean;
@@ -167,11 +170,11 @@ class GraphCtrl extends MetricsPanelCtrl {
 
 
   onDataError(err) {
   onDataError(err) {
     this.seriesList = [];
     this.seriesList = [];
+    this.annotations = [];
     this.render([]);
     this.render([]);
   }
   }
 
 
   onDataReceived(dataList) {
   onDataReceived(dataList) {
-
     this.dataList = dataList;
     this.dataList = dataList;
     this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
     this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
 
 
@@ -186,9 +189,10 @@ class GraphCtrl extends MetricsPanelCtrl {
       }
       }
     }
     }
 
 
-    this.annotationsPromise.then(annotations => {
+    this.annotationsPromise.then(result => {
       this.loading = false;
       this.loading = false;
-      this.seriesList.annotations = annotations;
+      this.alertState = result.alertState;
+      this.annotations = result.annotations;
       this.render(this.seriesList);
       this.render(this.seriesList);
     }, () => {
     }, () => {
       this.loading = false;
       this.loading = false;

+ 1 - 1
public/app/plugins/panel/graph/thresholds_form.ts

@@ -13,7 +13,7 @@ export class ThresholdFormCtrl {
   constructor($scope) {
   constructor($scope) {
     this.panel = this.panelCtrl.panel;
     this.panel = this.panelCtrl.panel;
 
 
-    if (this.panel.alert && this.panel.alert.enabled) {
+    if (this.panel.alert) {
       this.disabled = true;
       this.disabled = true;
     }
     }
 
 

+ 1 - 4
public/app/plugins/panel/table/editor.html

@@ -34,10 +34,7 @@
 					ng-change="editor.render()"
 					ng-change="editor.render()"
 					ng-model-onblur>
 					ng-model-onblur>
 			</div>
 			</div>
-			<gf-form-switch class="gf-form" label-class="width-4"
-				label="Scroll"
-				checked="editor.panel.scroll"
-				change="editor.render()"></gf-form-switch>
+			<gf-form-switch class="gf-form" label-class="width-4" label="Scroll" checked="editor.panel.scroll" on-change="editor.render()"></gf-form-switch>
 			<div class="gf-form max-width-17">
 			<div class="gf-form max-width-17">
 				<label class="gf-form-label width-6">Font size</label>
 				<label class="gf-form-label width-6">Font size</label>
 				<div class="gf-form-select-wrapper max-width-15">
 				<div class="gf-form-select-wrapper max-width-15">

+ 43 - 0
public/sass/pages/_alerting.scss

@@ -38,3 +38,46 @@
     top: 2px;
     top: 2px;
   }
   }
 }
 }
+
+.panel-has-alert {
+  .panel-alert-icon:before {
+    content: "\e611";
+    position: relative;
+    top: 1px;
+    left: -3px;
+  }
+}
+
+.panel-alert-state {
+  &--alerting {
+    animation: alerting-panel 2s 0s infinite;
+    opacity: 1;
+
+    .panel-alert-icon:before {
+      color: $critical;
+      content: "\e610";
+    }
+  }
+
+  &--ok {
+    box-shadow: 0 0 5px rgba(0,200,0,10.8);
+    .panel-alert-icon:before {
+      color: $online;
+      content: "\e611";
+    }
+  }
+}
+
+@keyframes alerting-panel {
+  0% {
+    box-shadow: none;
+  }
+  50% {
+    box-shadow: 0 0 10px $critical;
+  }
+  100% {
+    box-shadow: none;
+  }
+}
+
+