mirror of
				https://github.com/grafana/grafana.git
				synced 2025-02-25 18:55:37 -06:00 
			
		
		
		
	Merge branch 'master' into go_routines
This commit is contained in:
		@@ -244,7 +244,8 @@ func Register(r *macaron.Macaron) {
 | 
			
		||||
		r.Get("/search/", Search)
 | 
			
		||||
 | 
			
		||||
		// metrics
 | 
			
		||||
		r.Get("/metrics/test", wrap(GetTestMetrics))
 | 
			
		||||
		r.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
 | 
			
		||||
		r.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
 | 
			
		||||
 | 
			
		||||
		// metrics
 | 
			
		||||
		r.Get("/metrics", wrap(GetInternalMetrics))
 | 
			
		||||
 
 | 
			
		||||
@@ -96,13 +96,10 @@ func (slice DataSourceList) Swap(i, j int) {
 | 
			
		||||
	slice[i], slice[j] = slice[j], slice[i]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MetricQueryResultDto struct {
 | 
			
		||||
	Data []MetricQueryResultDataDto `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MetricQueryResultDataDto struct {
 | 
			
		||||
	Target     string       `json:"target"`
 | 
			
		||||
	DataPoints [][2]float64 `json:"datapoints"`
 | 
			
		||||
type MetricRequest struct {
 | 
			
		||||
	From    string             `json:"from"`
 | 
			
		||||
	To      string             `json:"to"`
 | 
			
		||||
	Queries []*simplejson.Json `json:"queries"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UserStars struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -165,7 +165,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if c.OrgRole == m.ROLE_ADMIN {
 | 
			
		||||
			if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
 | 
			
		||||
				appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
 | 
			
		||||
				appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,39 +2,54 @@ package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/grafana/grafana/pkg/api/dtos"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/metrics"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/middleware"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/tsdb"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/tsdb/testdata"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetTestMetrics(c *middleware.Context) Response {
 | 
			
		||||
	from := c.QueryInt64("from")
 | 
			
		||||
	to := c.QueryInt64("to")
 | 
			
		||||
	maxDataPoints := c.QueryInt64("maxDataPoints")
 | 
			
		||||
	stepInSeconds := (to - from) / maxDataPoints
 | 
			
		||||
// POST /api/tsdb/query
 | 
			
		||||
func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
 | 
			
		||||
	timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
 | 
			
		||||
 | 
			
		||||
	result := dtos.MetricQueryResultDto{}
 | 
			
		||||
	result.Data = make([]dtos.MetricQueryResultDataDto, 1)
 | 
			
		||||
	request := &tsdb.Request{TimeRange: timeRange}
 | 
			
		||||
 | 
			
		||||
	for seriesIndex := range result.Data {
 | 
			
		||||
		points := make([][2]float64, maxDataPoints)
 | 
			
		||||
		walker := rand.Float64() * 100
 | 
			
		||||
		time := from
 | 
			
		||||
	for _, query := range reqDto.Queries {
 | 
			
		||||
		request.Queries = append(request.Queries, &tsdb.Query{
 | 
			
		||||
			RefId:         query.Get("refId").MustString("A"),
 | 
			
		||||
			MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
 | 
			
		||||
			IntervalMs:    query.Get("intervalMs").MustInt64(1000),
 | 
			
		||||
			Model:         query,
 | 
			
		||||
			DataSource: &tsdb.DataSourceInfo{
 | 
			
		||||
				Name:     "Grafana TestDataDB",
 | 
			
		||||
				PluginId: "grafana-testdata-datasource",
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		for i := range points {
 | 
			
		||||
			points[i][0] = walker
 | 
			
		||||
			points[i][1] = float64(time)
 | 
			
		||||
			walker += rand.Float64() - 0.5
 | 
			
		||||
			time += stepInSeconds
 | 
			
		||||
		}
 | 
			
		||||
	resp, err := tsdb.HandleRequest(request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ApiError(500, "Metric request error", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex)
 | 
			
		||||
		result.Data[seriesIndex].DataPoints = points
 | 
			
		||||
	return Json(200, &resp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GET /api/tsdb/testdata/scenarios
 | 
			
		||||
func GetTestDataScenarios(c *middleware.Context) Response {
 | 
			
		||||
	result := make([]interface{}, 0)
 | 
			
		||||
 | 
			
		||||
	for _, scenario := range testdata.ScenarioRegistry {
 | 
			
		||||
		result = append(result, map[string]interface{}{
 | 
			
		||||
			"id":          scenario.Id,
 | 
			
		||||
			"name":        scenario.Name,
 | 
			
		||||
			"description": scenario.Description,
 | 
			
		||||
			"stringInput": scenario.StringInput,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return Json(200, &result)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ type DataSourcePlugin struct {
 | 
			
		||||
	FrontendPluginBase
 | 
			
		||||
	Annotations bool   `json:"annotations"`
 | 
			
		||||
	Metrics     bool   `json:"metrics"`
 | 
			
		||||
	Alerting    bool   `json:"alerting"`
 | 
			
		||||
	BuiltIn     bool   `json:"builtIn"`
 | 
			
		||||
	Mixed       bool   `json:"mixed"`
 | 
			
		||||
	App         string `json:"app"`
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,12 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
 | 
			
		||||
	appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1)
 | 
			
		||||
	fp.IncludedInAppId = app.Id
 | 
			
		||||
	fp.BaseUrl = app.BaseUrl
 | 
			
		||||
	fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
 | 
			
		||||
 | 
			
		||||
	if isExternalPlugin(app.PluginDir) {
 | 
			
		||||
		fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
 | 
			
		||||
	} else {
 | 
			
		||||
		fp.Module = util.JoinUrlFragments("app/plugins/app/"+app.Id, appSubPath) + "/module"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (fp *FrontendPluginBase) handleModuleDefaults() {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/grafana/grafana/pkg/components/simplejson"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/services/alerting"
 | 
			
		||||
	"gopkg.in/guregu/null.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -13,13 +14,13 @@ var (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AlertEvaluator interface {
 | 
			
		||||
	Eval(reducedValue *float64) bool
 | 
			
		||||
	Eval(reducedValue null.Float) bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NoDataEvaluator struct{}
 | 
			
		||||
 | 
			
		||||
func (e *NoDataEvaluator) Eval(reducedValue *float64) bool {
 | 
			
		||||
	return reducedValue == nil
 | 
			
		||||
func (e *NoDataEvaluator) Eval(reducedValue null.Float) bool {
 | 
			
		||||
	return reducedValue.Valid == false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ThresholdEvaluator struct {
 | 
			
		||||
@@ -43,16 +44,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
 | 
			
		||||
	return defaultEval, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool {
 | 
			
		||||
	if reducedValue == nil {
 | 
			
		||||
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
 | 
			
		||||
	if reducedValue.Valid == false {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch e.Type {
 | 
			
		||||
	case "gt":
 | 
			
		||||
		return *reducedValue > e.Threshold
 | 
			
		||||
		return reducedValue.Float64 > e.Threshold
 | 
			
		||||
	case "lt":
 | 
			
		||||
		return *reducedValue < e.Threshold
 | 
			
		||||
		return reducedValue.Float64 < e.Threshold
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
@@ -86,16 +87,18 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
 | 
			
		||||
	return rangedEval, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *RangedEvaluator) Eval(reducedValue *float64) bool {
 | 
			
		||||
	if reducedValue == nil {
 | 
			
		||||
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
 | 
			
		||||
	if reducedValue.Valid == false {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	floatValue := reducedValue.Float64
 | 
			
		||||
 | 
			
		||||
	switch e.Type {
 | 
			
		||||
	case "within_range":
 | 
			
		||||
		return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue)
 | 
			
		||||
		return (e.Lower < floatValue && e.Upper > floatValue) || (e.Upper < floatValue && e.Lower > floatValue)
 | 
			
		||||
	case "outside_range":
 | 
			
		||||
		return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue)
 | 
			
		||||
		return (e.Upper < floatValue && e.Lower < floatValue) || (e.Upper > floatValue && e.Lower > floatValue)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ package conditions
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"gopkg.in/guregu/null.v3"
 | 
			
		||||
 | 
			
		||||
	"github.com/grafana/grafana/pkg/components/simplejson"
 | 
			
		||||
	. "github.com/smartystreets/goconvey/convey"
 | 
			
		||||
)
 | 
			
		||||
@@ -14,7 +16,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
 | 
			
		||||
	evaluator, err := NewAlertEvaluator(jsonModel)
 | 
			
		||||
	So(err, ShouldBeNil)
 | 
			
		||||
 | 
			
		||||
	return evaluator.Eval(&reducedValue)
 | 
			
		||||
	return evaluator.Eval(null.FloatFrom(reducedValue))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestEvalutors(t *testing.T) {
 | 
			
		||||
@@ -51,6 +53,6 @@ func TestEvalutors(t *testing.T) {
 | 
			
		||||
		evaluator, err := NewAlertEvaluator(jsonModel)
 | 
			
		||||
		So(err, ShouldBeNil)
 | 
			
		||||
 | 
			
		||||
		So(evaluator.Eval(nil), ShouldBeTrue)
 | 
			
		||||
		So(evaluator.Eval(null.FloatFromPtr(nil)), ShouldBeTrue)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,8 +34,8 @@ type AlertQuery struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *QueryCondition) Eval(context *alerting.EvalContext) {
 | 
			
		||||
	timerange := tsdb.NewTimerange(c.Query.From, c.Query.To)
 | 
			
		||||
	seriesList, err := c.executeQuery(context, timerange)
 | 
			
		||||
	timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
 | 
			
		||||
	seriesList, err := c.executeQuery(context, timeRange)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		context.Error = err
 | 
			
		||||
		return
 | 
			
		||||
@@ -46,21 +46,21 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
 | 
			
		||||
		reducedValue := c.Reducer.Reduce(series)
 | 
			
		||||
		evalMatch := c.Evaluator.Eval(reducedValue)
 | 
			
		||||
 | 
			
		||||
		if reducedValue == nil {
 | 
			
		||||
		if reducedValue.Valid == false {
 | 
			
		||||
			emptySerieCount++
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if context.IsTestRun {
 | 
			
		||||
			context.Logs = append(context.Logs, &alerting.ResultLogEntry{
 | 
			
		||||
				Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, *reducedValue),
 | 
			
		||||
				Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue.Float64),
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if evalMatch {
 | 
			
		||||
			context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
 | 
			
		||||
				Metric: series.Name,
 | 
			
		||||
				Value:  *reducedValue,
 | 
			
		||||
				Value:  reducedValue.Float64,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -69,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
 | 
			
		||||
	context.Firing = len(context.EvalMatches) > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
	getDsInfo := &m.GetDataSourceByIdQuery{
 | 
			
		||||
		Id:    c.Query.DatasourceId,
 | 
			
		||||
		OrgId: context.Rule.OrgId,
 | 
			
		||||
@@ -79,7 +79,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange t
 | 
			
		||||
		return nil, fmt.Errorf("Could not find datasource")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req := c.getRequestForAlertRule(getDsInfo.Result, timerange)
 | 
			
		||||
	req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
 | 
			
		||||
	result := make(tsdb.TimeSeriesSlice, 0)
 | 
			
		||||
 | 
			
		||||
	resp, err := c.HandleRequest(req)
 | 
			
		||||
@@ -105,9 +105,9 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange t
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timerange tsdb.TimeRange) *tsdb.Request {
 | 
			
		||||
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.Request {
 | 
			
		||||
	req := &tsdb.Request{
 | 
			
		||||
		TimeRange: timerange,
 | 
			
		||||
		TimeRange: timeRange,
 | 
			
		||||
		Queries: []*tsdb.Query{
 | 
			
		||||
			{
 | 
			
		||||
				RefId: "A",
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ package conditions
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	null "gopkg.in/guregu/null.v3"
 | 
			
		||||
 | 
			
		||||
	"github.com/grafana/grafana/pkg/bus"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/components/simplejson"
 | 
			
		||||
	m "github.com/grafana/grafana/pkg/models"
 | 
			
		||||
@@ -41,9 +43,8 @@ func TestQueryCondition(t *testing.T) {
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			Convey("should fire when avg is above 100", func() {
 | 
			
		||||
				one := float64(120)
 | 
			
		||||
				two := float64(0)
 | 
			
		||||
				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
 | 
			
		||||
				points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
 | 
			
		||||
				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
 | 
			
		||||
				ctx.exec()
 | 
			
		||||
 | 
			
		||||
				So(ctx.result.Error, ShouldBeNil)
 | 
			
		||||
@@ -51,9 +52,8 @@ func TestQueryCondition(t *testing.T) {
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			Convey("Should not fire when avg is below 100", func() {
 | 
			
		||||
				one := float64(90)
 | 
			
		||||
				two := float64(0)
 | 
			
		||||
				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
 | 
			
		||||
				points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
 | 
			
		||||
				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
 | 
			
		||||
				ctx.exec()
 | 
			
		||||
 | 
			
		||||
				So(ctx.result.Error, ShouldBeNil)
 | 
			
		||||
@@ -61,11 +61,9 @@ func TestQueryCondition(t *testing.T) {
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			Convey("Should fire if only first serie matches", func() {
 | 
			
		||||
				one := float64(120)
 | 
			
		||||
				two := float64(0)
 | 
			
		||||
				ctx.series = tsdb.TimeSeriesSlice{
 | 
			
		||||
					tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}}),
 | 
			
		||||
					tsdb.NewTimeSeries("test2", [][2]*float64{{&two, &two}}),
 | 
			
		||||
					tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
 | 
			
		||||
					tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
 | 
			
		||||
				}
 | 
			
		||||
				ctx.exec()
 | 
			
		||||
 | 
			
		||||
@@ -76,8 +74,8 @@ func TestQueryCondition(t *testing.T) {
 | 
			
		||||
			Convey("Empty series", func() {
 | 
			
		||||
				Convey("Should set NoDataFound both series are empty", func() {
 | 
			
		||||
					ctx.series = tsdb.TimeSeriesSlice{
 | 
			
		||||
						tsdb.NewTimeSeries("test1", [][2]*float64{}),
 | 
			
		||||
						tsdb.NewTimeSeries("test2", [][2]*float64{}),
 | 
			
		||||
						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
 | 
			
		||||
						tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
 | 
			
		||||
					}
 | 
			
		||||
					ctx.exec()
 | 
			
		||||
 | 
			
		||||
@@ -86,10 +84,9 @@ func TestQueryCondition(t *testing.T) {
 | 
			
		||||
				})
 | 
			
		||||
 | 
			
		||||
				Convey("Should set NoDataFound both series contains null", func() {
 | 
			
		||||
					one := float64(120)
 | 
			
		||||
					ctx.series = tsdb.TimeSeriesSlice{
 | 
			
		||||
						tsdb.NewTimeSeries("test1", [][2]*float64{{nil, &one}}),
 | 
			
		||||
						tsdb.NewTimeSeries("test2", [][2]*float64{{nil, &one}}),
 | 
			
		||||
						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()
 | 
			
		||||
 | 
			
		||||
@@ -98,11 +95,9 @@ func TestQueryCondition(t *testing.T) {
 | 
			
		||||
				})
 | 
			
		||||
 | 
			
		||||
				Convey("Should not set NoDataFound if one serie is empty", func() {
 | 
			
		||||
					one := float64(120)
 | 
			
		||||
					two := float64(0)
 | 
			
		||||
					ctx.series = tsdb.TimeSeriesSlice{
 | 
			
		||||
						tsdb.NewTimeSeries("test1", [][2]*float64{}),
 | 
			
		||||
						tsdb.NewTimeSeries("test2", [][2]*float64{{&one, &two}}),
 | 
			
		||||
						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
 | 
			
		||||
						tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
 | 
			
		||||
					}
 | 
			
		||||
					ctx.exec()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,19 +4,20 @@ import (
 | 
			
		||||
	"math"
 | 
			
		||||
 | 
			
		||||
	"github.com/grafana/grafana/pkg/tsdb"
 | 
			
		||||
	"gopkg.in/guregu/null.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type QueryReducer interface {
 | 
			
		||||
	Reduce(timeSeries *tsdb.TimeSeries) *float64
 | 
			
		||||
	Reduce(timeSeries *tsdb.TimeSeries) null.Float
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SimpleReducer struct {
 | 
			
		||||
	Type string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
 | 
			
		||||
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
 | 
			
		||||
	if len(series.Points) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
		return null.FloatFromPtr(nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	value := float64(0)
 | 
			
		||||
@@ -25,36 +26,36 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
 | 
			
		||||
	switch s.Type {
 | 
			
		||||
	case "avg":
 | 
			
		||||
		for _, point := range series.Points {
 | 
			
		||||
			if point[0] != nil {
 | 
			
		||||
				value += *point[0]
 | 
			
		||||
			if point[0].Valid {
 | 
			
		||||
				value += point[0].Float64
 | 
			
		||||
				allNull = false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		value = value / float64(len(series.Points))
 | 
			
		||||
	case "sum":
 | 
			
		||||
		for _, point := range series.Points {
 | 
			
		||||
			if point[0] != nil {
 | 
			
		||||
				value += *point[0]
 | 
			
		||||
			if point[0].Valid {
 | 
			
		||||
				value += point[0].Float64
 | 
			
		||||
				allNull = false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case "min":
 | 
			
		||||
		value = math.MaxFloat64
 | 
			
		||||
		for _, point := range series.Points {
 | 
			
		||||
			if point[0] != nil {
 | 
			
		||||
			if point[0].Valid {
 | 
			
		||||
				allNull = false
 | 
			
		||||
				if value > *point[0] {
 | 
			
		||||
					value = *point[0]
 | 
			
		||||
				if value > point[0].Float64 {
 | 
			
		||||
					value = point[0].Float64
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case "max":
 | 
			
		||||
		value = -math.MaxFloat64
 | 
			
		||||
		for _, point := range series.Points {
 | 
			
		||||
			if point[0] != nil {
 | 
			
		||||
			if point[0].Valid {
 | 
			
		||||
				allNull = false
 | 
			
		||||
				if value < *point[0] {
 | 
			
		||||
					value = *point[0]
 | 
			
		||||
				if value < point[0].Float64 {
 | 
			
		||||
					value = point[0].Float64
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -64,10 +65,10 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if allNull {
 | 
			
		||||
		return nil
 | 
			
		||||
		return null.FloatFromPtr(nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &value
 | 
			
		||||
	return null.FloatFrom(value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSimpleReducer(typ string) *SimpleReducer {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,44 +10,41 @@ import (
 | 
			
		||||
func TestSimpleReducer(t *testing.T) {
 | 
			
		||||
	Convey("Test simple reducer by calculating", t, func() {
 | 
			
		||||
		Convey("avg", func() {
 | 
			
		||||
			result := *testReducer("avg", 1, 2, 3)
 | 
			
		||||
			result := testReducer("avg", 1, 2, 3)
 | 
			
		||||
			So(result, ShouldEqual, float64(2))
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		Convey("sum", func() {
 | 
			
		||||
			result := *testReducer("sum", 1, 2, 3)
 | 
			
		||||
			result := testReducer("sum", 1, 2, 3)
 | 
			
		||||
			So(result, ShouldEqual, float64(6))
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		Convey("min", func() {
 | 
			
		||||
			result := *testReducer("min", 3, 2, 1)
 | 
			
		||||
			result := testReducer("min", 3, 2, 1)
 | 
			
		||||
			So(result, ShouldEqual, float64(1))
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		Convey("max", func() {
 | 
			
		||||
			result := *testReducer("max", 1, 2, 3)
 | 
			
		||||
			result := testReducer("max", 1, 2, 3)
 | 
			
		||||
			So(result, ShouldEqual, float64(3))
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		Convey("count", func() {
 | 
			
		||||
			result := *testReducer("count", 1, 2, 3000)
 | 
			
		||||
			result := testReducer("count", 1, 2, 3000)
 | 
			
		||||
			So(result, ShouldEqual, float64(3))
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testReducer(typ string, datapoints ...float64) *float64 {
 | 
			
		||||
func testReducer(typ string, datapoints ...float64) float64 {
 | 
			
		||||
	reducer := NewSimpleReducer(typ)
 | 
			
		||||
	var timeserie [][2]*float64
 | 
			
		||||
	dummieTimestamp := float64(521452145)
 | 
			
		||||
	series := &tsdb.TimeSeries{
 | 
			
		||||
		Name: "test time serie",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for idx := range datapoints {
 | 
			
		||||
		timeserie = append(timeserie, [2]*float64{&datapoints[idx], &dummieTimestamp})
 | 
			
		||||
		series.Points = append(series.Points, tsdb.NewTimePoint(datapoints[idx], 1234134))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tsdb := &tsdb.TimeSeries{
 | 
			
		||||
		Name:   "test time serie",
 | 
			
		||||
		Points: timeserie,
 | 
			
		||||
	}
 | 
			
		||||
	return reducer.Reduce(tsdb)
 | 
			
		||||
	return reducer.Reduce(series).Float64
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"github.com/grafana/grafana/pkg/setting"
 | 
			
		||||
	_ "github.com/grafana/grafana/pkg/tsdb/graphite"
 | 
			
		||||
	_ "github.com/grafana/grafana/pkg/tsdb/prometheus"
 | 
			
		||||
	_ "github.com/grafana/grafana/pkg/tsdb/testdata"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var engine *alerting.Engine
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result.QueryResults = make(map[string]*tsdb.QueryResult)
 | 
			
		||||
	queryRes := &tsdb.QueryResult{}
 | 
			
		||||
	queryRes := tsdb.NewQueryResult()
 | 
			
		||||
 | 
			
		||||
	for _, series := range data {
 | 
			
		||||
		queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
 | 
			
		||||
			Name:   series.Target,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
package graphite
 | 
			
		||||
 | 
			
		||||
import "github.com/grafana/grafana/pkg/tsdb"
 | 
			
		||||
 | 
			
		||||
type TargetResponseDTO struct {
 | 
			
		||||
	Target     string        `json:"target"`
 | 
			
		||||
	DataPoints [][2]*float64 `json:"datapoints"`
 | 
			
		||||
	Target     string                `json:"target"`
 | 
			
		||||
	DataPoints tsdb.TimeSeriesPoints `json:"datapoints"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +1,31 @@
 | 
			
		||||
package tsdb
 | 
			
		||||
 | 
			
		||||
import "github.com/grafana/grafana/pkg/components/simplejson"
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/grafana/grafana/pkg/components/simplejson"
 | 
			
		||||
	"gopkg.in/guregu/null.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Query struct {
 | 
			
		||||
	RefId      string
 | 
			
		||||
	Query      string
 | 
			
		||||
	Model      *simplejson.Json
 | 
			
		||||
	Depends    []string
 | 
			
		||||
	DataSource *DataSourceInfo
 | 
			
		||||
	Results    []*TimeSeries
 | 
			
		||||
	Exclude    bool
 | 
			
		||||
	RefId         string
 | 
			
		||||
	Model         *simplejson.Json
 | 
			
		||||
	Depends       []string
 | 
			
		||||
	DataSource    *DataSourceInfo
 | 
			
		||||
	Results       []*TimeSeries
 | 
			
		||||
	Exclude       bool
 | 
			
		||||
	MaxDataPoints int64
 | 
			
		||||
	IntervalMs    int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type QuerySlice []*Query
 | 
			
		||||
 | 
			
		||||
type Request struct {
 | 
			
		||||
	TimeRange     TimeRange
 | 
			
		||||
	MaxDataPoints int
 | 
			
		||||
	Queries       QuerySlice
 | 
			
		||||
	TimeRange *TimeRange
 | 
			
		||||
	Queries   QuerySlice
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Response struct {
 | 
			
		||||
	BatchTimings []*BatchTiming
 | 
			
		||||
	Results      map[string]*QueryResult
 | 
			
		||||
	BatchTimings []*BatchTiming          `json:"timings"`
 | 
			
		||||
	Results      map[string]*QueryResult `json:"results"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DataSourceInfo struct {
 | 
			
		||||
@@ -49,19 +52,41 @@ type BatchResult struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type QueryResult struct {
 | 
			
		||||
	Error  error
 | 
			
		||||
	RefId  string
 | 
			
		||||
	Series TimeSeriesSlice
 | 
			
		||||
	Error  error           `json:"error"`
 | 
			
		||||
	RefId  string          `json:"refId"`
 | 
			
		||||
	Series TimeSeriesSlice `json:"series"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TimeSeries struct {
 | 
			
		||||
	Name   string        `json:"name"`
 | 
			
		||||
	Points [][2]*float64 `json:"points"`
 | 
			
		||||
	Name   string           `json:"name"`
 | 
			
		||||
	Points TimeSeriesPoints `json:"points"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TimePoint [2]null.Float
 | 
			
		||||
type TimeSeriesPoints []TimePoint
 | 
			
		||||
type TimeSeriesSlice []*TimeSeries
 | 
			
		||||
 | 
			
		||||
func NewTimeSeries(name string, points [][2]*float64) *TimeSeries {
 | 
			
		||||
func NewQueryResult() *QueryResult {
 | 
			
		||||
	return &QueryResult{
 | 
			
		||||
		Series: make(TimeSeriesSlice, 0),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTimePoint(value float64, timestamp float64) TimePoint {
 | 
			
		||||
	return TimePoint{null.FloatFrom(value), null.FloatFrom(timestamp)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTimeSeriesPointsFromArgs(values ...float64) TimeSeriesPoints {
 | 
			
		||||
	points := make(TimeSeriesPoints, 0)
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < len(values); i += 2 {
 | 
			
		||||
		points = append(points, NewTimePoint(values[i], values[i+1]))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return points
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTimeSeries(name string, points TimeSeriesPoints) *TimeSeries {
 | 
			
		||||
	return &TimeSeries{
 | 
			
		||||
		Name:   name,
 | 
			
		||||
		Points: points,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,8 @@ import (
 | 
			
		||||
	"github.com/grafana/grafana/pkg/log"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/tsdb"
 | 
			
		||||
	"github.com/prometheus/client_golang/api/prometheus"
 | 
			
		||||
	"golang.org/x/net/context"
 | 
			
		||||
	pmodel "github.com/prometheus/common/model"
 | 
			
		||||
	"golang.org/x/net/context"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PrometheusExecutor struct {
 | 
			
		||||
@@ -111,12 +111,12 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	start, err := queryContext.TimeRange.FromTime()
 | 
			
		||||
	start, err := queryContext.TimeRange.ParseFrom()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	end, err := queryContext.TimeRange.ToTime()
 | 
			
		||||
	end, err := queryContext.TimeRange.ParseTo()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -132,7 +132,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
 | 
			
		||||
 | 
			
		||||
func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) {
 | 
			
		||||
	queryResults := make(map[string]*tsdb.QueryResult)
 | 
			
		||||
	queryRes := &tsdb.QueryResult{}
 | 
			
		||||
	queryRes := tsdb.NewQueryResult()
 | 
			
		||||
 | 
			
		||||
	data, ok := value.(pmodel.Matrix)
 | 
			
		||||
	if !ok {
 | 
			
		||||
@@ -140,17 +140,15 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, v := range data {
 | 
			
		||||
		var points [][2]*float64
 | 
			
		||||
		for _, k := range v.Values {
 | 
			
		||||
			timestamp := float64(k.Timestamp)
 | 
			
		||||
			val := float64(k.Value)
 | 
			
		||||
			points = append(points, [2]*float64{&val, ×tamp})
 | 
			
		||||
		series := tsdb.TimeSeries{
 | 
			
		||||
			Name: formatLegend(v.Metric, query),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
 | 
			
		||||
			Name:   formatLegend(v.Metric, query),
 | 
			
		||||
			Points: points,
 | 
			
		||||
		})
 | 
			
		||||
		for _, k := range v.Values {
 | 
			
		||||
			series.Points = append(series.Points, tsdb.NewTimePoint(float64(k.Value), float64(k.Timestamp.Unix()*1000)))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		queryRes.Series = append(queryRes.Series, &series)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	queryResults["A"] = queryRes
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ package tsdb
 | 
			
		||||
import "sync"
 | 
			
		||||
 | 
			
		||||
type QueryContext struct {
 | 
			
		||||
	TimeRange   TimeRange
 | 
			
		||||
	TimeRange   *TimeRange
 | 
			
		||||
	Queries     QuerySlice
 | 
			
		||||
	Results     map[string]*QueryResult
 | 
			
		||||
	ResultsChan chan *BatchResult
 | 
			
		||||
@@ -11,7 +11,7 @@ type QueryContext struct {
 | 
			
		||||
	BatchWaits  sync.WaitGroup
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewQueryContext(queries QuerySlice, timeRange TimeRange) *QueryContext {
 | 
			
		||||
func NewQueryContext(queries QuerySlice, timeRange *TimeRange) *QueryContext {
 | 
			
		||||
	return &QueryContext{
 | 
			
		||||
		TimeRange:   timeRange,
 | 
			
		||||
		Queries:     queries,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										130
									
								
								pkg/tsdb/testdata/scenarios.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								pkg/tsdb/testdata/scenarios.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
			
		||||
package testdata
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/grafana/grafana/pkg/log"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/tsdb"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ScenarioHandler func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult
 | 
			
		||||
 | 
			
		||||
type Scenario struct {
 | 
			
		||||
	Id          string          `json:"id"`
 | 
			
		||||
	Name        string          `json:"name"`
 | 
			
		||||
	StringInput string          `json:"stringOption"`
 | 
			
		||||
	Description string          `json:"description"`
 | 
			
		||||
	Handler     ScenarioHandler `json:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var ScenarioRegistry map[string]*Scenario
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	ScenarioRegistry = make(map[string]*Scenario)
 | 
			
		||||
	logger := log.New("tsdb.testdata")
 | 
			
		||||
 | 
			
		||||
	logger.Debug("Initializing TestData Scenario")
 | 
			
		||||
 | 
			
		||||
	registerScenario(&Scenario{
 | 
			
		||||
		Id:   "random_walk",
 | 
			
		||||
		Name: "Random Walk",
 | 
			
		||||
 | 
			
		||||
		Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
 | 
			
		||||
			timeWalkerMs := context.TimeRange.GetFromAsMsEpoch()
 | 
			
		||||
			to := context.TimeRange.GetToAsMsEpoch()
 | 
			
		||||
 | 
			
		||||
			series := newSeriesForQuery(query)
 | 
			
		||||
 | 
			
		||||
			points := make(tsdb.TimeSeriesPoints, 0)
 | 
			
		||||
			walker := rand.Float64() * 100
 | 
			
		||||
 | 
			
		||||
			for i := int64(0); i < 10000 && timeWalkerMs < to; i++ {
 | 
			
		||||
				points = append(points, tsdb.NewTimePoint(walker, float64(timeWalkerMs)))
 | 
			
		||||
 | 
			
		||||
				walker += rand.Float64() - 0.5
 | 
			
		||||
				timeWalkerMs += query.IntervalMs
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			series.Points = points
 | 
			
		||||
 | 
			
		||||
			queryRes := tsdb.NewQueryResult()
 | 
			
		||||
			queryRes.Series = append(queryRes.Series, series)
 | 
			
		||||
			return queryRes
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	registerScenario(&Scenario{
 | 
			
		||||
		Id:   "no_data_points",
 | 
			
		||||
		Name: "No Data Points",
 | 
			
		||||
		Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
 | 
			
		||||
			return tsdb.NewQueryResult()
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	registerScenario(&Scenario{
 | 
			
		||||
		Id:   "datapoints_outside_range",
 | 
			
		||||
		Name: "Datapoints Outside Range",
 | 
			
		||||
		Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
 | 
			
		||||
			queryRes := tsdb.NewQueryResult()
 | 
			
		||||
 | 
			
		||||
			series := newSeriesForQuery(query)
 | 
			
		||||
			outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000
 | 
			
		||||
 | 
			
		||||
			series.Points = append(series.Points, tsdb.NewTimePoint(10, float64(outsideTime)))
 | 
			
		||||
			queryRes.Series = append(queryRes.Series, series)
 | 
			
		||||
 | 
			
		||||
			return queryRes
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	registerScenario(&Scenario{
 | 
			
		||||
		Id:          "csv_metric_values",
 | 
			
		||||
		Name:        "CSV Metric Values",
 | 
			
		||||
		StringInput: "1,20,90,30,5,0",
 | 
			
		||||
		Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
 | 
			
		||||
			queryRes := tsdb.NewQueryResult()
 | 
			
		||||
 | 
			
		||||
			stringInput := query.Model.Get("stringInput").MustString()
 | 
			
		||||
			values := []float64{}
 | 
			
		||||
			for _, strVal := range strings.Split(stringInput, ",") {
 | 
			
		||||
				if val, err := strconv.ParseFloat(strVal, 64); err == nil {
 | 
			
		||||
					values = append(values, val)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(values) == 0 {
 | 
			
		||||
				return queryRes
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			series := newSeriesForQuery(query)
 | 
			
		||||
			startTime := context.TimeRange.GetFromAsMsEpoch()
 | 
			
		||||
			endTime := context.TimeRange.GetToAsMsEpoch()
 | 
			
		||||
			step := (endTime - startTime) / int64(len(values)-1)
 | 
			
		||||
 | 
			
		||||
			for _, val := range values {
 | 
			
		||||
				series.Points = append(series.Points, tsdb.NewTimePoint(val, float64(startTime)))
 | 
			
		||||
				startTime += step
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			queryRes.Series = append(queryRes.Series, series)
 | 
			
		||||
 | 
			
		||||
			return queryRes
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func registerScenario(scenario *Scenario) {
 | 
			
		||||
	ScenarioRegistry[scenario.Id] = scenario
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newSeriesForQuery(query *tsdb.Query) *tsdb.TimeSeries {
 | 
			
		||||
	alias := query.Model.Get("alias").MustString("")
 | 
			
		||||
	if alias == "" {
 | 
			
		||||
		alias = query.RefId + "-series"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &tsdb.TimeSeries{Name: alias}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								pkg/tsdb/testdata/testdata.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								pkg/tsdb/testdata/testdata.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
package testdata
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/grafana/grafana/pkg/log"
 | 
			
		||||
	"github.com/grafana/grafana/pkg/tsdb"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TestDataExecutor struct {
 | 
			
		||||
	*tsdb.DataSourceInfo
 | 
			
		||||
	log log.Logger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
 | 
			
		||||
	return &TestDataExecutor{
 | 
			
		||||
		DataSourceInfo: dsInfo,
 | 
			
		||||
		log:            log.New("tsdb.testdata"),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
 | 
			
		||||
	result := &tsdb.BatchResult{}
 | 
			
		||||
	result.QueryResults = make(map[string]*tsdb.QueryResult)
 | 
			
		||||
 | 
			
		||||
	for _, query := range queries {
 | 
			
		||||
		scenarioId := query.Model.Get("scenarioId").MustString("random_walk")
 | 
			
		||||
		if scenario, exist := ScenarioRegistry[scenarioId]; exist {
 | 
			
		||||
			result.QueryResults[query.RefId] = scenario.Handler(query, context)
 | 
			
		||||
			result.QueryResults[query.RefId].RefId = query.RefId
 | 
			
		||||
		} else {
 | 
			
		||||
			e.log.Error("Scenario not found", "scenarioId", scenarioId)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
@@ -2,12 +2,13 @@ package tsdb
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewTimerange(from, to string) TimeRange {
 | 
			
		||||
	return TimeRange{
 | 
			
		||||
func NewTimeRange(from, to string) *TimeRange {
 | 
			
		||||
	return &TimeRange{
 | 
			
		||||
		From: from,
 | 
			
		||||
		To:   to,
 | 
			
		||||
		Now:  time.Now(),
 | 
			
		||||
@@ -20,9 +21,45 @@ type TimeRange struct {
 | 
			
		||||
	Now  time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tr TimeRange) FromTime() (time.Time, error) {
 | 
			
		||||
	fromRaw := strings.Replace(tr.From, "now-", "", 1)
 | 
			
		||||
func (tr *TimeRange) GetFromAsMsEpoch() int64 {
 | 
			
		||||
	return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tr *TimeRange) GetToAsMsEpoch() int64 {
 | 
			
		||||
	return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tr *TimeRange) MustGetFrom() time.Time {
 | 
			
		||||
	if res, err := tr.ParseFrom(); err != nil {
 | 
			
		||||
		return time.Unix(0, 0)
 | 
			
		||||
	} else {
 | 
			
		||||
		return res
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tr *TimeRange) MustGetTo() time.Time {
 | 
			
		||||
	if res, err := tr.ParseTo(); err != nil {
 | 
			
		||||
		return time.Unix(0, 0)
 | 
			
		||||
	} else {
 | 
			
		||||
		return res
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func tryParseUnixMsEpoch(val string) (time.Time, bool) {
 | 
			
		||||
	if val, err := strconv.ParseInt(val, 10, 64); err == nil {
 | 
			
		||||
		seconds := val / 1000
 | 
			
		||||
		nano := (val - seconds*1000) * 1000000
 | 
			
		||||
		return time.Unix(seconds, nano), true
 | 
			
		||||
	}
 | 
			
		||||
	return time.Time{}, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tr *TimeRange) ParseFrom() (time.Time, error) {
 | 
			
		||||
	if res, ok := tryParseUnixMsEpoch(tr.From); ok {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fromRaw := strings.Replace(tr.From, "now-", "", 1)
 | 
			
		||||
	diff, err := time.ParseDuration("-" + fromRaw)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return time.Time{}, err
 | 
			
		||||
@@ -31,7 +68,7 @@ func (tr TimeRange) FromTime() (time.Time, error) {
 | 
			
		||||
	return tr.Now.Add(diff), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tr TimeRange) ToTime() (time.Time, error) {
 | 
			
		||||
func (tr *TimeRange) ParseTo() (time.Time, error) {
 | 
			
		||||
	if tr.To == "now" {
 | 
			
		||||
		return tr.Now, nil
 | 
			
		||||
	} else if strings.HasPrefix(tr.To, "now-") {
 | 
			
		||||
@@ -45,5 +82,9 @@ func (tr TimeRange) ToTime() (time.Time, error) {
 | 
			
		||||
		return tr.Now.Add(diff), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res, ok := tryParseUnixMsEpoch(tr.To); ok {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,13 +23,13 @@ func TestTimeRange(t *testing.T) {
 | 
			
		||||
				fiveMinAgo, _ := time.ParseDuration("-5m")
 | 
			
		||||
				expected := now.Add(fiveMinAgo)
 | 
			
		||||
 | 
			
		||||
				res, err := tr.FromTime()
 | 
			
		||||
				res, err := tr.ParseFrom()
 | 
			
		||||
				So(err, ShouldBeNil)
 | 
			
		||||
				So(res.Unix(), ShouldEqual, expected.Unix())
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			Convey("now ", func() {
 | 
			
		||||
				res, err := tr.ToTime()
 | 
			
		||||
				res, err := tr.ParseTo()
 | 
			
		||||
				So(err, ShouldBeNil)
 | 
			
		||||
				So(res.Unix(), ShouldEqual, now.Unix())
 | 
			
		||||
			})
 | 
			
		||||
@@ -46,7 +46,7 @@ func TestTimeRange(t *testing.T) {
 | 
			
		||||
				fiveHourAgo, _ := time.ParseDuration("-5h")
 | 
			
		||||
				expected := now.Add(fiveHourAgo)
 | 
			
		||||
 | 
			
		||||
				res, err := tr.FromTime()
 | 
			
		||||
				res, err := tr.ParseFrom()
 | 
			
		||||
				So(err, ShouldBeNil)
 | 
			
		||||
				So(res.Unix(), ShouldEqual, expected.Unix())
 | 
			
		||||
			})
 | 
			
		||||
@@ -54,12 +54,29 @@ func TestTimeRange(t *testing.T) {
 | 
			
		||||
			Convey("now-10m ", func() {
 | 
			
		||||
				fiveMinAgo, _ := time.ParseDuration("-10m")
 | 
			
		||||
				expected := now.Add(fiveMinAgo)
 | 
			
		||||
				res, err := tr.ToTime()
 | 
			
		||||
				res, err := tr.ParseTo()
 | 
			
		||||
				So(err, ShouldBeNil)
 | 
			
		||||
				So(res.Unix(), ShouldEqual, expected.Unix())
 | 
			
		||||
			})
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		Convey("can parse unix epocs", func() {
 | 
			
		||||
			var err error
 | 
			
		||||
			tr := TimeRange{
 | 
			
		||||
				From: "1474973725473",
 | 
			
		||||
				To:   "1474975757930",
 | 
			
		||||
				Now:  now,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			res, err := tr.ParseFrom()
 | 
			
		||||
			So(err, ShouldBeNil)
 | 
			
		||||
			So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474973725473)
 | 
			
		||||
 | 
			
		||||
			res, err = tr.ParseTo()
 | 
			
		||||
			So(err, ShouldBeNil)
 | 
			
		||||
			So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474975757930)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		Convey("Cannot parse asdf", func() {
 | 
			
		||||
			var err error
 | 
			
		||||
			tr := TimeRange{
 | 
			
		||||
@@ -68,10 +85,10 @@ func TestTimeRange(t *testing.T) {
 | 
			
		||||
				Now:  now,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			_, err = tr.FromTime()
 | 
			
		||||
			_, err = tr.ParseFrom()
 | 
			
		||||
			So(err, ShouldNotBeNil)
 | 
			
		||||
 | 
			
		||||
			_, err = tr.ToTime()
 | 
			
		||||
			_, err = tr.ParseTo()
 | 
			
		||||
			So(err, ShouldNotBeNil)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,9 @@ func TestMetricQuery(t *testing.T) {
 | 
			
		||||
		Convey("Given 3 queries for 2 data sources", func() {
 | 
			
		||||
			request := &Request{
 | 
			
		||||
				Queries: QuerySlice{
 | 
			
		||||
					{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
 | 
			
		||||
					{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
 | 
			
		||||
					{RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2}},
 | 
			
		||||
					{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
 | 
			
		||||
					{RefId: "B", DataSource: &DataSourceInfo{Id: 1}},
 | 
			
		||||
					{RefId: "C", DataSource: &DataSourceInfo{Id: 2}},
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -31,9 +31,9 @@ func TestMetricQuery(t *testing.T) {
 | 
			
		||||
		Convey("Given query 2 depends on query 1", func() {
 | 
			
		||||
			request := &Request{
 | 
			
		||||
				Queries: QuerySlice{
 | 
			
		||||
					{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
 | 
			
		||||
					{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 2}},
 | 
			
		||||
					{RefId: "C", Query: "#A / #B", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
 | 
			
		||||
					{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
 | 
			
		||||
					{RefId: "B", DataSource: &DataSourceInfo{Id: 2}},
 | 
			
		||||
					{RefId: "C", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -55,7 +55,7 @@ func TestMetricQuery(t *testing.T) {
 | 
			
		||||
	Convey("When executing request with one query", t, func() {
 | 
			
		||||
		req := &Request{
 | 
			
		||||
			Queries: QuerySlice{
 | 
			
		||||
				{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -74,8 +74,8 @@ func TestMetricQuery(t *testing.T) {
 | 
			
		||||
	Convey("When executing one request with two queries from same data source", t, func() {
 | 
			
		||||
		req := &Request{
 | 
			
		||||
			Queries: QuerySlice{
 | 
			
		||||
				{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
				{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
				{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -100,9 +100,9 @@ func TestMetricQuery(t *testing.T) {
 | 
			
		||||
	Convey("When executing one request with three queries from different datasources", t, func() {
 | 
			
		||||
		req := &Request{
 | 
			
		||||
			Queries: QuerySlice{
 | 
			
		||||
				{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
				{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
				{RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
 | 
			
		||||
				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
				{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 | 
			
		||||
				{RefId: "C", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -117,7 +117,7 @@ func TestMetricQuery(t *testing.T) {
 | 
			
		||||
	Convey("When query uses data source of unknown type", t, func() {
 | 
			
		||||
		req := &Request{
 | 
			
		||||
			Queries: QuerySlice{
 | 
			
		||||
				{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
 | 
			
		||||
				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -129,10 +129,10 @@ func TestMetricQuery(t *testing.T) {
 | 
			
		||||
		req := &Request{
 | 
			
		||||
			Queries: QuerySlice{
 | 
			
		||||
				{
 | 
			
		||||
					RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
 | 
			
		||||
					RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					RefId: "B", Query: "#A / 2", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
 | 
			
		||||
					RefId: "B", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user