Selaa lähdekoodia

Merge branch 'master' into graph-panel-non-timeseries

Torkel Ödegaard 9 vuotta sitten
vanhempi
commit
36252602af

+ 2 - 0
CHANGELOG.md

@@ -9,12 +9,14 @@
 * **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609)
 * **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456)
 * **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
+* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
 
 # 3.1.2 (unreleased)
 * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
 * **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767)
 * **Internal Metrics**: Fixed issue with dots in instance_name when sending internal metrics to Graphite, fixes [#5739](https://github.com/grafana/grafana/issues/5739)
 * **Grafana-CLI**: Add default plugin path for MAC OS, fixes [#5806](https://github.com/grafana/grafana/issues/5806)
+* **Grafana-CLI**: Improve error message for upgrade-all command, fixes [#5885](https://github.com/grafana/grafana/issues/5885)
 
 # 3.1.1 (2016-08-01)
 * **IFrame embedding**: Fixed issue of using full iframe height, fixes [#5605](https://github.com/grafana/grafana/issues/5606)

+ 1 - 1
README.md

@@ -78,7 +78,7 @@ the latest master builds [here](http://grafana.org/download/builds)
 
 ### Dependencies
 
-- Go 1.5
+- Go 1.6
 - NodeJS v4+
 - [Godep](https://github.com/tools/godep)
 

+ 1 - 1
build.go

@@ -34,7 +34,7 @@ var (
 	binaries              []string = []string{"grafana-server", "grafana-cli"}
 )
 
-const minGoVersion = 1.3
+const minGoVersion = 1.6
 
 func main() {
 	log.SetOutput(os.Stdout)

+ 1 - 1
circle.yml

@@ -31,4 +31,4 @@ deployment:
     branch: master
     owner: grafana
     commands:
-      - ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
+      - ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}

+ 1 - 1
conf/sample.ini

@@ -318,7 +318,7 @@ check_for_updates = true
 #   \______(_______;;;)__;;;)
 
 [alerting]
-enabled = false
+;enabled = false
 
 #################################### Internal Grafana Metrics ##########################
 # Metrics available at HTTP API Url /api/metrics

+ 1 - 1
docs/sources/datasources/cloudwatch.md

@@ -77,7 +77,7 @@ Example dimension queries which will return list of resources for individual AWS
 
 Service | Query
 ------- | -----
-EBS | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
+ELB | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
 ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)`
 RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)`
 RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)`

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

@@ -88,6 +88,8 @@ Another way is put a webserver like Nginx or Apache in front of Grafana and have
 
 `http` or `https`
 
+> **Note** Grafana versions earlier than 3.0 are vulnerable to [POODLE](https://en.wikipedia.org/wiki/POODLE). So we strongly recommend to upgrade to 3.x or use a reverse proxy for ssl termination.
+
 ### domain
 
 This setting is only used in as a part of the `root_url` setting (see below). Important if you

+ 68 - 0
pkg/api/alerting.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/alerting"
+	"github.com/grafana/grafana/pkg/services/annotations"
 )
 
 func ValidateOrgAlert(c *middleware.Context) {
@@ -212,3 +213,70 @@ func DeleteAlertNotification(c *middleware.Context) Response {
 
 	return ApiSuccess("Notification deleted")
 }
+
+func GetAlertHistory(c *middleware.Context) Response {
+	alertId, err := getAlertIdForRequest(c)
+	if err != nil {
+		return ApiError(400, "Invalid request", err)
+	}
+
+	query := &annotations.ItemQuery{
+		AlertId: alertId,
+		Type:    annotations.AlertType,
+		OrgId:   c.OrgId,
+		Limit:   c.QueryInt64("limit"),
+	}
+
+	repo := annotations.GetRepository()
+
+	items, err := repo.Find(query)
+	if err != nil {
+		return ApiError(500, "Failed to get history for alert", err)
+	}
+
+	var result []dtos.AlertHistory
+	for _, item := range items {
+		result = append(result, dtos.AlertHistory{
+			AlertId:   item.AlertId,
+			Timestamp: item.Timestamp,
+			Data:      item.Data,
+			NewState:  item.NewState,
+			Text:      item.Text,
+			Metric:    item.Metric,
+			Title:     item.Title,
+		})
+	}
+
+	return Json(200, result)
+}
+
+func getAlertIdForRequest(c *middleware.Context) (int64, error) {
+	alertId := c.QueryInt64("alertId")
+	panelId := c.QueryInt64("panelId")
+	dashboardId := c.QueryInt64("dashboardId")
+
+	if alertId == 0 && dashboardId == 0 && panelId == 0 {
+		return 0, fmt.Errorf("Missing alertId or dashboardId and panelId")
+	}
+
+	if alertId == 0 {
+		//fetch alertId
+		query := models.GetAlertsQuery{
+			OrgId:       c.OrgId,
+			DashboardId: dashboardId,
+			PanelId:     panelId,
+		}
+
+		if err := bus.Dispatch(&query); err != nil {
+			return 0, err
+		}
+
+		if len(query.Result) != 1 {
+			return 0, fmt.Errorf("PanelId is not unique on dashboard")
+		}
+
+		alertId = query.Result[0].Id
+	}
+
+	return alertId, nil
+}

+ 5 - 1
pkg/api/api.go

@@ -19,6 +19,9 @@ func Register(r *macaron.Macaron) {
 	quota := middleware.Quota
 	bind := binding.Bind
 
+	// automatically set HEAD for every GET
+	r.SetAutoHead(true)
+
 	// not logged in views
 	r.Get("/", reqSignedIn, Index)
 	r.Get("/logout", Logout)
@@ -247,11 +250,12 @@ func Register(r *macaron.Macaron) {
 
 		r.Group("/alerts", func() {
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
-			//r.Get("/:alertId/states", wrap(GetAlertStates))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/", wrap(GetAlerts))
 		})
 
+		r.Get("/alert-history", wrap(GetAlertHistory))
+
 		r.Get("/alert-notifications", wrap(GetAlertNotifications))
 
 		r.Group("/alert-notifications", func() {

+ 1 - 1
pkg/api/datasources.go

@@ -22,7 +22,6 @@ func GetDataSources(c *middleware.Context) {
 
 	result := make(dtos.DataSourceList, 0)
 	for _, ds := range query.Result {
-
 		dsItem := dtos.DataSource{
 			Id:        ds.Id,
 			OrgId:     ds.OrgId,
@@ -35,6 +34,7 @@ func GetDataSources(c *middleware.Context) {
 			User:      ds.User,
 			BasicAuth: ds.BasicAuth,
 			IsDefault: ds.IsDefault,
+			JsonData:  ds.JsonData,
 		}
 
 		if plugin, exists := plugins.DataSources[ds.Type]; exists {

+ 11 - 0
pkg/api/dtos/alerting.go

@@ -52,3 +52,14 @@ type EvalMatch struct {
 	Metric string            `json:"metric"`
 	Value  float64           `json:"value"`
 }
+
+type AlertHistory struct {
+	AlertId   int64     `json:"alertId"`
+	NewState  string    `json:"newState"`
+	Timestamp time.Time `json:"timestamp"`
+	Title     string    `json:"title"`
+	Text      string    `json:"text"`
+	Metric    string    `json:"metric"`
+
+	Data *simplejson.Json `json:"data"`
+}

+ 1 - 1
pkg/cmd/grafana-cli/commands/commands.go

@@ -14,7 +14,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C
 		cmd := &contextCommandLine{context}
 		if err := command(cmd); err != nil {
 			logger.Errorf("\n%s: ", color.RedString("Error"))
-			logger.Errorf("%s\n\n", err)
+			logger.Errorf("%s %s\n\n", color.RedString("✗"), err)
 
 			cmd.ShowHelp()
 			os.Exit(1)

+ 10 - 2
pkg/cmd/grafana-cli/commands/upgrade_all_command.go

@@ -53,8 +53,16 @@ func upgradeAllCommand(c CommandLine) error {
 	for _, p := range pluginsToUpgrade {
 		logger.Infof("Updating %v \n", p.Id)
 
-		s.RemoveInstalledPlugin(pluginsDir, p.Id)
-		InstallPlugin(p.Id, "", c)
+		var err error
+		err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
+		if err != nil {
+			return err
+		}
+
+		err = InstallPlugin(p.Id, "", c)
+		if err != nil {
+			return err
+		}
 	}
 
 	return nil

+ 37 - 32
pkg/metrics/metrics.go

@@ -9,34 +9,36 @@ func init() {
 }
 
 var (
-	M_Instance_Start                     Counter
-	M_Page_Status_200                    Counter
-	M_Page_Status_500                    Counter
-	M_Page_Status_404                    Counter
-	M_Api_Status_500                     Counter
-	M_Api_Status_404                     Counter
-	M_Api_User_SignUpStarted             Counter
-	M_Api_User_SignUpCompleted           Counter
-	M_Api_User_SignUpInvite              Counter
-	M_Api_Dashboard_Save                 Timer
-	M_Api_Dashboard_Get                  Timer
-	M_Api_Dashboard_Search               Timer
-	M_Api_Admin_User_Create              Counter
-	M_Api_Login_Post                     Counter
-	M_Api_Login_OAuth                    Counter
-	M_Api_Org_Create                     Counter
-	M_Api_Dashboard_Snapshot_Create      Counter
-	M_Api_Dashboard_Snapshot_External    Counter
-	M_Api_Dashboard_Snapshot_Get         Counter
-	M_Models_Dashboard_Insert            Counter
-	M_Alerting_Result_Critical           Counter
-	M_Alerting_Result_Warning            Counter
-	M_Alerting_Result_Info               Counter
-	M_Alerting_Result_Ok                 Counter
-	M_Alerting_Active_Alerts             Counter
-	M_Alerting_Notification_Sent_Slack   Counter
-	M_Alerting_Notification_Sent_Email   Counter
-	M_Alerting_Notification_Sent_Webhook Counter
+	M_Instance_Start                       Counter
+	M_Page_Status_200                      Counter
+	M_Page_Status_500                      Counter
+	M_Page_Status_404                      Counter
+	M_Api_Status_500                       Counter
+	M_Api_Status_404                       Counter
+	M_Api_User_SignUpStarted               Counter
+	M_Api_User_SignUpCompleted             Counter
+	M_Api_User_SignUpInvite                Counter
+	M_Api_Dashboard_Save                   Timer
+	M_Api_Dashboard_Get                    Timer
+	M_Api_Dashboard_Search                 Timer
+	M_Api_Admin_User_Create                Counter
+	M_Api_Login_Post                       Counter
+	M_Api_Login_OAuth                      Counter
+	M_Api_Org_Create                       Counter
+	M_Api_Dashboard_Snapshot_Create        Counter
+	M_Api_Dashboard_Snapshot_External      Counter
+	M_Api_Dashboard_Snapshot_Get           Counter
+	M_Models_Dashboard_Insert              Counter
+	M_Alerting_Result_State_Critical       Counter
+	M_Alerting_Result_State_Warning        Counter
+	M_Alerting_Result_State_Ok             Counter
+	M_Alerting_Result_State_Paused         Counter
+	M_Alerting_Result_State_Pending        Counter
+	M_Alerting_Result_State_ExecutionError Counter
+	M_Alerting_Active_Alerts               Counter
+	M_Alerting_Notification_Sent_Slack     Counter
+	M_Alerting_Notification_Sent_Email     Counter
+	M_Alerting_Notification_Sent_Webhook   Counter
 
 	// Timers
 	M_DataSource_ProxyReq_Timer Timer
@@ -75,10 +77,13 @@ func initMetricVars(settings *MetricSettings) {
 
 	M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert")
 
-	M_Alerting_Result_Critical = RegCounter("alerting.result", "severity", "critical")
-	M_Alerting_Result_Warning = RegCounter("alerting.result", "severity", "warning")
-	M_Alerting_Result_Info = RegCounter("alerting.result", "severity", "info")
-	M_Alerting_Result_Ok = RegCounter("alerting.result", "severity", "ok")
+	M_Alerting_Result_State_Critical = RegCounter("alerting.result", "state", "critical")
+	M_Alerting_Result_State_Warning = RegCounter("alerting.result", "state", "warning")
+	M_Alerting_Result_State_Ok = RegCounter("alerting.result", "state", "ok")
+	M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
+	M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending")
+	M_Alerting_Result_State_ExecutionError = RegCounter("alerting.result", "state", "execution_error")
+
 	M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
 	M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
 	M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")

+ 10 - 4
pkg/services/alerting/conditions/query.go

@@ -106,10 +106,16 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb.
 				RefId: "A",
 				Query: c.Query.Model.Get("target").MustString(),
 				DataSource: &tsdb.DataSourceInfo{
-					Id:       datasource.Id,
-					Name:     datasource.Name,
-					PluginId: datasource.Type,
-					Url:      datasource.Url,
+					Id:                datasource.Id,
+					Name:              datasource.Name,
+					PluginId:          datasource.Type,
+					Url:               datasource.Url,
+					User:              datasource.User,
+					Password:          datasource.Password,
+					Database:          datasource.Database,
+					BasicAuth:         datasource.BasicAuth,
+					BasicAuthUser:     datasource.BasicAuthUser,
+					BasicAuthPassword: datasource.BasicAuthPassword,
 				},
 			},
 		},

+ 27 - 17
pkg/services/alerting/eval_context.go

@@ -28,36 +28,46 @@ type EvalContext struct {
 	ImageOnDiskPath string
 }
 
-func (a *EvalContext) GetDurationMs() float64 {
-	return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
+type StateDescription struct {
+	Color string
+	Text  string
+	Data  string
 }
 
-func (c *EvalContext) GetColor() string {
-	if !c.Firing {
-		return "#36a64f"
-	}
-
-	if c.Rule.Severity == m.AlertSeverityWarning {
-		return "#fd821b"
-	} else {
-		return "#D63232"
+func (c *EvalContext) GetStateModel() *StateDescription {
+	if c.Error != nil {
+		return &StateDescription{
+			Color: "#D63232",
+			Text:  "EXECUTION ERROR",
+		}
 	}
-}
 
-func (c *EvalContext) GetStateText() string {
 	if !c.Firing {
-		return "OK"
+		return &StateDescription{
+			Color: "#36a64f",
+			Text:  "OK",
+		}
 	}
 
 	if c.Rule.Severity == m.AlertSeverityWarning {
-		return "WARNING"
+		return &StateDescription{
+			Color: "#fd821b",
+			Text:  "WARNING",
+		}
 	} else {
-		return "CRITICAL"
+		return &StateDescription{
+			Color: "#D63232",
+			Text:  "CRITICAL",
+		}
 	}
 }
 
+func (a *EvalContext) GetDurationMs() float64 {
+	return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
+}
+
 func (c *EvalContext) GetNotificationTitle() string {
-	return "[" + c.GetStateText() + "] " + c.Rule.Name
+	return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
 }
 
 func (c *EvalContext) getDashboardSlug() (string, error) {

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

@@ -48,7 +48,6 @@ func (n *RootNotifier) Notify(context *EvalContext) {
 
 	for _, notifier := range notifiers {
 		n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
-
 		go notifier.Notify(context)
 	}
 }

+ 1 - 1
pkg/services/alerting/notifiers/email.go

@@ -54,7 +54,7 @@ func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
 			"State":         context.Rule.State,
 			"Name":          context.Rule.Name,
 			"Severity":      context.Rule.Severity,
-			"SeverityColor": context.GetColor(),
+			"SeverityColor": context.GetStateModel().Color,
 			"Message":       context.Rule.Message,
 			"RuleUrl":       ruleUrl,
 			"ImageLink":     context.ImagePublicUrl,

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

@@ -61,13 +61,26 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
 		}
 	}
 
+	if context.Error != nil {
+		fields = append(fields, map[string]interface{}{
+			"title": "Error message",
+			"value": context.Error.Error(),
+			"short": false,
+		})
+	}
+
+	message := ""
+	if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
+		message = context.Rule.Message
+	}
+
 	body := map[string]interface{}{
 		"attachments": []map[string]interface{}{
 			{
-				"color":       context.GetColor(),
+				"color":       context.GetStateModel().Color,
 				"title":       context.GetNotificationTitle(),
 				"title_link":  ruleUrl,
-				"text":        context.Rule.Message,
+				"text":        message,
 				"fields":      fields,
 				"image_url":   context.ImagePublicUrl,
 				"footer":      "Grafana v" + setting.BuildVersion,

+ 20 - 11
pkg/services/alerting/result_handler.go

@@ -4,6 +4,7 @@ import (
 	"time"
 
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
@@ -30,17 +31,20 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
 	oldState := ctx.Rule.State
 
 	exeuctionError := ""
+	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.AlertStateExeuctionError
 		exeuctionError = ctx.Error.Error()
+		annotationData.Set("errorMessage", exeuctionError)
 	} else if ctx.Firing {
 		ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity)
+		annotationData = simplejson.NewFromAny(ctx.EvalMatches)
 	} else {
 		ctx.Rule.State = m.AlertStateOK
 	}
 
-	countSeverity(ctx.Rule.Severity)
+	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)
 
@@ -61,10 +65,11 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
 			Type:      annotations.AlertType,
 			AlertId:   ctx.Rule.Id,
 			Title:     ctx.Rule.Name,
-			Text:      ctx.GetStateText(),
+			Text:      ctx.GetStateModel().Text,
 			NewState:  string(ctx.Rule.State),
 			PrevState: string(oldState),
 			Timestamp: time.Now(),
+			Data:      annotationData,
 		}
 
 		annotationRepo := annotations.GetRepository()
@@ -76,15 +81,19 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
 	}
 }
 
-func countSeverity(state m.AlertSeverityType) {
+func countStateResult(state m.AlertStateType) {
 	switch state {
-	case m.AlertSeverityOK:
-		metrics.M_Alerting_Result_Ok.Inc(1)
-	case m.AlertSeverityInfo:
-		metrics.M_Alerting_Result_Info.Inc(1)
-	case m.AlertSeverityWarning:
-		metrics.M_Alerting_Result_Warning.Inc(1)
-	case m.AlertSeverityCritical:
-		metrics.M_Alerting_Result_Critical.Inc(1)
+	case m.AlertStateCritical:
+		metrics.M_Alerting_Result_State_Critical.Inc(1)
+	case m.AlertStateWarning:
+		metrics.M_Alerting_Result_State_Warning.Inc(1)
+	case m.AlertStateOK:
+		metrics.M_Alerting_Result_State_Ok.Inc(1)
+	case m.AlertStatePaused:
+		metrics.M_Alerting_Result_State_Paused.Inc(1)
+	case m.AlertStatePending:
+		metrics.M_Alerting_Result_State_Pending.Inc(1)
+	case m.AlertStateExeuctionError:
+		metrics.M_Alerting_Result_State_ExecutionError.Inc(1)
 	}
 }

+ 9 - 0
pkg/services/annotations/annotations.go

@@ -8,6 +8,15 @@ import (
 
 type Repository interface {
 	Save(item *Item) error
+	Find(query *ItemQuery) ([]*Item, error)
+}
+
+type ItemQuery struct {
+	OrgId   int64    `json:"orgId"`
+	Type    ItemType `json:"type"`
+	AlertId int64    `json:"alertId"`
+
+	Limit int64 `json:"alertId"`
 }
 
 var repositoryInstance Repository

+ 37 - 0
pkg/services/sqlstore/annotation.go

@@ -1,6 +1,9 @@
 package sqlstore
 
 import (
+	"bytes"
+	"fmt"
+
 	"github.com/go-xorm/xorm"
 	"github.com/grafana/grafana/pkg/services/annotations"
 )
@@ -17,5 +20,39 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
 
 		return nil
 	})
+}
+
+func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
+	var sql bytes.Buffer
+	params := make([]interface{}, 0)
+
+	sql.WriteString(`SELECT *
+						from annotation
+						`)
+
+	sql.WriteString(`WHERE org_id = ?`)
+	params = append(params, query.OrgId)
+
+	if query.AlertId != 0 {
+		sql.WriteString(` AND alert_id = ?`)
+		params = append(params, query.AlertId)
+	}
+
+	if query.Type != "" {
+		sql.WriteString(` AND type = ?`)
+		params = append(params, string(query.Type))
+	}
+
+	if query.Limit == 0 {
+		query.Limit = 10
+	}
+
+	sql.WriteString(fmt.Sprintf("ORDER BY timestamp DESC LIMIT %v", query.Limit))
+
+	items := make([]*annotations.Item, 0)
+	if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
+		return nil, err
+	}
 
+	return items, nil
 }

+ 53 - 11
pkg/tsdb/graphite/graphite.go

@@ -2,9 +2,11 @@ package graphite
 
 import (
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/url"
+	"path"
 	"strings"
 	"time"
 
@@ -12,6 +14,10 @@ import (
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
+var (
+	HttpClient = http.Client{Timeout: time.Duration(10 * time.Second)}
+)
+
 type GraphiteExecutor struct {
 	*tsdb.DataSourceInfo
 }
@@ -30,7 +36,7 @@ func init() {
 func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
 	result := &tsdb.BatchResult{}
 
-	params := url.Values{
+	formData := url.Values{
 		"from":          []string{"-" + formatTimeRange(context.TimeRange.From)},
 		"until":         []string{formatTimeRange(context.TimeRange.To)},
 		"format":        []string{"json"},
@@ -38,28 +44,24 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
 	}
 
 	for _, query := range queries {
-		params["target"] = []string{query.Query}
-		glog.Debug("Graphite request", "query", query.Query)
+		formData["target"] = []string{query.Query}
 	}
 
-	client := http.Client{Timeout: time.Duration(10 * time.Second)}
-	res, err := client.PostForm(e.Url+"/render?", params)
+	glog.Info("Graphite request body", "formdata", formData.Encode())
+
+	req, err := e.createRequest(formData)
 	if err != nil {
 		result.Error = err
 		return result
 	}
-	defer res.Body.Close()
-
-	body, err := ioutil.ReadAll(res.Body)
+	res, err := HttpClient.Do(req)
 	if err != nil {
 		result.Error = err
 		return result
 	}
 
-	var data []TargetResponseDTO
-	err = json.Unmarshal(body, &data)
+	data, err := e.parseResponse(res)
 	if err != nil {
-		glog.Info("Failed to unmarshal graphite response", "error", err, "body", string(body))
 		result.Error = err
 		return result
 	}
@@ -77,6 +79,46 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
 	return result
 }
 
+func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDTO, error) {
+	body, err := ioutil.ReadAll(res.Body)
+	defer res.Body.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	if res.StatusCode == http.StatusUnauthorized {
+		glog.Info("Request is Unauthorized", "status", res.Status, "body", string(body))
+		return nil, fmt.Errorf("Request is Unauthorized status: %v body: %s", res.Status, string(body))
+	}
+
+	var data []TargetResponseDTO
+	err = json.Unmarshal(body, &data)
+	if err != nil {
+		glog.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body))
+		return nil, err
+	}
+
+	return data, nil
+}
+
+func (e *GraphiteExecutor) createRequest(data url.Values) (*http.Request, error) {
+	u, _ := url.Parse(e.Url)
+	u.Path = path.Join(u.Path, "render")
+
+	req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode()))
+	if err != nil {
+		glog.Info("Failed to create request", "error", err)
+		return nil, fmt.Errorf("Failed to create request. error: %v", err)
+	}
+
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	if e.BasicAuth {
+		req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
+	}
+
+	return req, err
+}
+
 func formatTimeRange(input string) string {
 	if input == "now" {
 		return input

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

@@ -113,7 +113,7 @@ function (_, $, coreModule) {
           if (str[0] === '/') { str = str.substring(1); }
           if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
           try {
-            return item.toLowerCase().match(str);
+            return item.toLowerCase().match(str.toLowerCase());
           } catch(e) {
             return false;
           }

+ 18 - 0
public/app/features/alerting/alert_tab_ctrl.ts

@@ -5,6 +5,7 @@ import {ThresholdMapper} from './threshold_mapper';
 import {QueryPart} from 'app/core/components/query_part/query_part';
 import alertDef from './alert_def';
 import config from 'app/core/config';
+import moment from 'moment';
 
 export class AlertTabCtrl {
   panel: any;
@@ -22,6 +23,7 @@ export class AlertTabCtrl {
   alertNotifications;
   error: string;
   appSubUrl: string;
+  alertHistory: any;
 
   /** @ngInject */
   constructor(private $scope,
@@ -60,6 +62,7 @@ export class AlertTabCtrl {
     // build notification model
     this.notifications = [];
     this.alertNotifications = [];
+    this.alertHistory = [];
 
     return this.backendSrv.get('/api/alert-notifications').then(res => {
       this.notifications = res;
@@ -71,6 +74,19 @@ export class AlertTabCtrl {
           this.alertNotifications.push(model);
         }
       });
+    }).then(() => {
+      this.backendSrv.get(`/api/alert-history?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}`).then(res => {
+        this.alertHistory = _.map(res, ah => {
+          ah.time = moment(ah.timestamp).format('MMM D, YYYY HH:mm:ss');
+          ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
+
+          ah.metrics = _.map(ah.data, ev=> {
+            return ev.Metric + "=" + ev.Value;
+          }).join(', ');
+
+          return ah;
+        });
+      });
     });
   }
 
@@ -192,6 +208,8 @@ export class AlertTabCtrl {
           this.error = 'Currently the alerting backend only supports Graphite queries';
         } else if (this.templateSrv.variableExists(foundTarget.target)) {
           this.error = 'Template variables are not supported in alert queries';
+        } else {
+          this.error = '';
         }
       });
     }

+ 1 - 1
public/app/features/alerting/notification_edit_ctrl.ts

@@ -38,8 +38,8 @@ export class AlertNotificationEditCtrl {
       });
     } else {
       this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => {
-        this.$location.path('alerting/notification/' + res.id + '/edit');
         this.$scope.appEvent('alert-success', ['Notification created', '']);
+        this.$location.path('alerting/notifications');
       });
     }
   }

+ 25 - 0
public/app/features/alerting/partials/alert_tab.html

@@ -122,6 +122,31 @@
 				<textarea class="gf-form-input width-20" rows="10" ng-model="ctrl.alert.message"  placeholder="Notification message details..."></textarea>
 			</div>
 		</div>
+
+		<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
+			<h5 class="section-heading">Alert history</h5>
+			<section class="card-section card-list-layout-list">
+				<ol class="card-list" >
+					<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
+						<div class="card-item card-item--alert">
+							<div class="card-item-body">
+								<div class="card-item-details">
+									<div class="card-item-sub-name">
+										<span class="alert-list-item-state {{ah.stateModel.stateClass}}">
+											<i class="{{ah.stateModel.iconClass}}"></i>
+											{{ah.stateModel.text}}
+										</span> {{ah.metrics}}
+									</div>
+									<div class="card-item-sub-name">
+										{{ah.time}}
+									</div>
+								</div>
+							</div>
+						</div>
+					</li>
+				</ol>
+			</section>
+		</div>
 	</div>
 </div>
 

+ 1 - 1
public/app/features/dashboard/import/dash_import.html

@@ -36,7 +36,7 @@
 
 			<div class="gf-form-group">
 				<div class="gf-form">
-					<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-ctrl="ctrl.jsonText"></textarea>
+					<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-model="ctrl.jsonText"></textarea>
 				</div>
 				<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
 					<i class="fa fa-paste"></i>

+ 9 - 0
public/app/plugins/datasource/influxdb/query_part.ts

@@ -254,6 +254,15 @@ register({
   renderer: functionRenderer,
 });
 
+register({
+  type: 'elapsed',
+  addStrategy: addTransformationStrategy,
+  category: categories.Transformations,
+  params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}],
+  defaultParams: ['10s'],
+  renderer: functionRenderer,
+});
+
 // Selectors
 register({
   type: 'bottom',

+ 1 - 1
public/sass/components/edit_sidemenu.scss

@@ -5,7 +5,7 @@
 }
 
 .edit-sidemenu-aside {
-  width: 14rem;
+  width: 16rem;
 }
 
 .edit-sidemenu {

+ 0 - 5
symlink_git_hooks.sh

@@ -1,5 +0,0 @@
-#/bin/bash
-
-#ln -s -f .hooks/* .git/hooks/
-cd .git/hooks/
-cp --symbolic-link -f ../../.hooks/* .