diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 2c2acb2ca56..ae909734617 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -25,6 +25,7 @@ be ready to build dashboards for you CloudWatch metrics. 3. Click the `Add new` link in the top header. 4. Select `CloudWatch` from the dropdown. + > NOTE: If at any moment you have issues with getting this datasource to work and grafana is giving you undescriptive errors then dont forget to check your log file (try looking in /var/log/grafana/). Name | Description ------------ | ------------- @@ -47,6 +48,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu ### AWS credentials file Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server. + > NOTE: If you think you have the credentials file in the right place but it is still not working then you might try moving your .aws file to '/usr/share/grafana/' and make sure your credentials file has at most 0644 permissions. Example content: diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index ff4f51729af..5880d18c0e3 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -34,11 +34,16 @@ List available plugins grafana-cli plugins list-remote ``` -Install a plugin type +Install the latest version of a plugin ``` grafana-cli plugins install ``` +Install a specific version of a plugin +``` +grafana-cli plugins install +``` + List installed plugins ``` grafana-cli plugins ls diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index e745f820aec..56c02baa744 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -264,7 +264,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { return ApiError(500, "", err) } - var response models.AlertStateType = models.AlertStateNoData + var response models.AlertStateType = models.AlertStatePending pausedState := "un paused" if cmd.Paused { response = models.AlertStatePaused diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index 08a4bf4693e..6c739f8b0ee 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/ioutil" + "net" "net/http" "net/url" "path" @@ -25,6 +26,16 @@ func Init(version string) { grafanaVersion = version tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 002f2369c9b..1cdcc71e132 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -41,6 +41,7 @@ var ( M_Alerting_Result_State_Paused Counter M_Alerting_Result_State_NoData Counter M_Alerting_Result_State_ExecError Counter + M_Alerting_Result_State_Pending Counter M_Alerting_Active_Alerts Counter M_Alerting_Notification_Sent_Slack Counter M_Alerting_Notification_Sent_Email Counter @@ -102,6 +103,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused") M_Alerting_Result_State_NoData = RegCounter("alerting.result", "state", "no_data") M_Alerting_Result_State_ExecError = RegCounter("alerting.result", "state", "exec_error") + M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending") M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts") M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack") diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 7531be90e88..ec2e8e18841 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -11,11 +11,12 @@ type AlertSeverityType string type NoDataOption string const ( - AlertStateNoData AlertStateType = "no_data" - AlertStateExecError AlertStateType = "execution_error" - AlertStatePaused AlertStateType = "paused" - AlertStateAlerting AlertStateType = "alerting" - AlertStateOK AlertStateType = "ok" + AlertStateNoData AlertStateType = "no_data" + AlertStateExecError AlertStateType = "execution_error" + AlertStatePaused AlertStateType = "paused" + AlertStateAlerting AlertStateType = "alerting" + AlertStateOK AlertStateType = "ok" + AlertStatePending AlertStateType = "pending" ) const ( @@ -26,7 +27,7 @@ const ( ) func (s AlertStateType) IsValid() bool { - return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused + return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused || s == AlertStatePending } func (s NoDataOption) IsValid() bool { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index a9a99ba919e..b73db9d590e 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -33,15 +33,17 @@ type AlertQuery struct { To string } -func (c *QueryCondition) Eval(context *alerting.EvalContext) { +func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.ConditionResult, error) { timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To) + seriesList, err := c.executeQuery(context, timeRange) if err != nil { - context.Error = err - return + return nil, err } emptySerieCount := 0 + evalMatchCount := 0 + var matches []*alerting.EvalMatch for _, series := range seriesList { reducedValue := c.Reducer.Reduce(series) evalMatch := c.Evaluator.Eval(reducedValue) @@ -58,15 +60,20 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { } if evalMatch { - context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{ + evalMatchCount++ + + matches = append(matches, &alerting.EvalMatch{ Metric: series.Name, Value: reducedValue.Float64, }) } } - context.NoDataFound = emptySerieCount == len(seriesList) - context.Firing = len(context.EvalMatches) > 0 + return &alerting.ConditionResult{ + Firing: evalMatchCount > 0, + NoDataFound: emptySerieCount == len(seriesList), + EvalMatches: matches, + }, nil } func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index 43e0381a80c..c3797beaf37 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -46,19 +46,19 @@ func TestQueryCondition(t *testing.T) { Convey("should fire when avg is above 100", func() { points := tsdb.NewTimeSeriesPointsFromArgs(120, 0) ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)} - ctx.exec() + cr, err := ctx.exec() - So(ctx.result.Error, ShouldBeNil) - So(ctx.result.Firing, ShouldBeTrue) + So(err, ShouldBeNil) + So(cr.Firing, ShouldBeTrue) }) Convey("Should not fire when avg is below 100", func() { points := tsdb.NewTimeSeriesPointsFromArgs(90, 0) ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)} - ctx.exec() + cr, err := ctx.exec() - So(ctx.result.Error, ShouldBeNil) - So(ctx.result.Firing, ShouldBeFalse) + So(err, ShouldBeNil) + So(cr.Firing, ShouldBeFalse) }) Convey("Should fire if only first serie matches", func() { @@ -66,10 +66,10 @@ func TestQueryCondition(t *testing.T) { tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)), } - ctx.exec() + cr, err := ctx.exec() - So(ctx.result.Error, ShouldBeNil) - So(ctx.result.Firing, ShouldBeTrue) + So(err, ShouldBeNil) + So(cr.Firing, ShouldBeTrue) }) Convey("Empty series", func() { @@ -78,10 +78,10 @@ func TestQueryCondition(t *testing.T) { tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()), } - ctx.exec() + cr, err := ctx.exec() - So(ctx.result.Error, ShouldBeNil) - So(ctx.result.NoDataFound, ShouldBeTrue) + So(err, ShouldBeNil) + So(cr.NoDataFound, ShouldBeTrue) }) Convey("Should set NoDataFound both series contains null", func() { @@ -89,10 +89,10 @@ func TestQueryCondition(t *testing.T) { tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}), tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}), } - ctx.exec() + cr, err := ctx.exec() - So(ctx.result.Error, ShouldBeNil) - So(ctx.result.NoDataFound, ShouldBeTrue) + So(err, ShouldBeNil) + So(cr.NoDataFound, ShouldBeTrue) }) Convey("Should not set NoDataFound if one serie is empty", func() { @@ -100,10 +100,10 @@ func TestQueryCondition(t *testing.T) { tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)), } - ctx.exec() + cr, err := ctx.exec() - So(ctx.result.Error, ShouldBeNil) - So(ctx.result.NoDataFound, ShouldBeFalse) + So(err, ShouldBeNil) + So(cr.NoDataFound, ShouldBeFalse) }) }) }) @@ -120,7 +120,7 @@ type queryConditionTestContext struct { type queryConditionScenarioFunc func(c *queryConditionTestContext) -func (ctx *queryConditionTestContext) exec() { +func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) { jsonModel, err := simplejson.NewJson([]byte(`{ "type": "query", "query": { @@ -146,7 +146,7 @@ func (ctx *queryConditionTestContext) exec() { }, nil } - condition.Eval(ctx.result) + return condition.Eval(ctx.result) } func queryConditionScenario(desc string, fn queryConditionScenarioFunc) { diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 74054ba8191..538c639abb8 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -20,8 +20,12 @@ func NewEvalHandler() *DefaultEvalHandler { } func (e *DefaultEvalHandler) Eval(context *EvalContext) { + firing := true for _, condition := range context.Rule.Conditions { - condition.Eval(context) + cr, err := condition.Eval(context) + if err != nil { + context.Error = err + } // break if condition could not be evaluated if context.Error != nil { @@ -29,11 +33,15 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { } // break if result has not triggered yet - if context.Firing == false { + if cr.Firing == false { + firing = false break } + + context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...) } + context.Firing = firing context.EndTime = time.Now() elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond metrics.M_Alerting_Exeuction_Time.Update(elapsedTime) diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index b69e62f9622..4c2ec24b506 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -8,11 +8,12 @@ import ( ) type conditionStub struct { - firing bool + firing bool + matches []*EvalMatch } -func (c *conditionStub) Eval(context *EvalContext) { - context.Firing = c.firing +func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) { + return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil } func TestAlertingExecutor(t *testing.T) { @@ -30,10 +31,10 @@ func TestAlertingExecutor(t *testing.T) { So(context.Firing, ShouldEqual, true) }) - Convey("Show return false with not passing condition", func() { + Convey("Show return false with not passing asdf", func() { context := NewEvalContext(context.TODO(), &Rule{ Conditions: []Condition{ - &conditionStub{firing: true}, + &conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}}, &conditionStub{firing: false}, }, }) diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index d78e84f6974..786fae846ad 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -3,6 +3,8 @@ package alerting import ( "errors" + "fmt" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" @@ -104,7 +106,8 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { panelQuery := findPanelQueryByRefId(panel, queryRefId) if panelQuery == nil { - return nil, ValidationError{Reason: "Alert refes to query that cannot be found"} + reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId) + return nil, ValidationError{Reason: reason} } dsName := "" diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 583e12a120d..cc2561473e3 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -21,6 +21,12 @@ type Notifier interface { GetIsDefault() bool } +type ConditionResult struct { + Firing bool + NoDataFound bool + EvalMatches []*EvalMatch +} + type Condition interface { - Eval(result *EvalContext) + Eval(result *EvalContext) (*ConditionResult, error) } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 2017d9d7670..3fe7661b78c 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -49,7 +49,7 @@ func (n *RootNotifier) Notify(context *EvalContext) error { return err } - n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "Amount to send", len(notifiers)) + n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers)) if len(notifiers) == 0 { return nil diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 7238af179d7..e7b6ab79456 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -22,17 +22,21 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } + recipient := model.Settings.Get("recipient").MustString() + return &SlackNotifier{ NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), Url: url, + Recipient: recipient, log: log.New("alerting.notifier.slack"), }, nil } type SlackNotifier struct { NotifierBase - Url string - log log.Logger + Url string + Recipient string + log log.Logger } func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { @@ -85,6 +89,12 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { "ts": time.Now().Unix(), }, }, + "parse": "full", // to linkify urls, users and channels in alert message. + } + + //recipient override + if this.Recipient != "" { + body["channel"] = this.Recipient } data, _ := json.Marshal(&body) diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index d786e8d599d..d2ad9345416 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -86,7 +86,12 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { handler.log.Error("Failed to save annotation for new alert state", "error", err) } - handler.notifier.Notify(evalContext) + if (oldState == m.AlertStatePending) && (evalContext.Rule.State == m.AlertStateOK) { + handler.log.Info("Notfication not sent", "oldState", oldState, "newState", evalContext.Rule.State) + } else { + handler.notifier.Notify(evalContext) + } + } return nil @@ -98,6 +103,8 @@ func (handler *DefaultResultHandler) shouldUpdateAlertState(evalContext *EvalCon func countStateResult(state m.AlertStateType) { switch state { + case m.AlertStatePending: + metrics.M_Alerting_Result_State_Pending.Inc(1) case m.AlertStateAlerting: metrics.M_Alerting_Result_State_Alerting.Inc(1) case m.AlertStateOK: diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go index 6144c01d54d..f8761efcd90 100644 --- a/pkg/services/alerting/rule_test.go +++ b/pkg/services/alerting/rule_test.go @@ -10,7 +10,9 @@ import ( type FakeCondition struct{} -func (f *FakeCondition) Eval(context *EvalContext) {} +func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) { + return &ConditionResult{}, nil +} func TestAlertRuleModel(t *testing.T) { Convey("Testing alert rule", t, func() { diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 9e54e906d99..4bcce804ee4 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -99,7 +99,7 @@ func createDialer() (*gomail.Dialer, error) { tlsconfig.Certificates = []tls.Certificate{cert} } - d := gomail.NewPlainDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password) + d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password) d.TLSConfig = tlsconfig return d, nil } diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 4824b000bcb..44693f8d474 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -173,7 +173,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor } else { alert.Updated = time.Now() alert.Created = time.Now() - alert.State = m.AlertStateNoData + alert.State = m.AlertStatePending alert.NewStateDate = time.Now() _, err := sess.Insert(alert) @@ -260,7 +260,7 @@ func PauseAlertRule(cmd *m.PauseAlertCommand) error { if cmd.Paused { newState = m.AlertStatePaused } else { - newState = m.AlertStateNoData + newState = m.AlertStatePending } alert.State = newState diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index 02126c2984a..e22b1c48c47 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -47,7 +47,7 @@ func TestAlertingDataAccess(t *testing.T) { So(err2, ShouldBeNil) So(alert.Name, ShouldEqual, "Alerting title") So(alert.Message, ShouldEqual, "Alerting message") - So(alert.State, ShouldEqual, "no_data") + So(alert.State, ShouldEqual, "pending") So(alert.Frequency, ShouldEqual, 1) }) @@ -77,7 +77,7 @@ func TestAlertingDataAccess(t *testing.T) { So(query.Result[0].Name, ShouldEqual, "Name") Convey("Alert state should not be updated", func() { - So(query.Result[0].State, ShouldEqual, "no_data") + So(query.Result[0].State, ShouldEqual, "pending") }) }) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index a2783a107fe..3d6af5beb5b 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -2,15 +2,14 @@ package graphite import ( "context" - "crypto/tls" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "path" + "regexp" "strings" - "time" "golang.org/x/net/context/ctxhttp" @@ -36,14 +35,7 @@ func init() { glog = log.New("tsdb.graphite") tsdb.RegisterExecutor("graphite", NewGraphiteExecutor) - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - - HttpClient = &http.Client{ - Timeout: time.Duration(15 * time.Second), - Transport: tr, - } + HttpClient = tsdb.GetDefaultClient() } func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { @@ -58,9 +50,9 @@ func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, for _, query := range queries { if fullTarget, err := query.Model.Get("targetFull").String(); err == nil { - formData["target"] = []string{fullTarget} + formData["target"] = []string{fixIntervalFormat(fullTarget)} } else { - formData["target"] = []string{query.Model.Get("target").MustString()} + formData["target"] = []string{fixIntervalFormat(query.Model.Get("target").MustString())} } } @@ -150,3 +142,17 @@ func formatTimeRange(input string) string { } return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1) } + +func fixIntervalFormat(target string) string { + rMinute := regexp.MustCompile(`'(\d+)m'`) + rMin := regexp.MustCompile("m") + target = rMinute.ReplaceAllStringFunc(target, func(m string) string { + return rMin.ReplaceAllString(m, "min") + }) + rMonth := regexp.MustCompile(`'(\d+)M'`) + rMon := regexp.MustCompile("M") + target = rMonth.ReplaceAllStringFunc(target, func(M string) string { + return rMon.ReplaceAllString(M, "mon") + }) + return target +} diff --git a/pkg/tsdb/graphite/graphite_test.go b/pkg/tsdb/graphite/graphite_test.go index 7a3bba9035b..c1a2736293b 100644 --- a/pkg/tsdb/graphite/graphite_test.go +++ b/pkg/tsdb/graphite/graphite_test.go @@ -1 +1,61 @@ package graphite + +import ( + . "github.com/smartystreets/goconvey/convey" + "testing" +) + +func TestGraphiteFunctions(t *testing.T) { + Convey("Testing Graphite Functions", t, func() { + + Convey("formatting time range for now", func() { + + timeRange := formatTimeRange("now") + So(timeRange, ShouldEqual, "now") + + }) + + Convey("formatting time range for now-1m", func() { + + timeRange := formatTimeRange("now-1m") + So(timeRange, ShouldEqual, "now-1min") + + }) + + Convey("formatting time range for now-1M", func() { + + timeRange := formatTimeRange("now-1M") + So(timeRange, ShouldEqual, "now-1mon") + + }) + + Convey("fix interval format in query for 1m", func() { + + timeRange := fixIntervalFormat("aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1m'), 4)") + So(timeRange, ShouldEqual, "aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1min'), 4)") + + }) + + Convey("fix interval format in query for 1M", func() { + + timeRange := fixIntervalFormat("aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1M'), 4)") + So(timeRange, ShouldEqual, "aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1mon'), 4)") + + }) + + Convey("should not override query for 1M", func() { + + timeRange := fixIntervalFormat("app.grafana.*.dashboards.views.1M.count") + So(timeRange, ShouldEqual, "app.grafana.*.dashboards.views.1M.count") + + }) + + Convey("should not override query for 1m", func() { + + timeRange := fixIntervalFormat("app.grafana.*.dashboards.views.1m.count") + So(timeRange, ShouldEqual, "app.grafana.*.dashboards.views.1m.count") + + }) + + }) +} diff --git a/pkg/tsdb/http.go b/pkg/tsdb/http.go new file mode 100644 index 00000000000..f5de146d470 --- /dev/null +++ b/pkg/tsdb/http.go @@ -0,0 +1,29 @@ +package tsdb + +import ( + "crypto/tls" + "net" + "net/http" + "time" +) + +func GetDefaultClient() *http.Client { + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + return &http.Client{ + Timeout: time.Duration(30 * time.Second), + Transport: tr, + } +} diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index b546a6ee3a9..5e5b893ad23 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -2,13 +2,11 @@ package influxdb import ( "context" - "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "path" - "time" "golang.org/x/net/context/ctxhttp" @@ -41,14 +39,7 @@ func init() { glog = log.New("tsdb.influxdb") tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor) - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - - HttpClient = &http.Client{ - Timeout: time.Duration(15 * time.Second), - Transport: tr, - } + HttpClient = tsdb.GetDefaultClient() } func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 3ecd52ca723..c5e5b0020d1 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -2,19 +2,17 @@ package opentsdb import ( "context" - "crypto/tls" "fmt" "path" "strconv" "strings" - "time" "golang.org/x/net/context/ctxhttp" + "encoding/json" "io/ioutil" "net/http" "net/url" - "encoding/json" "gopkg.in/guregu/null.v3" @@ -40,14 +38,7 @@ func init() { plog = log.New("tsdb.opentsdb") tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor) - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - - HttpClient = &http.Client{ - Timeout: time.Duration(15 * time.Second), - Transport: tr, - } + HttpClient = tsdb.GetDefaultClient() } func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { @@ -58,9 +49,9 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, tsdbQuery.Start = queryContext.TimeRange.GetFromAsMsEpoch() tsdbQuery.End = queryContext.TimeRange.GetToAsMsEpoch() - for _ , query := range queries { - metric := e.buildMetric(query) - tsdbQuery.Queries = append(tsdbQuery.Queries, metric) + for _, query := range queries { + metric := e.buildMetric(query) + tsdbQuery.Queries = append(tsdbQuery.Queries, metric) } if setting.Env == setting.DEV { @@ -104,7 +95,7 @@ func (e *OpenTsdbExecutor) createRequest(data OpenTsdbQuery) (*http.Request, err if e.BasicAuth { req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) } - + return req, err } @@ -152,61 +143,61 @@ func (e *OpenTsdbExecutor) parseResponse(query OpenTsdbQuery, res *http.Response return queryResults, nil } -func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) (map[string]interface{}) { +func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) map[string]interface{} { metric := make(map[string]interface{}) - // Setting metric and aggregator - metric["metric"] = query.Model.Get("metric").MustString() - metric["aggregator"] = query.Model.Get("aggregator").MustString() + // Setting metric and aggregator + metric["metric"] = query.Model.Get("metric").MustString() + metric["aggregator"] = query.Model.Get("aggregator").MustString() - // Setting downsampling options - disableDownsampling := query.Model.Get("disableDownsampling").MustBool() - if !disableDownsampling { - downsampleInterval := query.Model.Get("downsampleInterval").MustString() - if downsampleInterval == "" { - downsampleInterval = "1m" //default value for blank - } - downsample := downsampleInterval + "-" + query.Model.Get("downsampleAggregator").MustString() - if query.Model.Get("downsampleFillPolicy").MustString() != "none" { - metric["downsample"] = downsample + "-" + query.Model.Get("downsampleFillPolicy").MustString() - } else { - metric["downsample"] = downsample - } + // Setting downsampling options + disableDownsampling := query.Model.Get("disableDownsampling").MustBool() + if !disableDownsampling { + downsampleInterval := query.Model.Get("downsampleInterval").MustString() + if downsampleInterval == "" { + downsampleInterval = "1m" //default value for blank + } + downsample := downsampleInterval + "-" + query.Model.Get("downsampleAggregator").MustString() + if query.Model.Get("downsampleFillPolicy").MustString() != "none" { + metric["downsample"] = downsample + "-" + query.Model.Get("downsampleFillPolicy").MustString() + } else { + metric["downsample"] = downsample + } + } + + // Setting rate options + if query.Model.Get("shouldComputeRate").MustBool() { + + metric["rate"] = true + rateOptions := make(map[string]interface{}) + rateOptions["counter"] = query.Model.Get("isCounter").MustBool() + + counterMax, counterMaxCheck := query.Model.CheckGet("counterMax") + if counterMaxCheck { + rateOptions["counterMax"] = counterMax.MustFloat64() } - // Setting rate options - if query.Model.Get("shouldComputeRate").MustBool() { - - metric["rate"] = true - rateOptions := make(map[string]interface{}) - rateOptions["counter"] = query.Model.Get("isCounter").MustBool() - - counterMax, counterMaxCheck := query.Model.CheckGet("counterMax") - if counterMaxCheck { - rateOptions["counterMax"] = counterMax.MustFloat64() - } - - resetValue, resetValueCheck := query.Model.CheckGet("counterResetValue") - if resetValueCheck { - rateOptions["resetValue"] = resetValue.MustFloat64() - } - - metric["rateOptions"] = rateOptions + resetValue, resetValueCheck := query.Model.CheckGet("counterResetValue") + if resetValueCheck { + rateOptions["resetValue"] = resetValue.MustFloat64() } - // Setting tags - tags, tagsCheck := query.Model.CheckGet("tags") - if tagsCheck && len(tags.MustMap()) > 0 { - metric["tags"] = tags.MustMap() - } + metric["rateOptions"] = rateOptions + } - // Setting filters - filters, filtersCheck := query.Model.CheckGet("filters") - if filtersCheck && len(filters.MustArray()) > 0 { - metric["filters"] = filters.MustArray() - } + // Setting tags + tags, tagsCheck := query.Model.CheckGet("tags") + if tagsCheck && len(tags.MustMap()) > 0 { + metric["tags"] = tags.MustMap() + } - return metric + // Setting filters + filters, filtersCheck := query.Model.CheckGet("filters") + if filtersCheck && len(filters.MustArray()) > 0 { + metric["filters"] = filters.MustArray() + } + + return metric } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 6dc4146ad0e..ec5b098cbaf 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -3,7 +3,6 @@ package prometheus import ( "context" "fmt" - "net/http" "regexp" "strings" "time" @@ -25,8 +24,7 @@ func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { } var ( - plog log.Logger - HttpClient http.Client + plog log.Logger ) func init() { @@ -83,6 +81,10 @@ func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlic func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string { reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) + if query.LegendFormat == "" { + return metric.String() + } + result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { labelName := strings.Replace(string(in), "{{", "", 1) labelName = strings.Replace(labelName, "}}", "", 1) @@ -110,10 +112,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom return nil, err } - format, err := queryModel.Model.Get("legendFormat").String() - if err != nil { - return nil, err - } + format := queryModel.Model.Get("legendFormat").MustString("") start, err := queryContext.TimeRange.ParseFrom() if err != nil { @@ -158,9 +157,3 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb queryResults["A"] = queryRes return queryResults, nil } - -/* -func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult { - result.Error = err - return result -}*/ diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index a4c38cae582..d66ef75e479 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -22,5 +22,19 @@ func TestPrometheus(t *testing.T) { So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}") }) + + Convey("build full serie name", func() { + metric := map[p.LabelName]p.LabelValue{ + p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"), + p.LabelName("app"): p.LabelValue("backend"), + p.LabelName("device"): p.LabelValue("mobile"), + } + + query := &PrometheusQuery{ + LegendFormat: "", + } + + So(formatLegend(metric, query), ShouldEqual, `http_request_total{app="backend", device="mobile"}`) + }) }) } diff --git a/public/app/features/admin/partials/settings.html b/public/app/features/admin/partials/settings.html index 5e070f4d3c0..5371670a9ba 100644 --- a/public/app/features/admin/partials/settings.html +++ b/public/app/features/admin/partials/settings.html @@ -7,7 +7,7 @@
- These system settings are defined in grafana.ini or grafana.custom.ini (or overriden in ENV variables). + These system settings are defined in grafana.ini or custom.ini (or overriden in ENV variables). To change these you currently need to restart grafana.
diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts index 9c567d2ebc5..8696e593074 100644 --- a/public/app/features/alerting/alert_def.ts +++ b/public/app/features/alerting/alert_def.ts @@ -87,6 +87,13 @@ function getStateDisplayModel(state) { stateClass: 'alert-state-paused' }; } + case 'pending': { + return { + text: 'PENDING', + iconClass: "fa fa-exclamation", + stateClass: 'alert-state-warning' + }; + } } } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index c0bbf49f77b..47999b5289d 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -59,10 +59,21 @@

Slack settings

-
+
Url
+
+ Recipient + + + + Override default channel or user, use #channel-name or @username + +
diff --git a/public/app/features/templating/constant_variable.ts b/public/app/features/templating/constant_variable.ts index bf31dba96f9..59659459f85 100644 --- a/public/app/features/templating/constant_variable.ts +++ b/public/app/features/templating/constant_variable.ts @@ -16,6 +16,7 @@ export class ConstantVariable implements Variable { label: '', query: '', current: {}, + options: [], }; /** @ngInject **/ diff --git a/public/app/features/templating/specs/template_srv_specs.ts b/public/app/features/templating/specs/template_srv_specs.ts index 94b1e211293..ca336e97ba3 100644 --- a/public/app/features/templating/specs/template_srv_specs.ts +++ b/public/app/features/templating/specs/template_srv_specs.ts @@ -145,6 +145,11 @@ describe('templateSrv', function() { expect(result).to.be('test|test2'); }); + it('multi value and distributed should render distributed string', function() { + var result = _templateSrv.formatValue(['test','test2'], 'distributed', { name: 'build' }); + expect(result).to.be('test,build=test2'); + }); + it('slash should be properly escaped in regex format', function() { var result = _templateSrv.formatValue('Gi3/14', 'regex'); expect(result).to.be('Gi3\\/14'); diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index dadb8f23a89..e4627ac6165 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -95,6 +95,9 @@ function (angular, _, kbn) { } return value.join('|'); } + case "distributed": { + return this.distributeVariable(value, variable.name); + } default: { if (typeof value === 'string') { return value; @@ -210,6 +213,17 @@ function (angular, _, kbn) { }); }; + this.distributeVariable = function(value, variable) { + value = _.map(value, function(val, index) { + if (index !== 0) { + return variable + "=" + val; + } else { + return val; + } + }); + return value.join(','); + }; + }); }); diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index 4ef7c9761fe..76ee763cafc 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -244,7 +244,7 @@ function (angular, _, dateMath) { var interpolated; try { - interpolated = templateSrv.replace(query); + interpolated = templateSrv.replace(query, {}, 'distributed'); } catch (err) { return $q.reject(err); diff --git a/public/app/plugins/panel/alertlist/editor.html b/public/app/plugins/panel/alertlist/editor.html index b75a7e453aa..b2038df34b8 100644 --- a/public/app/plugins/panel/alertlist/editor.html +++ b/public/app/plugins/panel/alertlist/editor.html @@ -20,12 +20,4 @@
- -
- -
- -
- -
diff --git a/public/app/plugins/panel/alertlist/module.html b/public/app/plugins/panel/alertlist/module.html index 13a32f1eb1e..2940e2ae33e 100644 --- a/public/app/plugins/panel/alertlist/module.html +++ b/public/app/plugins/panel/alertlist/module.html @@ -1,4 +1,4 @@ -
+
  1. diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index 96d7ad18e7c..543c5f04833 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -17,6 +17,7 @@ class AlertListPanel extends PanelCtrl { {text: 'Recent state changes', value: 'changes'} ]; + contentHeight: string; stateFilter: any = {}; currentAlerts: any = []; alertHistory: any = []; @@ -27,6 +28,7 @@ class AlertListPanel extends PanelCtrl { stateFilter: [] }; + /** @ngInject */ constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) { super($scope, $injector); @@ -55,6 +57,7 @@ class AlertListPanel extends PanelCtrl { } onRender() { + this.contentHeight = "max-height: " + this.height + "px;"; if (this.panel.show === 'current') { this.getCurrentAlertState(); } diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 6bd83cd81c0..9e111bf866d 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -41,6 +41,7 @@ @import "components/tags"; @import "components/panel_graph"; @import "components/submenu"; +@import "components/panel_alertlist"; @import "components/panel_dashlist"; @import "components/panel_pluginlist"; @import "components/panel_singlestat"; diff --git a/public/sass/components/_panel_alertlist.scss b/public/sass/components/_panel_alertlist.scss new file mode 100644 index 00000000000..41f63b61bd4 --- /dev/null +++ b/public/sass/components/_panel_alertlist.scss @@ -0,0 +1,3 @@ +.panel-alert-list { + overflow-y: scroll; +}