mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 09:42:06 +08:00
Merge branch 'master' of github.com:grafana/grafana
This commit is contained in:
@ -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:
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -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 := ""
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
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 (
|
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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}*/
|
|
||||||
|
@ -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"}`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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'">
|
||||||
|
@ -16,6 +16,7 @@ export class ConstantVariable implements Variable {
|
|||||||
label: '',
|
label: '',
|
||||||
query: '',
|
query: '',
|
||||||
current: {},
|
current: {},
|
||||||
|
options: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @ngInject **/
|
/** @ngInject **/
|
||||||
|
@ -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');
|
||||||
|
@ -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(',');
|
||||||
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
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;
|
||||||
|
}
|
Reference in New Issue
Block a user