Prometheus: Implement stepMode for alerting queries (#36796)

* Add select component for choosing step option

* Add onStepChange

* Add functionality for max step

* Rename minInterval to stepInterval to describe min, max and exact step interval

* Change select option from standard to exact

* Add new type StepType for better type safety

* Add tests for adjustInterval

* Add functionality and tests for exact step option

* Prometheus: Spell out min and max in select component

* Prometheus: Change width of step select component and add placeholder

* Prometheus: Adjust for the factor in exact step

* Prometheus: Update tooltip of step lable to include max and exact options and add padding to select component to give it some breathing room from other components

* Update snapshot for step tooltip

* Prometheus: make tooltip more informative

* Prometheus: add tooltip to interval input element

* Prometheus: extract default step option

* Prometheus: update snapshot for PromQueryEditor

* Prometheus: change step labels to uppercase

* Prometheus: define a default step option

* Prometheus: use default step option in both ui component and logic

* Prometheus: update snapshot for PromQueryEditor

* Prometheus: refactor datasource.ts for better readability

* Prometheus: change tool tip for step

* Prometheus: update snapshots

* Prometheus: add correct styling

* Prometheus: update snapshots

* Prometheus change variable name to something less superfluous

* Prometheus: refactor

* Prometheus: add new test for adjustInterval

* Docs: Update docummentation on the step parameter for prometheus

* Prometheus: make step input field smaller and change placeholder text to 15s

* Prometheus: update snapshots

* Prometheus: Make stepMode uniform in all places in the code

* Adjust step based on stepMode

* Adjust comment

* Check if we have queryInterval

* Refactor, add safe interval

* Fix merge resolutions

* Fix tests and add tests

* Update snapshot

* Update docs/sources/datasources/prometheus.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/datasources/prometheus.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/datasources/prometheus.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/datasources/prometheus.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/datasources/prometheus.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/datasources/prometheus.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/datasources/prometheus.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Implement calculation with intervalMode in calculator.go

* Update tests, add calculate safe interval method

* Replace panic with error

* Update pkg/tsdb/interval/interval_test.go

Co-authored-by: idafurjes <36131195+idafurjes@users.noreply.github.com>

* Update pkg/tsdb/calculator_test.go

Co-authored-by: idafurjes <36131195+idafurjes@users.noreply.github.com>

* Impotrt require

* Remove lint errors

Co-authored-by: Olof Bourghardt <ob655088@gmail.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
Co-authored-by: idafurjes <36131195+idafurjes@users.noreply.github.com>
This commit is contained in:
Ivana Huckova
2021-08-05 09:09:49 +02:00
committed by GitHub
co-authored by achatterjee-grafana idafurjes Olof Bourghardt
parent d49ce5ad47
commit 1083bef030
14 changed files with 279 additions and 51 deletions
+36 -6
View File
@@ -18,6 +18,14 @@ var (
day = time.Hour * 24
)
type IntervalMode string
const (
Min IntervalMode = "min"
Max IntervalMode = "max"
Exact IntervalMode = "exact"
)
type Interval struct {
Text string
Value time.Duration
@@ -28,7 +36,8 @@ type intervalCalculator struct {
}
type Calculator interface {
Calculate(timerange backend.TimeRange, minInterval time.Duration) Interval
Calculate(timerange backend.TimeRange, minInterval time.Duration, intervalMode IntervalMode) (Interval, error)
CalculateSafeInterval(timerange backend.TimeRange, resolution int64) Interval
}
type CalculatorOptions struct {
@@ -53,16 +62,37 @@ func (i *Interval) Milliseconds() int64 {
return i.Value.Nanoseconds() / int64(time.Millisecond)
}
func (ic *intervalCalculator) Calculate(timerange backend.TimeRange, minInterval time.Duration) Interval {
func (ic *intervalCalculator) Calculate(timerange backend.TimeRange, intrvl time.Duration, intervalMode IntervalMode) (Interval, error) {
to := timerange.To.UnixNano()
from := timerange.From.UnixNano()
intrvl := time.Duration((to - from) / defaultRes)
calculatedIntrvl := time.Duration((to - from) / defaultRes)
if intrvl < minInterval {
return Interval{Text: interval.FormatDuration(minInterval), Value: minInterval}
switch intervalMode {
case Min:
if calculatedIntrvl < intrvl {
return Interval{Text: interval.FormatDuration(intrvl), Value: intrvl}, nil
}
case Max:
if calculatedIntrvl > intrvl {
return Interval{Text: interval.FormatDuration(intrvl), Value: intrvl}, nil
}
case Exact:
return Interval{Text: interval.FormatDuration(intrvl), Value: intrvl}, nil
default:
return Interval{}, fmt.Errorf("unrecognized intervalMode: %v", intervalMode)
}
rounded := roundInterval(intrvl)
rounded := roundInterval(calculatedIntrvl)
return Interval{Text: interval.FormatDuration(rounded), Value: rounded}, nil
}
func (ic *intervalCalculator) CalculateSafeInterval(timerange backend.TimeRange, safeRes int64) Interval {
to := timerange.To.UnixNano()
from := timerange.From.UnixNano()
safeInterval := time.Duration((to - from) / safeRes)
rounded := roundInterval(safeInterval)
return Interval{Text: interval.FormatDuration(rounded), Value: rounded}
}
+44 -8
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIntervalCalculator_Calculate(t *testing.T) {
@@ -15,19 +16,54 @@ func TestIntervalCalculator_Calculate(t *testing.T) {
timeNow := time.Now()
testCases := []struct {
name string
timeRange backend.TimeRange
expected string
name string
timeRange backend.TimeRange
intervalMode IntervalMode
expected string
}{
{"from 5m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(5 * time.Minute)}, "200ms"},
{"from 15m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(15 * time.Minute)}, "500ms"},
{"from 30m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(30 * time.Minute)}, "1s"},
{"from 1h to now", backend.TimeRange{From: timeNow, To: timeNow.Add(60 * time.Minute)}, "2s"},
{"from 5m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(5 * time.Minute)}, Min, "200ms"},
{"from 5m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(5 * time.Minute)}, Max, "1ms"},
{"from 5m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(5 * time.Minute)}, Exact, "1ms"},
{"from 15m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(15 * time.Minute)}, Min, "500ms"},
{"from 15m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(15 * time.Minute)}, Max, "1ms"},
{"from 15m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(15 * time.Minute)}, Exact, "1ms"},
{"from 30m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(30 * time.Minute)}, Min, "1s"},
{"from 30m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(30 * time.Minute)}, Max, "1ms"},
{"from 30m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(30 * time.Minute)}, Exact, "1ms"},
{"from 1h to now", backend.TimeRange{From: timeNow, To: timeNow.Add(1440 * time.Minute)}, Min, "1m"},
{"from 1h to now", backend.TimeRange{From: timeNow, To: timeNow.Add(1440 * time.Minute)}, Max, "1ms"},
{"from 1h to now", backend.TimeRange{From: timeNow, To: timeNow.Add(1440 * time.Minute)}, Exact, "1ms"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
interval := calculator.Calculate(tc.timeRange, time.Millisecond*1)
interval, err := calculator.Calculate(tc.timeRange, time.Millisecond*1, tc.intervalMode)
require.Nil(t, err)
assert.Equal(t, tc.expected, interval.Text)
})
}
}
func TestIntervalCalculator_CalculateSafeInterval(t *testing.T) {
calculator := NewCalculator(CalculatorOptions{})
timeNow := time.Now()
testCases := []struct {
name string
timeRange backend.TimeRange
safeResolution int64
expected string
}{
{"from 5m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(5 * time.Minute)}, 11000, "20ms"},
{"from 15m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(15 * time.Minute)}, 11000, "100ms"},
{"from 30m to now", backend.TimeRange{From: timeNow, To: timeNow.Add(30 * time.Minute)}, 11000, "200ms"},
{"from 24h to now", backend.TimeRange{From: timeNow, To: timeNow.Add(1440 * time.Minute)}, 11000, "10s"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
interval := calculator.CalculateSafeInterval(tc.timeRange, tc.safeResolution)
assert.Equal(t, tc.expected, interval.Text)
})
}
@@ -45,7 +45,12 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) run(ctx context.Context, t
return queryResult, cloudMonitoringResponse{}, "", nil
}
intervalCalculator := interval.NewCalculator(interval.CalculatorOptions{})
interval := intervalCalculator.Calculate(*tsdbQuery.TimeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second)
interval, err := intervalCalculator.Calculate(*tsdbQuery.TimeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, "min")
if err != nil {
queryResult.Error = err
return queryResult, cloudMonitoringResponse{}, "", nil
}
timeFormat := "2006/01/02-15:04:05"
timeSeriesQuery.Query += fmt.Sprintf(" | graph_period %s | within d'%s', d'%s'", interval.Text, from.UTC().Format(timeFormat), to.UTC().Format(timeFormat))
+4 -1
View File
@@ -70,7 +70,10 @@ func (e *timeSeriesQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilde
if err != nil {
return err
}
intrvl := e.intervalCalculator.Calculate(e.dataQueries[0].TimeRange, minInterval)
intrvl, err := e.intervalCalculator.Calculate(e.dataQueries[0].TimeRange, minInterval, tsdb.Min)
if err != nil {
return err
}
b := ms.Search(intrvl)
b.Size(0)
+4 -2
View File
@@ -30,8 +30,10 @@ func (query *Query) Build(queryContext *backend.QueryDataRequest) (string, error
}
calculator := tsdb.NewCalculator(tsdb.CalculatorOptions{})
i := calculator.Calculate(queryContext.Queries[0].TimeRange, query.Interval)
i, err := calculator.Calculate(queryContext.Queries[0].TimeRange, query.Interval, tsdb.Min)
if err != nil {
return "", err
}
res = strings.ReplaceAll(res, "$timeFilter", query.renderTimeFilter(queryContext))
res = strings.ReplaceAll(res, "$interval", i.Text)
+28 -6
View File
@@ -29,7 +29,8 @@ type intervalCalculator struct {
}
type Calculator interface {
Calculate(timeRange plugins.DataTimeRange, minInterval time.Duration) Interval
Calculate(timeRange plugins.DataTimeRange, interval time.Duration, intervalMode string) (Interval, error)
CalculateSafeInterval(timeRange plugins.DataTimeRange, resolution int64) Interval
}
type CalculatorOptions struct {
@@ -54,16 +55,37 @@ func (i *Interval) Milliseconds() int64 {
return i.Value.Nanoseconds() / int64(time.Millisecond)
}
func (ic *intervalCalculator) Calculate(timerange plugins.DataTimeRange, minInterval time.Duration) Interval {
func (ic *intervalCalculator) Calculate(timerange plugins.DataTimeRange, interval time.Duration, intervalMode string) (Interval, error) {
to := timerange.MustGetTo().UnixNano()
from := timerange.MustGetFrom().UnixNano()
interval := time.Duration((to - from) / defaultRes)
calculatedInterval := time.Duration((to - from) / defaultRes)
if interval < minInterval {
return Interval{Text: FormatDuration(minInterval), Value: minInterval}
switch intervalMode {
case "min":
if calculatedInterval < interval {
return Interval{Text: FormatDuration(interval), Value: interval}, nil
}
case "max":
if calculatedInterval > interval {
return Interval{Text: FormatDuration(interval), Value: interval}, nil
}
case "exact":
return Interval{Text: FormatDuration(interval), Value: interval}, nil
default:
return Interval{}, fmt.Errorf("unrecognized intervalMode: %v", intervalMode)
}
rounded := roundInterval(interval)
rounded := roundInterval(calculatedInterval)
return Interval{Text: FormatDuration(rounded), Value: rounded}, nil
}
func (ic *intervalCalculator) CalculateSafeInterval(timerange plugins.DataTimeRange, safeRes int64) Interval {
to := timerange.MustGetTo().UnixNano()
from := timerange.MustGetFrom().UnixNano()
safeInterval := time.Duration((to - from) / safeRes)
rounded := roundInterval(safeInterval)
return Interval{Text: FormatDuration(rounded), Value: rounded}
}
+41 -8
View File
@@ -15,19 +15,52 @@ func TestIntervalCalculator_Calculate(t *testing.T) {
calculator := NewCalculator(CalculatorOptions{})
testCases := []struct {
name string
timeRange plugins.DataTimeRange
expected string
name string
timeRange plugins.DataTimeRange
intervalMode string
expected string
}{
{"from 5m to now", plugins.NewDataTimeRange("5m", "now"), "200ms"},
{"from 15m to now", plugins.NewDataTimeRange("15m", "now"), "500ms"},
{"from 30m to now", plugins.NewDataTimeRange("30m", "now"), "1s"},
{"from 1h to now", plugins.NewDataTimeRange("1h", "now"), "2s"},
{"from 5m to now", plugins.NewDataTimeRange("5m", "now"), "min", "200ms"},
{"from 5m to now", plugins.NewDataTimeRange("5m", "now"), "exact", "1ms"},
{"from 5m to now", plugins.NewDataTimeRange("5m", "now"), "max", "1ms"},
{"from 15m to now", plugins.NewDataTimeRange("15m", "now"), "min", "500ms"},
{"from 15m to now", plugins.NewDataTimeRange("15m", "now"), "max", "1ms"},
{"from 15m to now", plugins.NewDataTimeRange("15m", "now"), "exact", "1ms"},
{"from 30m to now", plugins.NewDataTimeRange("30m", "now"), "min", "1s"},
{"from 30m to now", plugins.NewDataTimeRange("30m", "now"), "max", "1ms"},
{"from 30m to now", plugins.NewDataTimeRange("30m", "now"), "exact", "1ms"},
{"from 24h to now", plugins.NewDataTimeRange("24h", "now"), "min", "1m"},
{"from 24h to now", plugins.NewDataTimeRange("24h", "now"), "max", "1ms"},
{"from 24h to now", plugins.NewDataTimeRange("24h", "now"), "exact", "1ms"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
interval := calculator.Calculate(tc.timeRange, time.Millisecond*1)
interval, err := calculator.Calculate(tc.timeRange, time.Millisecond*1, tc.intervalMode)
require.Nil(t, err)
assert.Equal(t, tc.expected, interval.Text)
})
}
}
func TestIntervalCalculator_CalculateSafeInterval(t *testing.T) {
calculator := NewCalculator(CalculatorOptions{})
testCases := []struct {
name string
timeRange plugins.DataTimeRange
safeResolution int64
expected string
}{
{"from 5m to now", plugins.NewDataTimeRange("5m", "now"), 11000, "20ms"},
{"from 15m to now", plugins.NewDataTimeRange("15m", "now"), 11000, "100ms"},
{"from 30m to now", plugins.NewDataTimeRange("30m", "now"), 11000, "200ms"},
{"from 24h to now", plugins.NewDataTimeRange("24h", "now"), 11000, "10s"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
interval := calculator.CalculateSafeInterval(tc.timeRange, tc.safeResolution)
assert.Equal(t, tc.expected, interval.Text)
})
}
+4 -1
View File
@@ -150,7 +150,10 @@ func (e *LokiExecutor) parseQuery(dsInfo *models.DataSource, queryContext plugin
return nil, fmt.Errorf("failed to parse Interval: %v", err)
}
interval := e.intervalCalculator.Calculate(*queryContext.TimeRange, dsInterval)
interval, err := e.intervalCalculator.Calculate(*queryContext.TimeRange, dsInterval, "min")
if err != nil {
return nil, err
}
step := time.Duration(int64(interval.Value))
qs = append(qs, &lokiQuery{
+27 -3
View File
@@ -23,6 +23,7 @@ import (
var (
plog log.Logger
legendFormat *regexp.Regexp = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
safeRes int64 = 11000
)
func init() {
@@ -122,6 +123,9 @@ func formatLegend(metric model.Metric, query *PrometheusQuery) string {
func (e *PrometheusExecutor) parseQuery(dsInfo *models.DataSource, query plugins.DataQuery) (
[]*PrometheusQuery, error) {
var intervalMode string
var adjustedInterval time.Duration
qs := []*PrometheusQuery{}
for _, queryModel := range query.Queries {
expr, err := queryModel.Model.Get("expr").String()
@@ -141,14 +145,34 @@ func (e *PrometheusExecutor) parseQuery(dsInfo *models.DataSource, query plugins
return nil, err
}
dsInterval, err := interval.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15)
hasQueryInterval := queryModel.Model.Get("interval").MustString("") != ""
// Only use stepMode if we have interval in query, otherwise use "min"
if hasQueryInterval {
intervalMode = queryModel.Model.Get("stepMode").MustString("min")
} else {
intervalMode = "min"
}
// Calculate interval value from query or data source settings or use default value
intervalValue, err := interval.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15)
if err != nil {
return nil, err
}
calculatedInterval, err := e.intervalCalculator.Calculate(*query.TimeRange, intervalValue, intervalMode)
if err != nil {
return nil, err
}
safeInterval := e.intervalCalculator.CalculateSafeInterval(*query.TimeRange, safeRes)
if calculatedInterval.Value > safeInterval.Value {
adjustedInterval = calculatedInterval.Value
} else {
adjustedInterval = safeInterval.Value
}
intervalFactor := queryModel.Model.Get("intervalFactor").MustInt64(1)
interval := e.intervalCalculator.Calculate(*query.TimeRange, dsInterval)
step := time.Duration(int64(interval.Value) * intervalFactor)
step := time.Duration(int64(adjustedInterval) * intervalFactor)
qs = append(qs, &PrometheusQuery{
Expr: expr,
+61 -1
View File
@@ -63,7 +63,7 @@ func TestPrometheus(t *testing.T) {
require.Equal(t, `http_request_total{app="backend", device="mobile"}`, formatLegend(metric, query))
})
t.Run("parsing query model with step", func(t *testing.T) {
t.Run("parsing query model with step and default stepMode", func(t *testing.T) {
query := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
@@ -76,6 +76,66 @@ func TestPrometheus(t *testing.T) {
require.Equal(t, time.Second*30, models[0].Step)
})
t.Run("parsing query model with step and exact stepMode", func(t *testing.T) {
query := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"refId": "A",
"stepMode": "exact",
"interval": "7s"
}`)
timerange := plugins.NewDataTimeRange("12h", "now")
query.TimeRange = &timerange
models, err := executor.parseQuery(dsInfo, query)
require.NoError(t, err)
require.Equal(t, time.Second*7, models[0].Step)
})
t.Run("parsing query model with short step and max stepMode", func(t *testing.T) {
query := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"refId": "A",
"stepMode": "max",
"interval": "6s"
}`)
timerange := plugins.NewDataTimeRange("12h", "now")
query.TimeRange = &timerange
models, err := executor.parseQuery(dsInfo, query)
require.NoError(t, err)
require.Equal(t, time.Second*6, models[0].Step)
})
t.Run("parsing query model with long step and max stepMode", func(t *testing.T) {
query := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"refId": "A",
"stepMode": "max",
"interval": "100s"
}`)
timerange := plugins.NewDataTimeRange("12h", "now")
query.TimeRange = &timerange
models, err := executor.parseQuery(dsInfo, query)
require.NoError(t, err)
require.Equal(t, time.Second*30, models[0].Step)
})
t.Run("parsing query model with unsafe interval", func(t *testing.T) {
query := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"refId": "A",
"stepMode": "max",
"interval": "2s"
}`)
timerange := plugins.NewDataTimeRange("12h", "now")
query.TimeRange = &timerange
models, err := executor.parseQuery(dsInfo, query)
require.NoError(t, err)
require.Equal(t, time.Second*5, models[0].Step)
})
t.Run("parsing query model without step parameter", func(t *testing.T) {
query := queryContext(`{
"expr": "go_goroutines",
+4 -1
View File
@@ -348,7 +348,10 @@ var Interpolate = func(query plugins.DataSubQuery, timeRange plugins.DataTimeRan
if err != nil {
return "", err
}
interval := sqlIntervalCalculator.Calculate(timeRange, minInterval)
interval, err := sqlIntervalCalculator.Calculate(timeRange, minInterval, "min")
if err != nil {
return "", err
}
sql = strings.ReplaceAll(sql, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10))
sql = strings.ReplaceAll(sql, "$__interval", interval.Text)