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

This commit is contained in:
Torkel Ödegaard
2016-11-04 12:15:08 +01:00
38 changed files with 348 additions and 162 deletions

View File

@ -25,6 +25,7 @@ be ready to build dashboards for you CloudWatch metrics.
3. Click the `Add new` link in the top header. 3. Click the `Add new` link in the top header.
4. Select `CloudWatch` from the dropdown. 4. Select `CloudWatch` from the dropdown.
> NOTE: If at any moment you have issues with getting this datasource to work and grafana is giving you undescriptive errors then dont forget to check your log file (try looking in /var/log/grafana/).
Name | Description Name | Description
------------ | ------------- ------------ | -------------
@ -47,6 +48,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
### AWS credentials file ### AWS credentials file
Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server. Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.
> NOTE: If you think you have the credentials file in the right place but it is still not working then you might try moving your .aws file to '/usr/share/grafana/' and make sure your credentials file has at most 0644 permissions.
Example content: Example content:

View File

@ -34,11 +34,16 @@ List available plugins
grafana-cli plugins list-remote grafana-cli plugins list-remote
``` ```
Install a plugin type Install the latest version of a plugin
``` ```
grafana-cli plugins install <plugin-id> grafana-cli plugins install <plugin-id>
``` ```
Install a specific version of a plugin
```
grafana-cli plugins install <plugin-id> <version>
```
List installed plugins List installed plugins
``` ```
grafana-cli plugins ls grafana-cli plugins ls

View File

@ -264,7 +264,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
return ApiError(500, "", err) return ApiError(500, "", err)
} }
var response models.AlertStateType = models.AlertStateNoData var response models.AlertStateType = models.AlertStatePending
pausedState := "un paused" pausedState := "un paused"
if cmd.Paused { if cmd.Paused {
response = models.AlertStatePaused response = models.AlertStatePaused

View File

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
@ -25,6 +26,16 @@ func Init(version string) {
grafanaVersion = version grafanaVersion = version
tr := &http.Transport{ tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
} }

View File

@ -41,6 +41,7 @@ var (
M_Alerting_Result_State_Paused Counter M_Alerting_Result_State_Paused Counter
M_Alerting_Result_State_NoData Counter M_Alerting_Result_State_NoData Counter
M_Alerting_Result_State_ExecError Counter M_Alerting_Result_State_ExecError Counter
M_Alerting_Result_State_Pending Counter
M_Alerting_Active_Alerts Counter M_Alerting_Active_Alerts Counter
M_Alerting_Notification_Sent_Slack Counter M_Alerting_Notification_Sent_Slack Counter
M_Alerting_Notification_Sent_Email Counter M_Alerting_Notification_Sent_Email Counter
@ -102,6 +103,7 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused") M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
M_Alerting_Result_State_NoData = RegCounter("alerting.result", "state", "no_data") M_Alerting_Result_State_NoData = RegCounter("alerting.result", "state", "no_data")
M_Alerting_Result_State_ExecError = RegCounter("alerting.result", "state", "exec_error") M_Alerting_Result_State_ExecError = RegCounter("alerting.result", "state", "exec_error")
M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending")
M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts") M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack") M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")

View File

@ -11,11 +11,12 @@ type AlertSeverityType string
type NoDataOption string type NoDataOption string
const ( const (
AlertStateNoData AlertStateType = "no_data" AlertStateNoData AlertStateType = "no_data"
AlertStateExecError AlertStateType = "execution_error" AlertStateExecError AlertStateType = "execution_error"
AlertStatePaused AlertStateType = "paused" AlertStatePaused AlertStateType = "paused"
AlertStateAlerting AlertStateType = "alerting" AlertStateAlerting AlertStateType = "alerting"
AlertStateOK AlertStateType = "ok" AlertStateOK AlertStateType = "ok"
AlertStatePending AlertStateType = "pending"
) )
const ( const (
@ -26,7 +27,7 @@ const (
) )
func (s AlertStateType) IsValid() bool { func (s AlertStateType) IsValid() bool {
return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused || s == AlertStatePending
} }
func (s NoDataOption) IsValid() bool { func (s NoDataOption) IsValid() bool {

View File

@ -33,15 +33,17 @@ type AlertQuery struct {
To string To string
} }
func (c *QueryCondition) Eval(context *alerting.EvalContext) { func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.ConditionResult, error) {
timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To) timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
seriesList, err := c.executeQuery(context, timeRange) seriesList, err := c.executeQuery(context, timeRange)
if err != nil { if err != nil {
context.Error = err return nil, err
return
} }
emptySerieCount := 0 emptySerieCount := 0
evalMatchCount := 0
var matches []*alerting.EvalMatch
for _, series := range seriesList { for _, series := range seriesList {
reducedValue := c.Reducer.Reduce(series) reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue) evalMatch := c.Evaluator.Eval(reducedValue)
@ -58,15 +60,20 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
} }
if evalMatch { if evalMatch {
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{ evalMatchCount++
matches = append(matches, &alerting.EvalMatch{
Metric: series.Name, Metric: series.Name,
Value: reducedValue.Float64, Value: reducedValue.Float64,
}) })
} }
} }
context.NoDataFound = emptySerieCount == len(seriesList) return &alerting.ConditionResult{
context.Firing = len(context.EvalMatches) > 0 Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList),
EvalMatches: matches,
}, nil
} }
func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {

View File

@ -46,19 +46,19 @@ func TestQueryCondition(t *testing.T) {
Convey("should fire when avg is above 100", func() { Convey("should fire when avg is above 100", func() {
points := tsdb.NewTimeSeriesPointsFromArgs(120, 0) points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)} ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
ctx.exec() cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil) So(err, ShouldBeNil)
So(ctx.result.Firing, ShouldBeTrue) So(cr.Firing, ShouldBeTrue)
}) })
Convey("Should not fire when avg is below 100", func() { Convey("Should not fire when avg is below 100", func() {
points := tsdb.NewTimeSeriesPointsFromArgs(90, 0) points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)} ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
ctx.exec() cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil) So(err, ShouldBeNil)
So(ctx.result.Firing, ShouldBeFalse) So(cr.Firing, ShouldBeFalse)
}) })
Convey("Should fire if only first serie matches", func() { Convey("Should fire if only first serie matches", func() {
@ -66,10 +66,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)), tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
} }
ctx.exec() cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil) So(err, ShouldBeNil)
So(ctx.result.Firing, ShouldBeTrue) So(cr.Firing, ShouldBeTrue)
}) })
Convey("Empty series", func() { Convey("Empty series", func() {
@ -78,10 +78,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
} }
ctx.exec() cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil) So(err, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeTrue) So(cr.NoDataFound, ShouldBeTrue)
}) })
Convey("Should set NoDataFound both series contains null", func() { Convey("Should set NoDataFound both series contains null", func() {
@ -89,10 +89,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}), tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}), tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
} }
ctx.exec() cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil) So(err, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeTrue) So(cr.NoDataFound, ShouldBeTrue)
}) })
Convey("Should not set NoDataFound if one serie is empty", func() { Convey("Should not set NoDataFound if one serie is empty", func() {
@ -100,10 +100,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
} }
ctx.exec() cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil) So(err, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeFalse) So(cr.NoDataFound, ShouldBeFalse)
}) })
}) })
}) })
@ -120,7 +120,7 @@ type queryConditionTestContext struct {
type queryConditionScenarioFunc func(c *queryConditionTestContext) type queryConditionScenarioFunc func(c *queryConditionTestContext)
func (ctx *queryConditionTestContext) exec() { func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) {
jsonModel, err := simplejson.NewJson([]byte(`{ jsonModel, err := simplejson.NewJson([]byte(`{
"type": "query", "type": "query",
"query": { "query": {
@ -146,7 +146,7 @@ func (ctx *queryConditionTestContext) exec() {
}, nil }, nil
} }
condition.Eval(ctx.result) return condition.Eval(ctx.result)
} }
func queryConditionScenario(desc string, fn queryConditionScenarioFunc) { func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {

View File

@ -20,8 +20,12 @@ func NewEvalHandler() *DefaultEvalHandler {
} }
func (e *DefaultEvalHandler) Eval(context *EvalContext) { func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true
for _, condition := range context.Rule.Conditions { for _, condition := range context.Rule.Conditions {
condition.Eval(context) cr, err := condition.Eval(context)
if err != nil {
context.Error = err
}
// break if condition could not be evaluated // break if condition could not be evaluated
if context.Error != nil { if context.Error != nil {
@ -29,11 +33,15 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
} }
// break if result has not triggered yet // break if result has not triggered yet
if context.Firing == false { if cr.Firing == false {
firing = false
break break
} }
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
} }
context.Firing = firing
context.EndTime = time.Now() context.EndTime = time.Now()
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
metrics.M_Alerting_Exeuction_Time.Update(elapsedTime) metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)

View File

@ -8,11 +8,12 @@ import (
) )
type conditionStub struct { type conditionStub struct {
firing bool firing bool
matches []*EvalMatch
} }
func (c *conditionStub) Eval(context *EvalContext) { func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
context.Firing = c.firing return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil
} }
func TestAlertingExecutor(t *testing.T) { func TestAlertingExecutor(t *testing.T) {
@ -30,10 +31,10 @@ func TestAlertingExecutor(t *testing.T) {
So(context.Firing, ShouldEqual, true) So(context.Firing, ShouldEqual, true)
}) })
Convey("Show return false with not passing condition", func() { Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{ context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{ Conditions: []Condition{
&conditionStub{firing: true}, &conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
&conditionStub{firing: false}, &conditionStub{firing: false},
}, },
}) })

View File

@ -3,6 +3,8 @@ package alerting
import ( import (
"errors" "errors"
"fmt"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
@ -104,7 +106,8 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
panelQuery := findPanelQueryByRefId(panel, queryRefId) panelQuery := findPanelQueryByRefId(panel, queryRefId)
if panelQuery == nil { if panelQuery == nil {
return nil, ValidationError{Reason: "Alert refes to query that cannot be found"} reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
return nil, ValidationError{Reason: reason}
} }
dsName := "" dsName := ""

View File

@ -21,6 +21,12 @@ type Notifier interface {
GetIsDefault() bool GetIsDefault() bool
} }
type Condition interface { type ConditionResult struct {
Eval(result *EvalContext) Firing bool
NoDataFound bool
EvalMatches []*EvalMatch
}
type Condition interface {
Eval(result *EvalContext) (*ConditionResult, error)
} }

View File

@ -49,7 +49,7 @@ func (n *RootNotifier) Notify(context *EvalContext) error {
return err return err
} }
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "Amount to send", len(notifiers)) n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
if len(notifiers) == 0 { if len(notifiers) == 0 {
return nil return nil

View File

@ -22,17 +22,21 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
} }
recipient := model.Settings.Get("recipient").MustString()
return &SlackNotifier{ return &SlackNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url, Url: url,
Recipient: recipient,
log: log.New("alerting.notifier.slack"), log: log.New("alerting.notifier.slack"),
}, nil }, nil
} }
type SlackNotifier struct { type SlackNotifier struct {
NotifierBase NotifierBase
Url string Url string
log log.Logger Recipient string
log log.Logger
} }
func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
@ -85,6 +89,12 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
"ts": time.Now().Unix(), "ts": time.Now().Unix(),
}, },
}, },
"parse": "full", // to linkify urls, users and channels in alert message.
}
//recipient override
if this.Recipient != "" {
body["channel"] = this.Recipient
} }
data, _ := json.Marshal(&body) data, _ := json.Marshal(&body)

View File

@ -86,7 +86,12 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
handler.log.Error("Failed to save annotation for new alert state", "error", err) handler.log.Error("Failed to save annotation for new alert state", "error", err)
} }
handler.notifier.Notify(evalContext) if (oldState == m.AlertStatePending) && (evalContext.Rule.State == m.AlertStateOK) {
handler.log.Info("Notfication not sent", "oldState", oldState, "newState", evalContext.Rule.State)
} else {
handler.notifier.Notify(evalContext)
}
} }
return nil return nil
@ -98,6 +103,8 @@ func (handler *DefaultResultHandler) shouldUpdateAlertState(evalContext *EvalCon
func countStateResult(state m.AlertStateType) { func countStateResult(state m.AlertStateType) {
switch state { switch state {
case m.AlertStatePending:
metrics.M_Alerting_Result_State_Pending.Inc(1)
case m.AlertStateAlerting: case m.AlertStateAlerting:
metrics.M_Alerting_Result_State_Alerting.Inc(1) metrics.M_Alerting_Result_State_Alerting.Inc(1)
case m.AlertStateOK: case m.AlertStateOK:

View File

@ -10,7 +10,9 @@ import (
type FakeCondition struct{} type FakeCondition struct{}
func (f *FakeCondition) Eval(context *EvalContext) {} func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{}, nil
}
func TestAlertRuleModel(t *testing.T) { func TestAlertRuleModel(t *testing.T) {
Convey("Testing alert rule", t, func() { Convey("Testing alert rule", t, func() {

View File

@ -99,7 +99,7 @@ func createDialer() (*gomail.Dialer, error) {
tlsconfig.Certificates = []tls.Certificate{cert} tlsconfig.Certificates = []tls.Certificate{cert}
} }
d := gomail.NewPlainDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password) d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
d.TLSConfig = tlsconfig d.TLSConfig = tlsconfig
return d, nil return d, nil
} }

View File

@ -173,7 +173,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor
} else { } else {
alert.Updated = time.Now() alert.Updated = time.Now()
alert.Created = time.Now() alert.Created = time.Now()
alert.State = m.AlertStateNoData alert.State = m.AlertStatePending
alert.NewStateDate = time.Now() alert.NewStateDate = time.Now()
_, err := sess.Insert(alert) _, err := sess.Insert(alert)
@ -260,7 +260,7 @@ func PauseAlertRule(cmd *m.PauseAlertCommand) error {
if cmd.Paused { if cmd.Paused {
newState = m.AlertStatePaused newState = m.AlertStatePaused
} else { } else {
newState = m.AlertStateNoData newState = m.AlertStatePending
} }
alert.State = newState alert.State = newState

View File

@ -47,7 +47,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(err2, ShouldBeNil) So(err2, ShouldBeNil)
So(alert.Name, ShouldEqual, "Alerting title") So(alert.Name, ShouldEqual, "Alerting title")
So(alert.Message, ShouldEqual, "Alerting message") So(alert.Message, ShouldEqual, "Alerting message")
So(alert.State, ShouldEqual, "no_data") So(alert.State, ShouldEqual, "pending")
So(alert.Frequency, ShouldEqual, 1) So(alert.Frequency, ShouldEqual, 1)
}) })
@ -77,7 +77,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(query.Result[0].Name, ShouldEqual, "Name") So(query.Result[0].Name, ShouldEqual, "Name")
Convey("Alert state should not be updated", func() { Convey("Alert state should not be updated", func() {
So(query.Result[0].State, ShouldEqual, "no_data") So(query.Result[0].State, ShouldEqual, "pending")
}) })
}) })

View File

@ -2,15 +2,14 @@ package graphite
import ( import (
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"regexp"
"strings" "strings"
"time"
"golang.org/x/net/context/ctxhttp" "golang.org/x/net/context/ctxhttp"
@ -36,14 +35,7 @@ func init() {
glog = log.New("tsdb.graphite") glog = log.New("tsdb.graphite")
tsdb.RegisterExecutor("graphite", NewGraphiteExecutor) tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
tr := &http.Transport{ HttpClient = tsdb.GetDefaultClient()
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
HttpClient = &http.Client{
Timeout: time.Duration(15 * time.Second),
Transport: tr,
}
} }
func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
@ -58,9 +50,9 @@ func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
for _, query := range queries { for _, query := range queries {
if fullTarget, err := query.Model.Get("targetFull").String(); err == nil { if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
formData["target"] = []string{fullTarget} formData["target"] = []string{fixIntervalFormat(fullTarget)}
} else { } else {
formData["target"] = []string{query.Model.Get("target").MustString()} formData["target"] = []string{fixIntervalFormat(query.Model.Get("target").MustString())}
} }
} }
@ -150,3 +142,17 @@ func formatTimeRange(input string) string {
} }
return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1) return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
} }
func fixIntervalFormat(target string) string {
rMinute := regexp.MustCompile(`'(\d+)m'`)
rMin := regexp.MustCompile("m")
target = rMinute.ReplaceAllStringFunc(target, func(m string) string {
return rMin.ReplaceAllString(m, "min")
})
rMonth := regexp.MustCompile(`'(\d+)M'`)
rMon := regexp.MustCompile("M")
target = rMonth.ReplaceAllStringFunc(target, func(M string) string {
return rMon.ReplaceAllString(M, "mon")
})
return target
}

View File

@ -1 +1,61 @@
package graphite package graphite
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestGraphiteFunctions(t *testing.T) {
Convey("Testing Graphite Functions", t, func() {
Convey("formatting time range for now", func() {
timeRange := formatTimeRange("now")
So(timeRange, ShouldEqual, "now")
})
Convey("formatting time range for now-1m", func() {
timeRange := formatTimeRange("now-1m")
So(timeRange, ShouldEqual, "now-1min")
})
Convey("formatting time range for now-1M", func() {
timeRange := formatTimeRange("now-1M")
So(timeRange, ShouldEqual, "now-1mon")
})
Convey("fix interval format in query for 1m", func() {
timeRange := fixIntervalFormat("aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1m'), 4)")
So(timeRange, ShouldEqual, "aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1min'), 4)")
})
Convey("fix interval format in query for 1M", func() {
timeRange := fixIntervalFormat("aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1M'), 4)")
So(timeRange, ShouldEqual, "aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1mon'), 4)")
})
Convey("should not override query for 1M", func() {
timeRange := fixIntervalFormat("app.grafana.*.dashboards.views.1M.count")
So(timeRange, ShouldEqual, "app.grafana.*.dashboards.views.1M.count")
})
Convey("should not override query for 1m", func() {
timeRange := fixIntervalFormat("app.grafana.*.dashboards.views.1m.count")
So(timeRange, ShouldEqual, "app.grafana.*.dashboards.views.1m.count")
})
})
}

29
pkg/tsdb/http.go Normal file
View File

@ -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,
}
}

View File

@ -2,13 +2,11 @@ package influxdb
import ( import (
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"time"
"golang.org/x/net/context/ctxhttp" "golang.org/x/net/context/ctxhttp"
@ -41,14 +39,7 @@ func init() {
glog = log.New("tsdb.influxdb") glog = log.New("tsdb.influxdb")
tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor) tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor)
tr := &http.Transport{ HttpClient = tsdb.GetDefaultClient()
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
HttpClient = &http.Client{
Timeout: time.Duration(15 * time.Second),
Transport: tr,
}
} }
func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {

View File

@ -2,19 +2,17 @@ package opentsdb
import ( import (
"context" "context"
"crypto/tls"
"fmt" "fmt"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time"
"golang.org/x/net/context/ctxhttp" "golang.org/x/net/context/ctxhttp"
"encoding/json"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"encoding/json"
"gopkg.in/guregu/null.v3" "gopkg.in/guregu/null.v3"
@ -40,14 +38,7 @@ func init() {
plog = log.New("tsdb.opentsdb") plog = log.New("tsdb.opentsdb")
tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor) tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor)
tr := &http.Transport{ HttpClient = tsdb.GetDefaultClient()
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
HttpClient = &http.Client{
Timeout: time.Duration(15 * time.Second),
Transport: tr,
}
} }
func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
@ -58,9 +49,9 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
tsdbQuery.Start = queryContext.TimeRange.GetFromAsMsEpoch() tsdbQuery.Start = queryContext.TimeRange.GetFromAsMsEpoch()
tsdbQuery.End = queryContext.TimeRange.GetToAsMsEpoch() tsdbQuery.End = queryContext.TimeRange.GetToAsMsEpoch()
for _ , query := range queries { for _, query := range queries {
metric := e.buildMetric(query) metric := e.buildMetric(query)
tsdbQuery.Queries = append(tsdbQuery.Queries, metric) tsdbQuery.Queries = append(tsdbQuery.Queries, metric)
} }
if setting.Env == setting.DEV { if setting.Env == setting.DEV {
@ -104,7 +95,7 @@ func (e *OpenTsdbExecutor) createRequest(data OpenTsdbQuery) (*http.Request, err
if e.BasicAuth { if e.BasicAuth {
req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
} }
return req, err return req, err
} }
@ -152,61 +143,61 @@ func (e *OpenTsdbExecutor) parseResponse(query OpenTsdbQuery, res *http.Response
return queryResults, nil return queryResults, nil
} }
func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) (map[string]interface{}) { func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) map[string]interface{} {
metric := make(map[string]interface{}) metric := make(map[string]interface{})
// Setting metric and aggregator // Setting metric and aggregator
metric["metric"] = query.Model.Get("metric").MustString() metric["metric"] = query.Model.Get("metric").MustString()
metric["aggregator"] = query.Model.Get("aggregator").MustString() metric["aggregator"] = query.Model.Get("aggregator").MustString()
// Setting downsampling options // Setting downsampling options
disableDownsampling := query.Model.Get("disableDownsampling").MustBool() disableDownsampling := query.Model.Get("disableDownsampling").MustBool()
if !disableDownsampling { if !disableDownsampling {
downsampleInterval := query.Model.Get("downsampleInterval").MustString() downsampleInterval := query.Model.Get("downsampleInterval").MustString()
if downsampleInterval == "" { if downsampleInterval == "" {
downsampleInterval = "1m" //default value for blank downsampleInterval = "1m" //default value for blank
} }
downsample := downsampleInterval + "-" + query.Model.Get("downsampleAggregator").MustString() downsample := downsampleInterval + "-" + query.Model.Get("downsampleAggregator").MustString()
if query.Model.Get("downsampleFillPolicy").MustString() != "none" { if query.Model.Get("downsampleFillPolicy").MustString() != "none" {
metric["downsample"] = downsample + "-" + query.Model.Get("downsampleFillPolicy").MustString() metric["downsample"] = downsample + "-" + query.Model.Get("downsampleFillPolicy").MustString()
} else { } else {
metric["downsample"] = downsample 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 resetValue, resetValueCheck := query.Model.CheckGet("counterResetValue")
if query.Model.Get("shouldComputeRate").MustBool() { if resetValueCheck {
rateOptions["resetValue"] = resetValue.MustFloat64()
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
} }
// Setting tags metric["rateOptions"] = rateOptions
tags, tagsCheck := query.Model.CheckGet("tags") }
if tagsCheck && len(tags.MustMap()) > 0 {
metric["tags"] = tags.MustMap()
}
// Setting filters // Setting tags
filters, filtersCheck := query.Model.CheckGet("filters") tags, tagsCheck := query.Model.CheckGet("tags")
if filtersCheck && len(filters.MustArray()) > 0 { if tagsCheck && len(tags.MustMap()) > 0 {
metric["filters"] = filters.MustArray() 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
} }

View File

@ -3,7 +3,6 @@ package prometheus
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -25,8 +24,7 @@ func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
} }
var ( var (
plog log.Logger plog log.Logger
HttpClient http.Client
) )
func init() { func init() {
@ -83,6 +81,10 @@ func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlic
func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string { func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
if query.LegendFormat == "" {
return metric.String()
}
result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
labelName := strings.Replace(string(in), "{{", "", 1) labelName := strings.Replace(string(in), "{{", "", 1)
labelName = strings.Replace(labelName, "}}", "", 1) labelName = strings.Replace(labelName, "}}", "", 1)
@ -110,10 +112,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
return nil, err return nil, err
} }
format, err := queryModel.Model.Get("legendFormat").String() format := queryModel.Model.Get("legendFormat").MustString("")
if err != nil {
return nil, err
}
start, err := queryContext.TimeRange.ParseFrom() start, err := queryContext.TimeRange.ParseFrom()
if err != nil { if err != nil {
@ -158,9 +157,3 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
queryResults["A"] = queryRes queryResults["A"] = queryRes
return queryResults, nil return queryResults, nil
} }
/*
func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult {
result.Error = err
return result
}*/

View File

@ -22,5 +22,19 @@ func TestPrometheus(t *testing.T) {
So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}") So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}")
}) })
Convey("build full serie name", func() {
metric := map[p.LabelName]p.LabelValue{
p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"),
p.LabelName("app"): p.LabelValue("backend"),
p.LabelName("device"): p.LabelValue("mobile"),
}
query := &PrometheusQuery{
LegendFormat: "",
}
So(formatLegend(metric, query), ShouldEqual, `http_request_total{app="backend", device="mobile"}`)
})
}) })
} }

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="grafana-info-box span8" style="margin: 20px 0 25px 0"> <div class="grafana-info-box span8" style="margin: 20px 0 25px 0">
These system settings are defined in grafana.ini or grafana.custom.ini (or overriden in ENV variables). These system settings are defined in grafana.ini or custom.ini (or overriden in ENV variables).
To change these you currently need to restart grafana. To change these you currently need to restart grafana.
</div> </div>

View File

@ -87,6 +87,13 @@ function getStateDisplayModel(state) {
stateClass: 'alert-state-paused' stateClass: 'alert-state-paused'
}; };
} }
case 'pending': {
return {
text: 'PENDING',
iconClass: "fa fa-exclamation",
stateClass: 'alert-state-warning'
};
}
} }
} }

View File

@ -59,10 +59,21 @@
<div class="gf-form-group" ng-if="ctrl.model.type === 'slack'"> <div class="gf-form-group" ng-if="ctrl.model.type === 'slack'">
<h3 class="page-heading">Slack settings</h3> <h3 class="page-heading">Slack settings</h3>
<div class="gf-form"> <div class="gf-form max-width-30">
<span class="gf-form-label width-6">Url</span> <span class="gf-form-label width-6">Url</span>
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input> <input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
</div> </div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Recipient</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.recipient"
data-placement="right">
</input>
<info-popover mode="right-absolute">
Override default channel or user, use #channel-name or @username
</info-popover>
</div>
</div> </div>
<div class="gf-form-group section" ng-if="ctrl.model.type === 'email'"> <div class="gf-form-group section" ng-if="ctrl.model.type === 'email'">

View File

@ -16,6 +16,7 @@ export class ConstantVariable implements Variable {
label: '', label: '',
query: '', query: '',
current: {}, current: {},
options: [],
}; };
/** @ngInject **/ /** @ngInject **/

View File

@ -145,6 +145,11 @@ describe('templateSrv', function() {
expect(result).to.be('test|test2'); expect(result).to.be('test|test2');
}); });
it('multi value and distributed should render distributed string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'distributed', { name: 'build' });
expect(result).to.be('test,build=test2');
});
it('slash should be properly escaped in regex format', function() { it('slash should be properly escaped in regex format', function() {
var result = _templateSrv.formatValue('Gi3/14', 'regex'); var result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).to.be('Gi3\\/14'); expect(result).to.be('Gi3\\/14');

View File

@ -95,6 +95,9 @@ function (angular, _, kbn) {
} }
return value.join('|'); return value.join('|');
} }
case "distributed": {
return this.distributeVariable(value, variable.name);
}
default: { default: {
if (typeof value === 'string') { if (typeof value === 'string') {
return value; return value;
@ -210,6 +213,17 @@ function (angular, _, kbn) {
}); });
}; };
this.distributeVariable = function(value, variable) {
value = _.map(value, function(val, index) {
if (index !== 0) {
return variable + "=" + val;
} else {
return val;
}
});
return value.join(',');
};
}); });
}); });

View File

@ -244,7 +244,7 @@ function (angular, _, dateMath) {
var interpolated; var interpolated;
try { try {
interpolated = templateSrv.replace(query); interpolated = templateSrv.replace(query, {}, 'distributed');
} }
catch (err) { catch (err) {
return $q.reject(err); return $q.reject(err);

View File

@ -20,12 +20,4 @@
<gf-form-switch class="gf-form" label="Execution error" label-class="width-10" checked="ctrl.stateFilter['execution_error']" on-change="ctrl.updateStateFilter()"></gf-form-switch> <gf-form-switch class="gf-form" label="Execution error" label-class="width-10" checked="ctrl.stateFilter['execution_error']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Alerting" label-class="width-10" checked="ctrl.stateFilter['alerting']" on-change="ctrl.updateStateFilter()"></gf-form-switch> <gf-form-switch class="gf-form" label="Alerting" label-class="width-10" checked="ctrl.stateFilter['alerting']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
</div> </div>
<div class="section gf-form-group" ng-if="ctrl.panel.show == 'changes'">
<!-- <h5 class="section-heading">Current state</h5> -->
</div>
<div class="section gf-form-group" ng-if="ctrl.panel.show == 'current'">
<!-- <h5 class="section-heading">Current state</h5> -->
</div>
</div> </div>

View File

@ -1,4 +1,4 @@
<div class="panel-alert-list"> <div class="panel-alert-list" style="{{ctrl.contentHeight}}">
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'"> <section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
<ol class="card-list"> <ol class="card-list">
<li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts"> <li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">

View File

@ -17,6 +17,7 @@ class AlertListPanel extends PanelCtrl {
{text: 'Recent state changes', value: 'changes'} {text: 'Recent state changes', value: 'changes'}
]; ];
contentHeight: string;
stateFilter: any = {}; stateFilter: any = {};
currentAlerts: any = []; currentAlerts: any = [];
alertHistory: any = []; alertHistory: any = [];
@ -27,6 +28,7 @@ class AlertListPanel extends PanelCtrl {
stateFilter: [] stateFilter: []
}; };
/** @ngInject */ /** @ngInject */
constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) { constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) {
super($scope, $injector); super($scope, $injector);
@ -55,6 +57,7 @@ class AlertListPanel extends PanelCtrl {
} }
onRender() { onRender() {
this.contentHeight = "max-height: " + this.height + "px;";
if (this.panel.show === 'current') { if (this.panel.show === 'current') {
this.getCurrentAlertState(); this.getCurrentAlertState();
} }

View File

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

View File

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