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.
3. Click the `Add new` link in the top header.
4. Select `CloudWatch` from the dropdown.
> NOTE: If at any moment you have issues with getting this datasource to work and grafana is giving you undescriptive errors then dont forget to check your log file (try looking in /var/log/grafana/).
Name | Description
------------ | -------------
......@@ -47,6 +48,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
### AWS credentials file
Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.
> NOTE: If you think you have the credentials file in the right place but it is still not working then you might try moving your .aws file to '/usr/share/grafana/' and make sure your credentials file has at most 0644 permissions.
Example content:
......
......@@ -34,11 +34,16 @@ List available plugins
grafana-cli plugins list-remote
```
Install a plugin type
Install the latest version of a plugin
```
grafana-cli plugins install <plugin-id>
```
Install a specific version of a plugin
```
grafana-cli plugins install <plugin-id> <version>
```
List installed plugins
```
grafana-cli plugins ls
......
......@@ -264,7 +264,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
return ApiError(500, "", err)
}
var response models.AlertStateType = models.AlertStateNoData
var response models.AlertStateType = models.AlertStatePending
pausedState := "un paused"
if cmd.Paused {
response = models.AlertStatePaused
......
......@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"path"
......@@ -25,6 +26,16 @@ func Init(version string) {
grafanaVersion = version
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
......
......@@ -41,6 +41,7 @@ var (
M_Alerting_Result_State_Paused Counter
M_Alerting_Result_State_NoData Counter
M_Alerting_Result_State_ExecError Counter
M_Alerting_Result_State_Pending Counter
M_Alerting_Active_Alerts Counter
M_Alerting_Notification_Sent_Slack Counter
M_Alerting_Notification_Sent_Email Counter
......@@ -102,6 +103,7 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
M_Alerting_Result_State_NoData = RegCounter("alerting.result", "state", "no_data")
M_Alerting_Result_State_ExecError = RegCounter("alerting.result", "state", "exec_error")
M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending")
M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
......
......@@ -16,6 +16,7 @@ const (
AlertStatePaused AlertStateType = "paused"
AlertStateAlerting AlertStateType = "alerting"
AlertStateOK AlertStateType = "ok"
AlertStatePending AlertStateType = "pending"
)
const (
......@@ -26,7 +27,7 @@ const (
)
func (s AlertStateType) IsValid() bool {
return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused
return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused || s == AlertStatePending
}
func (s NoDataOption) IsValid() bool {
......
......@@ -33,15 +33,17 @@ type AlertQuery struct {
To string
}
func (c *QueryCondition) Eval(context *alerting.EvalContext) {
func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.ConditionResult, error) {
timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
seriesList, err := c.executeQuery(context, timeRange)
if err != nil {
context.Error = err
return
return nil, err
}
emptySerieCount := 0
evalMatchCount := 0
var matches []*alerting.EvalMatch
for _, series := range seriesList {
reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue)
......@@ -58,15 +60,20 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
}
if evalMatch {
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
evalMatchCount++
matches = append(matches, &alerting.EvalMatch{
Metric: series.Name,
Value: reducedValue.Float64,
})
}
}
context.NoDataFound = emptySerieCount == len(seriesList)
context.Firing = len(context.EvalMatches) > 0
return &alerting.ConditionResult{
Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList),
EvalMatches: matches,
}, nil
}
func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {
......
......@@ -46,19 +46,19 @@ func TestQueryCondition(t *testing.T) {
Convey("should fire when avg is above 100", func() {
points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.Firing, ShouldBeTrue)
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeTrue)
})
Convey("Should not fire when avg is below 100", func() {
points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.Firing, ShouldBeFalse)
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeFalse)
})
Convey("Should fire if only first serie matches", func() {
......@@ -66,10 +66,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.Firing, ShouldBeTrue)
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeTrue)
})
Convey("Empty series", func() {
......@@ -78,10 +78,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeTrue)
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeTrue)
})
Convey("Should set NoDataFound both series contains null", func() {
......@@ -89,10 +89,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeTrue)
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeTrue)
})
Convey("Should not set NoDataFound if one serie is empty", func() {
......@@ -100,10 +100,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeFalse)
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeFalse)
})
})
})
......@@ -120,7 +120,7 @@ type queryConditionTestContext struct {
type queryConditionScenarioFunc func(c *queryConditionTestContext)
func (ctx *queryConditionTestContext) exec() {
func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) {
jsonModel, err := simplejson.NewJson([]byte(`{
"type": "query",
"query": {
......@@ -146,7 +146,7 @@ func (ctx *queryConditionTestContext) exec() {
}, nil
}
condition.Eval(ctx.result)
return condition.Eval(ctx.result)
}
func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {
......
......@@ -20,8 +20,12 @@ func NewEvalHandler() *DefaultEvalHandler {
}
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true
for _, condition := range context.Rule.Conditions {
condition.Eval(context)
cr, err := condition.Eval(context)
if err != nil {
context.Error = err
}
// break if condition could not be evaluated
if context.Error != nil {
......@@ -29,11 +33,15 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
}
// break if result has not triggered yet
if context.Firing == false {
if cr.Firing == false {
firing = false
break
}
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
}
context.Firing = firing
context.EndTime = time.Now()
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)
......
......@@ -9,10 +9,11 @@ import (
type conditionStub struct {
firing bool
matches []*EvalMatch
}
func (c *conditionStub) Eval(context *EvalContext) {
context.Firing = c.firing
func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil
}
func TestAlertingExecutor(t *testing.T) {
......@@ -30,10 +31,10 @@ func TestAlertingExecutor(t *testing.T) {
So(context.Firing, ShouldEqual, true)
})
Convey("Show return false with not passing condition", func() {
Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true},
&conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
&conditionStub{firing: false},
},
})
......
......@@ -3,6 +3,8 @@ package alerting
import (
"errors"
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
......@@ -104,7 +106,8 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
panelQuery := findPanelQueryByRefId(panel, queryRefId)
if panelQuery == nil {
return nil, ValidationError{Reason: "Alert refes to query that cannot be found"}
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
return nil, ValidationError{Reason: reason}
}
dsName := ""
......
......@@ -21,6 +21,12 @@ type Notifier interface {
GetIsDefault() bool
}
type ConditionResult struct {
Firing bool
NoDataFound bool
EvalMatches []*EvalMatch
}
type Condition interface {
Eval(result *EvalContext)
Eval(result *EvalContext) (*ConditionResult, error)
}
......@@ -49,7 +49,7 @@ func (n *RootNotifier) Notify(context *EvalContext) error {
return err
}
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "Amount to send", len(notifiers))
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
if len(notifiers) == 0 {
return nil
......
......@@ -22,9 +22,12 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
}
recipient := model.Settings.Get("recipient").MustString()
return &SlackNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url,
Recipient: recipient,
log: log.New("alerting.notifier.slack"),
}, nil
}
......@@ -32,6 +35,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
type SlackNotifier struct {
NotifierBase
Url string
Recipient string
log log.Logger
}
......@@ -85,6 +89,12 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
"ts": time.Now().Unix(),
},
},
"parse": "full", // to linkify urls, users and channels in alert message.
}
//recipient override
if this.Recipient != "" {
body["channel"] = this.Recipient
}
data, _ := json.Marshal(&body)
......
......@@ -86,9 +86,14 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
handler.log.Error("Failed to save annotation for new alert state", "error", err)
}
if (oldState == m.AlertStatePending) && (evalContext.Rule.State == m.AlertStateOK) {
handler.log.Info("Notfication not sent", "oldState", oldState, "newState", evalContext.Rule.State)
} else {
handler.notifier.Notify(evalContext)
}
}
return nil
}
......@@ -98,6 +103,8 @@ func (handler *DefaultResultHandler) shouldUpdateAlertState(evalContext *EvalCon
func countStateResult(state m.AlertStateType) {
switch state {
case m.AlertStatePending:
metrics.M_Alerting_Result_State_Pending.Inc(1)
case m.AlertStateAlerting:
metrics.M_Alerting_Result_State_Alerting.Inc(1)
case m.AlertStateOK:
......
......@@ -10,7 +10,9 @@ import (
type FakeCondition struct{}
func (f *FakeCondition) Eval(context *EvalContext) {}
func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{}, nil
}
func TestAlertRuleModel(t *testing.T) {
Convey("Testing alert rule", t, func() {
......
......@@ -99,7 +99,7 @@ func createDialer() (*gomail.Dialer, error) {
tlsconfig.Certificates = []tls.Certificate{cert}
}
d := gomail.NewPlainDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
d.TLSConfig = tlsconfig
return d, nil
}
......
......@@ -173,7 +173,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor
} else {
alert.Updated = time.Now()
alert.Created = time.Now()
alert.State = m.AlertStateNoData
alert.State = m.AlertStatePending
alert.NewStateDate = time.Now()
_, err := sess.Insert(alert)
......@@ -260,7 +260,7 @@ func PauseAlertRule(cmd *m.PauseAlertCommand) error {
if cmd.Paused {
newState = m.AlertStatePaused
} else {
newState = m.AlertStateNoData
newState = m.AlertStatePending
}
alert.State = newState
......
......@@ -47,7 +47,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(err2, ShouldBeNil)
So(alert.Name, ShouldEqual, "Alerting title")
So(alert.Message, ShouldEqual, "Alerting message")
So(alert.State, ShouldEqual, "no_data")
So(alert.State, ShouldEqual, "pending")
So(alert.Frequency, ShouldEqual, 1)
})
......@@ -77,7 +77,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(query.Result[0].Name, ShouldEqual, "Name")
Convey("Alert state should not be updated", func() {
So(query.Result[0].State, ShouldEqual, "no_data")
So(query.Result[0].State, ShouldEqual, "pending")
})
})
......
......@@ -2,15 +2,14 @@ package graphite
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"time"
"golang.org/x/net/context/ctxhttp"
......@@ -36,14 +35,7 @@ func init() {
glog = log.New("tsdb.graphite")
tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
HttpClient = &http.Client{
Timeout: time.Duration(15 * time.Second),
Transport: tr,
}
HttpClient = tsdb.GetDefaultClient()
}
func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
......@@ -58,9 +50,9 @@ func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
for _, query := range queries {
if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
formData["target"] = []string{fullTarget}
formData["target"] = []string{fixIntervalFormat(fullTarget)}
} else {
formData["target"] = []string{query.Model.Get("target").MustString()}
formData["target"] = []string{fixIntervalFormat(query.Model.Get("target").MustString())}
}
}
......@@ -150,3 +142,17 @@ func formatTimeRange(input string) string {
}
return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
}
func fixIntervalFormat(target string) string {
rMinute := regexp.MustCompile(`'(\d+)m'`)
rMin := regexp.MustCompile("m")
target = rMinute.ReplaceAllStringFunc(target, func(m string) string {
return rMin.ReplaceAllString(m, "min")
})
rMonth := regexp.MustCompile(`'(\d+)M'`)
rMon := regexp.MustCompile("M")
target = rMonth.ReplaceAllStringFunc(target, func(M string) string {
return rMon.ReplaceAllString(M, "mon")
})
return target
}
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
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"time"
"golang.org/x/net/context/ctxhttp"
......@@ -41,14 +39,7 @@ func init() {
glog = log.New("tsdb.influxdb")
tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
HttpClient = &http.Client{
Timeout: time.Duration(15 * time.Second),
Transport: tr,
}
HttpClient = tsdb.GetDefaultClient()
}
func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
......
......@@ -2,19 +2,17 @@ package opentsdb
import (
"context"
"crypto/tls"
"fmt"
"path"
"strconv"
"strings"
"time"
"golang.org/x/net/context/ctxhttp"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"encoding/json"
"gopkg.in/guregu/null.v3"
......@@ -40,14 +38,7 @@ func init() {
plog = log.New("tsdb.opentsdb")
tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
HttpClient = &http.Client{
Timeout: time.Duration(15 * time.Second),
Transport: tr,
}
HttpClient = tsdb.GetDefaultClient()
}
func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
......@@ -58,7 +49,7 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
tsdbQuery.Start = queryContext.TimeRange.GetFromAsMsEpoch()
tsdbQuery.End = queryContext.TimeRange.GetToAsMsEpoch()
for _ , query := range queries {
for _, query := range queries {
metric := e.buildMetric(query)
tsdbQuery.Queries = append(tsdbQuery.Queries, metric)
}
......@@ -152,7 +143,7 @@ func (e *OpenTsdbExecutor) parseResponse(query OpenTsdbQuery, res *http.Response
return queryResults, nil
}
func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) (map[string]interface{}) {
func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) map[string]interface{} {
metric := make(map[string]interface{})
......
......@@ -3,7 +3,6 @@ package prometheus
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"time"
......@@ -26,7 +25,6 @@ func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
var (
plog log.Logger
HttpClient http.Client
)
func init() {
......@@ -83,6 +81,10 @@ func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlic
func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
if query.LegendFormat == "" {
return metric.String()
}
result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
labelName := strings.Replace(string(in), "{{", "", 1)
labelName = strings.Replace(labelName, "}}", "", 1)
......@@ -110,10 +112,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
return nil, err
}
format, err := queryModel.Model.Get("legendFormat").String()
if err != nil {
return nil, err
}
format := queryModel.Model.Get("legendFormat").MustString("")
start, err := queryContext.TimeRange.ParseFrom()
if err != nil {
......@@ -158,9 +157,3 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
queryResults["A"] = queryRes
return queryResults, nil
}
/*
func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult {
result.Error = err
return result
}*/
......@@ -22,5 +22,19 @@ func TestPrometheus(t *testing.T) {
So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}")
})
Convey("build full serie name", func() {
metric := map[p.LabelName]p.LabelValue{
p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"),
p.LabelName("app"): p.LabelValue("backend"),
p.LabelName("device"): p.LabelValue("mobile"),
}
query := &PrometheusQuery{
LegendFormat: "",
}
So(formatLegend(metric, query), ShouldEqual, `http_request_total{app="backend", device="mobile"}`)
})
})
}
......@@ -7,7 +7,7 @@
</div>
<div class="grafana-info-box span8" style="margin: 20px 0 25px 0">
These system settings are defined in grafana.ini or grafana.custom.ini (or overriden in ENV variables).
These system settings are defined in grafana.ini or custom.ini (or overriden in ENV variables).
To change these you currently need to restart grafana.
</div>
......
......@@ -87,6 +87,13 @@ function getStateDisplayModel(state) {
stateClass: 'alert-state-paused'
};
}
case 'pending': {
return {
text: 'PENDING',
iconClass: "fa fa-exclamation",
stateClass: 'alert-state-warning'
};
}
}
}
......
......@@ -59,10 +59,21 @@
<div class="gf-form-group" ng-if="ctrl.model.type === 'slack'">
<h3 class="page-heading">Slack settings</h3>
<div class="gf-form">
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Url</span>
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Recipient</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.recipient"
data-placement="right">
</input>
<info-popover mode="right-absolute">
Override default channel or user, use #channel-name or @username
</info-popover>
</div>
</div>
<div class="gf-form-group section" ng-if="ctrl.model.type === 'email'">
......
......@@ -16,6 +16,7 @@ export class ConstantVariable implements Variable {
label: '',
query: '',
current: {},
options: [],
};
/** @ngInject **/
......
......@@ -145,6 +145,11 @@ describe('templateSrv', function() {
expect(result).to.be('test|test2');
});
it('multi value and distributed should render distributed string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'distributed', { name: 'build' });
expect(result).to.be('test,build=test2');
});
it('slash should be properly escaped in regex format', function() {
var result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).to.be('Gi3\\/14');
......
......@@ -95,6 +95,9 @@ function (angular, _, kbn) {
}
return value.join('|');
}
case "distributed": {
return this.distributeVariable(value, variable.name);
}
default: {
if (typeof value === 'string') {
return value;
......@@ -210,6 +213,17 @@ function (angular, _, kbn) {
});
};
this.distributeVariable = function(value, variable) {
value = _.map(value, function(val, index) {
if (index !== 0) {
return variable + "=" + val;
} else {
return val;
}
});
return value.join(',');
};
});
});
......@@ -244,7 +244,7 @@ function (angular, _, dateMath) {
var interpolated;
try {
interpolated = templateSrv.replace(query);
interpolated = templateSrv.replace(query, {}, 'distributed');
}
catch (err) {
return $q.reject(err);
......
......@@ -20,12 +20,4 @@
<gf-form-switch class="gf-form" label="Execution error" label-class="width-10" checked="ctrl.stateFilter['execution_error']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Alerting" label-class="width-10" checked="ctrl.stateFilter['alerting']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
</div>
<div class="section gf-form-group" ng-if="ctrl.panel.show == 'changes'">
<!-- <h5 class="section-heading">Current state</h5> -->
</div>
<div class="section gf-form-group" ng-if="ctrl.panel.show == 'current'">
<!-- <h5 class="section-heading">Current state</h5> -->
</div>
</div>
<div class="panel-alert-list">
<div class="panel-alert-list" style="{{ctrl.contentHeight}}">
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
<ol class="card-list">
<li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">
......
......@@ -17,6 +17,7 @@ class AlertListPanel extends PanelCtrl {
{text: 'Recent state changes', value: 'changes'}
];
contentHeight: string;
stateFilter: any = {};
currentAlerts: any = [];
alertHistory: any = [];
......@@ -27,6 +28,7 @@ class AlertListPanel extends PanelCtrl {
stateFilter: []
};
/** @ngInject */
constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) {
super($scope, $injector);
......@@ -55,6 +57,7 @@ class AlertListPanel extends PanelCtrl {
}
onRender() {
this.contentHeight = "max-height: " + this.height + "px;";
if (this.panel.show === 'current') {
this.getCurrentAlertState();
}
......
......@@ -41,6 +41,7 @@
@import "components/tags";
@import "components/panel_graph";
@import "components/submenu";
@import "components/panel_alertlist";
@import "components/panel_dashlist";
@import "components/panel_pluginlist";
@import "components/panel_singlestat";
......
.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