diff --git a/CHANGELOG.md b/CHANGELOG.md index 0352a2a3f8f..c4614d024d3 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/README.md b/README.md index 6dbfc5388c2..f8d1db6cd07 100644 --- a/README.md +++ b/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) diff --git a/build.go b/build.go index 4347c486063..9b8aae6a68a 100644 --- a/build.go +++ b/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) diff --git a/circle.yml b/circle.yml index ef11d6b038e..5640980b702 100644 --- a/circle.yml +++ b/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} \ No newline at end of file diff --git a/conf/sample.ini b/conf/sample.ini index 4fcbe5d1157..949f64df025 100644 --- a/conf/sample.ini +++ b/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 diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 92f4367d9ae..69338eaba40 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/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)` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index af3bb796e2d..4bb4d262d46 100644 --- a/docs/sources/installation/configuration.md +++ b/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 diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index f20e08a5cad..80b5108bbad 100644 --- a/pkg/api/alerting.go +++ b/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 +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 31184a5a095..132c609dde4 100644 --- a/pkg/api/api.go +++ b/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() { diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 62a83bdaa0b..18b48cd8e29 100644 --- a/pkg/api/datasources.go +++ b/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 { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 0ec0174a949..d5fbb2c3a9f 100644 --- a/pkg/api/dtos/alerting.go +++ b/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"` +} diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 2099a576647..b301924d956 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/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) diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go index 1a6df719053..636292cce11 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go +++ b/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 diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 9477f2738b5..80c7520eaf2 100644 --- a/pkg/metrics/metrics.go +++ b/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") diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index d94c2d468f2..0a661e8aaab 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/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, }, }, }, diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 70dc6b17a2f..29c51e02abd 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -28,36 +28,46 @@ type EvalContext struct { ImageOnDiskPath string } +type StateDescription struct { + Color string + Text string + Data string +} + +func (c *EvalContext) GetStateModel() *StateDescription { + if c.Error != nil { + return &StateDescription{ + Color: "#D63232", + Text: "EXECUTION ERROR", + } + } + + if !c.Firing { + return &StateDescription{ + Color: "#36a64f", + Text: "OK", + } + } + + if c.Rule.Severity == m.AlertSeverityWarning { + return &StateDescription{ + Color: "#fd821b", + Text: "WARNING", + } + } else { + return &StateDescription{ + Color: "#D63232", + Text: "CRITICAL", + } + } +} + func (a *EvalContext) GetDurationMs() float64 { return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000) } -func (c *EvalContext) GetColor() string { - if !c.Firing { - return "#36a64f" - } - - if c.Rule.Severity == m.AlertSeverityWarning { - return "#fd821b" - } else { - return "#D63232" - } -} - -func (c *EvalContext) GetStateText() string { - if !c.Firing { - return "OK" - } - - if c.Rule.Severity == m.AlertSeverityWarning { - return "WARNING" - } else { - return "CRITICAL" - } -} - func (c *EvalContext) GetNotificationTitle() string { - return "[" + c.GetStateText() + "] " + c.Rule.Name + return "[" + c.GetStateModel().Text + "] " + c.Rule.Name } func (c *EvalContext) getDashboardSlug() (string, error) { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 826aede2d11..c345594de2c 100644 --- a/pkg/services/alerting/notifier.go +++ b/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) } } diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 20b0bc06463..d63ca7d13f5 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/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, diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index e099c7da690..837621e80bb 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/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, diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 09c77c8edd7..f22afb279d8 100644 --- a/pkg/services/alerting/result_handler.go +++ b/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) } } diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index fd8c8ebdb9a..06be152ec31 100644 --- a/pkg/services/annotations/annotations.go +++ b/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 diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 0530952144e..5cf4461b370 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/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 } diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 2a68b23df71..0247ad0dca8 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/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 diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 4b3cd2e8de3..7669f2fb709 100644 --- a/public/app/core/directives/metric_segment.js +++ b/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; } diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 2f222802438..c5ed6187f35 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/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 = ''; } }); } diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index fe7aac197bc..fc33118cb00 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/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'); }); } } diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index a386d23945b..e755496b732 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -122,6 +122,31 @@ + +