Commit 2da2d5df by Torkel Ödegaard

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

parents c99f4cb9 7165c866
...@@ -25,6 +25,7 @@ be ready to build dashboards for you CloudWatch metrics. ...@@ -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 ...@@ -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 ...@@ -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 { ...@@ -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 ( ...@@ -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) { ...@@ -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 ( ...@@ -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) { ...@@ -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 ...@@ -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 ( ...@@ -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 { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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 { ...@@ -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() { ...@@ -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 { ...@@ -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) { ...@@ -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 ( ...@@ -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) { ...@@ -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 ...@@ -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) { ...@@ -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 { ...@@ -21,6 +21,12 @@ type Notifier interface {
GetIsDefault() bool GetIsDefault() bool
} }
type ConditionResult struct {
Firing bool
NoDataFound bool
EvalMatches []*EvalMatch
}
type Condition interface { type Condition interface {
Eval(result *EvalContext) Eval(result *EvalContext) (*ConditionResult, error)
} }
...@@ -49,7 +49,7 @@ func (n *RootNotifier) Notify(context *EvalContext) 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) { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ( ...@@ -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) { ...@@ -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 ...@@ -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 { ...@@ -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) { ...@@ -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) { ...@@ -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 ...@@ -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() { ...@@ -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, ...@@ -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 { ...@@ -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
}
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")
})
})
}
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 ...@@ -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() { ...@@ -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 ...@@ -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() { ...@@ -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, ...@@ -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 ...@@ -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 ...@@ -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 // Setting rate options
if query.Model.Get("shouldComputeRate").MustBool() { 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") metric["rate"] = true
if counterMaxCheck { rateOptions := make(map[string]interface{})
rateOptions["counterMax"] = counterMax.MustFloat64() rateOptions["counter"] = query.Model.Get("isCounter").MustBool()
}
resetValue, resetValueCheck := query.Model.CheckGet("counterResetValue")
if resetValueCheck {
rateOptions["resetValue"] = resetValue.MustFloat64()
}
metric["rateOptions"] = rateOptions counterMax, counterMaxCheck := query.Model.CheckGet("counterMax")
if counterMaxCheck {
rateOptions["counterMax"] = counterMax.MustFloat64()
} }
// Setting tags resetValue, resetValueCheck := query.Model.CheckGet("counterResetValue")
tags, tagsCheck := query.Model.CheckGet("tags") if resetValueCheck {
if tagsCheck && len(tags.MustMap()) > 0 { rateOptions["resetValue"] = resetValue.MustFloat64()
metric["tags"] = tags.MustMap()
} }
// Setting filters metric["rateOptions"] = rateOptions
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()
}
// Setting filters
filters, filtersCheck := query.Model.CheckGet("filters")
if filtersCheck && len(filters.MustArray()) > 0 {
metric["filters"] = filters.MustArray()
}
return metric return metric
} }
...@@ -3,7 +3,6 @@ package prometheus ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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) { ...@@ -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 @@ ...@@ -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) { ...@@ -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 @@ ...@@ -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 { ...@@ -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() { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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 @@ ...@@ -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>
<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 { ...@@ -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 { ...@@ -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 { ...@@ -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 @@ ...@@ -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";
......
.panel-alert-list {
overflow-y: scroll;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment