浏览代码

datasource: testdata - add predictable pulse scenario (#18142)

Adds pulse waveform. Is predictable in the sense that the start of the waveform is aligned to epoch time (instead of the start of the query time). This makes a useful signal for manual testing of alerting in the devenv.
Kyle Brandt 6 年之前
父节点
当前提交
ed099d5ca0

+ 106 - 0
pkg/tsdb/testdata/scenarios.go

@@ -9,6 +9,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/grafana/grafana/pkg/components/simplejson"
+
 	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/infra/log"
 	"github.com/grafana/grafana/pkg/tsdb"
@@ -102,6 +104,15 @@ func init() {
 		},
 	})
 
+	registerScenario(&Scenario{
+		Id:   "predictable_pulse",
+		Name: "Predictable Pulse",
+		Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
+			return getPredictablePulse(query, context)
+		},
+		Description: PredictablePulseDesc,
+	})
+
 	registerScenario(&Scenario{
 		Id:   "random_walk_table",
 		Name: "Random Walk Table",
@@ -342,6 +353,101 @@ func init() {
 	})
 }
 
+// PredictablePulseDesc is the description for the Predictable Pulse scenerio.
+const PredictablePulseDesc = `Predictable Pulse returns a pulse wave where there is a datapoint every timeStepSeconds.
+The wave cycles at timeStepSeconds*(onCount+offCount).
+The cycle of the wave is based off of absolute time (from the epoch) which makes it predictable.
+Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means times will all end in :00 seconds).`
+
+func getPredictablePulse(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
+	queryRes := tsdb.NewQueryResult()
+
+	// Process Input
+	var timeStep int64
+	var onCount int64
+	var offCount int64
+	var onValue null.Float
+	var offValue null.Float
+
+	options := query.Model.Get("pulseWave")
+
+	var err error
+	if timeStep, err = options.Get("timeStep").Int64(); err != nil {
+		queryRes.Error = fmt.Errorf("failed to parse timeStep value '%v' into integer: %v", options.Get("timeStep"), err)
+		return queryRes
+	}
+	if onCount, err = options.Get("onCount").Int64(); err != nil {
+		queryRes.Error = fmt.Errorf("failed to parse onCount value '%v' into integer: %v", options.Get("onCount"), err)
+		return queryRes
+	}
+	if offCount, err = options.Get("offCount").Int64(); err != nil {
+		queryRes.Error = fmt.Errorf("failed to parse offCount value '%v' into integer: %v", options.Get("offCount"), err)
+		return queryRes
+	}
+
+	fromStringOrNumber := func(val *simplejson.Json) (null.Float, error) {
+		switch v := val.Interface().(type) {
+		case json.Number:
+			fV, err := v.Float64()
+			if err != nil {
+				return null.Float{}, err
+			}
+			return null.FloatFrom(fV), nil
+		case string:
+			if v == "null" {
+				return null.FloatFromPtr(nil), nil
+			}
+			fV, err := strconv.ParseFloat(v, 64)
+			if err != nil {
+				return null.Float{}, err
+			}
+			return null.FloatFrom(fV), nil
+		default:
+			return null.Float{}, fmt.Errorf("failed to extract value")
+		}
+	}
+	onValue, err = fromStringOrNumber(options.Get("onValue"))
+	if err != nil {
+		queryRes.Error = fmt.Errorf("failed to parse onValue value '%v' into float: %v", options.Get("onValue"), err)
+		return queryRes
+	}
+	offValue, err = fromStringOrNumber(options.Get("offValue"))
+	if err != nil {
+		queryRes.Error = fmt.Errorf("failed to parse offValue value '%v' into float: %v", options.Get("offValue"), err)
+		return queryRes
+	}
+
+	from := context.TimeRange.GetFromAsMsEpoch()
+	to := context.TimeRange.GetToAsMsEpoch()
+
+	series := newSeriesForQuery(query)
+	points := make(tsdb.TimeSeriesPoints, 0)
+
+	timeStep = timeStep * 1000             // Seconds to Milliseconds
+	timeCursor := from - (from % timeStep) // Truncate Start
+	wavePeriod := timeStep * (onCount + offCount)
+	maxPoints := 10000 // Don't return too many points
+
+	onFor := func(mod int64) null.Float { // How many items in the cycle should get the on value
+		var i int64
+		for i = 0; i < onCount; i++ {
+			if mod == i*timeStep {
+				return onValue
+			}
+		}
+		return offValue
+	}
+	for i := 0; i < maxPoints && timeCursor < to; i++ {
+		point := tsdb.NewTimePoint(onFor(timeCursor%wavePeriod), float64(timeCursor))
+		points = append(points, point)
+		timeCursor += timeStep
+	}
+
+	series.Points = points
+	queryRes.Series = append(queryRes.Series, series)
+	return queryRes
+}
+
 func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResult {
 	timeWalkerMs := tsdbQuery.TimeRange.GetFromAsMsEpoch()
 	to := tsdbQuery.TimeRange.GetToAsMsEpoch()

+ 64 - 0
public/app/plugins/datasource/testdata/partials/query.editor.html

@@ -116,4 +116,68 @@
 			<gf-form-switch class="gf-form" label="Level" label-class="query-keyword width-5" checked="ctrl.target.levelColumn" switch-class="max-width-6" on-change="ctrl.refresh()"></gf-form-switch>
 		</div>
 	</div>
+
+	<!-- Predictable Pulse Scenario Options Form -->
+	<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'predictable_pulse'">
+		<div class="gf-form">
+			<label class="gf-form-label query-keyword width-7">
+				Step
+				<info-popover mode="right-normal">The number of seconds between datapoints.</info-popover>
+			</label>
+				<input type="number"
+				class="gf-form-input width-5"
+				placeholder="60"
+				ng-model="ctrl.target.pulseWave.timeStep"
+				ng-change="ctrl.refresh()"
+				ng-model-onblur />
+		</div>
+		<div class="gf-form">
+			<label class="gf-form-label query-keyword width-7">
+				On Count
+				<info-popover mode="right-normal">The number of values within a cycle, at the start of the cycle, that should have the onValue.</info-popover>
+			</label>
+			<input type="number"
+				class="gf-form-input width-3"
+				placeholder="3"
+				ng-model="ctrl.target.pulseWave.onCount"
+				ng-change="ctrl.refresh()"
+				ng-model-onblur />
+		</div>
+		<div class="gf-form">
+			<label class="gf-form-label query-keyword width-7">
+				Off Count
+				<info-popover mode="right-normal">The number of offValues within the cycle.</info-popover>
+			</label>
+			<input type="number"
+				class="gf-form-input width-3"
+				placeholder="6"
+				ng-model="ctrl.target.pulseWave.offCount"
+				ng-change="ctrl.refresh()"
+				ng-model-onblur />
+		</div>
+		<div class="gf-form">
+			<label class="gf-form-label query-keyword width-7">
+				On Value
+				<info-popover mode="right-normal">The value for "on values", may be a int, float, or null.</info-popover>
+			</label>
+			<input type="string"
+				class="gf-form-input width-5"
+				placeholder="1"
+				ng-model="ctrl.target.pulseWave.onValue"
+				ng-change="ctrl.refresh()"
+				ng-model-onblur />
+		</div>
+		<div class="gf-form">
+			<label class="gf-form-label query-keyword width-7">
+				Off Value
+				<info-popover mode="right-normal">The value for "off values", may be a int, float, or null.</info-popover>
+			</label>
+			<input type="string"
+				class="gf-form-input width-5"
+				placeholder="1"
+				ng-model="ctrl.target.pulseWave.offValue"
+				ng-change="ctrl.refresh()"
+				ng-model-onblur />
+		</div>
+	</div>
 </query-editor-row>

+ 14 - 0
public/app/plugins/datasource/testdata/query_ctrl.ts

@@ -5,6 +5,14 @@ import { defaultQuery } from './StreamHandler';
 import { getBackendSrv } from 'app/core/services/backend_srv';
 import { dateTime } from '@grafana/data';
 
+export const defaultPulse: any = {
+  timeStep: 60,
+  onCount: 3,
+  onValue: 2,
+  offCount: 3,
+  offValue: 1,
+};
+
 export class TestDataQueryCtrl extends QueryCtrl {
   static templateUrl = 'partials/query.editor.html';
 
@@ -75,6 +83,12 @@ export class TestDataQueryCtrl extends QueryCtrl {
       delete this.target.stream;
     }
 
+    if (this.target.scenarioId === 'predictable_pulse') {
+      this.target.pulseWave = _.defaults(this.target.pulseWave || {}, defaultPulse);
+    } else {
+      delete this.target.pulseWave;
+    }
+
     this.refresh();
   }