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

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

View File

@ -25,6 +25,7 @@ be ready to build dashboards for you CloudWatch metrics.
3. Click the `Add new` link in the top header.
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:

View File

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

View File

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

View File

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

View File

@ -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")

View File

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

View File

@ -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) {

View File

@ -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) {

View File

@ -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)

View File

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

View File

@ -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 := ""

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

@ -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")
})
})

View File

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

View File

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

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

@ -0,0 +1,29 @@
package tsdb
import (
"crypto/tls"
"net"
"net/http"
"time"
)
func GetDefaultClient() *http.Client {
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &http.Client{
Timeout: time.Duration(30 * time.Second),
Transport: tr,
}
}

View File

@ -2,13 +2,11 @@ package influxdb
import (
"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 {

View File

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

View File

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

View File

@ -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"}`)
})
})
}

View File

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

View File

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

View File

@ -59,10 +59,21 @@
<div class="gf-form-group" ng-if="ctrl.model.type === 'slack'">
<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'">

View File

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

View File

@ -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');

View File

@ -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(',');
};
});
});

View File

@ -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);

View File

@ -20,12 +20,4 @@
<gf-form-switch class="gf-form" label="Execution error" label-class="width-10" checked="ctrl.stateFilter['execution_error']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="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>

View File

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

View File

@ -17,6 +17,7 @@ class AlertListPanel extends PanelCtrl {
{text: 'Recent state changes', value: 'changes'}
];
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();
}

View File

@ -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";

View File

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