mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of github.com:grafana/grafana
This commit is contained in:
commit
2da2d5df56
@ -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:
|
||||
|
||||
|
@ -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 <plugin-id>
|
||||
```
|
||||
|
||||
Install a specific version of a plugin
|
||||
```
|
||||
grafana-cli plugins install <plugin-id> <version>
|
||||
```
|
||||
|
||||
List installed plugins
|
||||
```
|
||||
grafana-cli plugins ls
|
||||
|
@ -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
|
||||
|
@ -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},
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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},
|
||||
},
|
||||
})
|
||||
|
@ -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 := ""
|
||||
|
@ -21,6 +21,12 @@ type Notifier interface {
|
||||
GetIsDefault() bool
|
||||
}
|
||||
|
||||
type Condition interface {
|
||||
Eval(result *EvalContext)
|
||||
type ConditionResult struct {
|
||||
Firing bool
|
||||
NoDataFound bool
|
||||
EvalMatches []*EvalMatch
|
||||
}
|
||||
|
||||
type Condition interface {
|
||||
Eval(result *EvalContext) (*ConditionResult, error)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
29
pkg/tsdb/http.go
Normal file
29
pkg/tsdb/http.go
Normal 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,
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}*/
|
||||
|
@ -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"}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
<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.
|
||||
</div>
|
||||
|
||||
|
@ -87,6 +87,13 @@ function getStateDisplayModel(state) {
|
||||
stateClass: 'alert-state-paused'
|
||||
};
|
||||
}
|
||||
case 'pending': {
|
||||
return {
|
||||
text: 'PENDING',
|
||||
iconClass: "fa fa-exclamation",
|
||||
stateClass: 'alert-state-warning'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,10 +59,21 @@
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.model.type === 'slack'">
|
||||
<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>
|
||||
<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 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 class="gf-form-group section" ng-if="ctrl.model.type === 'email'">
|
||||
|
@ -16,6 +16,7 @@ export class ConstantVariable implements Variable {
|
||||
label: '',
|
||||
query: '',
|
||||
current: {},
|
||||
options: [],
|
||||
};
|
||||
|
||||
/** @ngInject **/
|
||||
|
@ -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');
|
||||
|
@ -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(',');
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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="Alerting" label-class="width-10" checked="ctrl.stateFilter['alerting']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
|
||||
</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>
|
||||
|
@ -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'">
|
||||
<ol class="card-list">
|
||||
<li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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";
|
||||
|
3
public/sass/components/_panel_alertlist.scss
Normal file
3
public/sass/components/_panel_alertlist.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.panel-alert-list {
|
||||
overflow-y: scroll;
|
||||
}
|
Loading…
Reference in New Issue
Block a user