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 03:09:49 -04:00 committed by GitHub
parent d49ce5ad47
commit 1083bef030
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 279 additions and 51 deletions

View File

@ -42,19 +42,19 @@ Open a graph in edit mode by clicking the title > Edit (or by pressing `e` key w
{{< figure src="/static/img/docs/v45/prometheus_query_editor_still.png"
animated-gif="/static/img/docs/v45/prometheus_query_editor.gif" >}}
| Name | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Query expression` | Prometheus query expression, check out the [Prometheus documentation](http://prometheus.io/docs/querying/basics/). |
| `Legend format` | Controls the name of the time series, using name or pattern. For example `{{hostname}}` is replaced with the label value for the label `hostname`. |
| `Step` | Use 'Minimum' or 'Maximum' step mode to set the lower or upper bounds respectively on the interval between data points. For example, set "minimum 1h" to hint that measurements were not taken more frequently. Use the 'Exact' step mode to set an exact interval between data points. `$__interval` and `$__rate_interval` are supported. |
| Name | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Query expression` | Prometheus query expression. For more information, refer to the [Prometheus documentation](http://prometheus.io/docs/querying/basics/). |
| `Legend format` | Controls the name of the time series, using name or pattern. For example, `{{hostname}}` is replaced by the label value for the label `hostname`. |
| `Step` | Use 'Minimum' or 'Maximum' step mode to set the lower or upper bounds respectively on the interval between data points. For example, set "minimum 1h" to hint that measurements are not frequent (taken hourly). Use the 'Exact' step mode to set a precise interval between data points. `$__interval` and `$__rate_interval` are supported. |
| `Resolution` | `1/1` sets both the `$__interval` variable and the [`step` parameter of Prometheus range queries](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) such that each pixel corresponds to one data point. For better performance, lower resolutions can be picked. `1/2` only retrieves a data point for every other pixel, and `1/10` retrieves one data point per 10 pixels. Note that both _Min time interval_ and _Step_ limit the final value of `$__interval` and `step`. |
| `Resolution` | `1/1` sets both the `$__interval` variable and the [`step` parameter of Prometheus range queries](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) such that each pixel corresponds to one data point. For better performance, you can pick lower resolutions. `1/2` only retrieves a data point for every other pixel, and `1/10` retrieves one data point per 10 pixels. Both _Min time interval_ and _Step_ limit the final value of `$__interval` and `step`. |
| `Metric lookup` | Search for metric names in this input field. |
| `Format as` | Switch between `Table`, `Time series`, or `Heatmap`. `Table` will only work in the Table panel. `Heatmap` is suitable for displaying metrics of the Histogram type on a Heatmap panel. Under the hood, it converts cumulative histograms to regular ones and sorts series by the bucket bound. |
| `Instant` | Perform an "instant" query, to return only the latest value that Prometheus has scraped for the requested time series. Instant queries return results much faster than normal range queries. Use them to look up label sets. |
| `Min time interval` | This value multiplied by the denominator from the _Resolution_ setting sets a lower limit to both the `$__interval` variable and the [`step` parameter of Prometheus range queries](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries). Defaults to _Scrape interval_ as set in the data source options. |
| `Exemplars` | Run and show exemplars in the graph. |
| `Metric lookup` | Search for metric names in this input field. |
| `Format as` | You can switch between `Table` `Time series` or `Heatmap` options. The `Table` option works only in the Table panel. `Heatmap` displays metrics of the Histogram type on a Heatmap panel. Under the hood, it converts cumulative histograms to regular ones and sorts series by the bucket bound. |
| `Instant` | Perform an "instant" query to return only the latest value that Prometheus has scraped for the requested time series. Instant queries can return results much faster than normal range queries. Use them to look up label sets. |
| `Min time interval` | This value multiplied by the denominator from the _Resolution_ setting sets a lower limit to both the `$__interval` variable and the [`step` parameter of Prometheus range queries](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries). Defaults to _Scrape interval_ as specified in the data source options. |
| `Exemplars` | Run and show exemplars in the graph. |
> **Note:** Grafana modifies the request dates for queries to align them with the dynamically calculated step. This ensures consistent display of metrics data, but it can result in a small gap of data at the right edge of a graph.
@ -160,7 +160,7 @@ OK: rate(http_requests_total[5m])
Better: rate(http_requests_total[$__rate_interval])
```
Details: `$__rate_interval` is defined as max(`$__interval` + _Scrape interval_, 4 \* _Scrape interval_), where _Scrape interval_ is the Min step setting (AKA query_interval, a setting per PromQL query) if any is set. Otherwise, the Scrape interval setting in the Prometheus data source is used. (The Min interval setting in the panel is modified by the resolution setting and therefore doesn't have any effect on _Scrape interval_.) [This article](https://grafana.com/blog/2020/09/28/new-in-grafana-7.2-__rate_interval-for-prometheus-rate-queries-that-just-work/) contains additional details.
Details: `$__rate_interval` is defined as max(`$__interval` + _Scrape interval_, 4 \* _Scrape interval_), where _Scrape interval_ is the Min step setting (AKA query*interval, a setting per PromQL query) if any is set. Otherwise, the Scrape interval setting in the Prometheus data source is used. (The Min interval setting in the panel is modified by the resolution setting and therefore doesn't have any effect on \_Scrape interval*.) [This article](https://grafana.com/blog/2020/09/28/new-in-grafana-7.2-__rate_interval-for-prometheus-rate-queries-that-just-work/) contains additional details.
### Using variables in queries

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

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

View File

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

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)

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)

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

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

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{

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,

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",

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)

View File

@ -58,7 +58,13 @@ export class PromQueryEditor extends PureComponent<PromQueryEditorProps, State>
constructor(props: PromQueryEditorProps) {
super(props);
// Use default query to prevent undefined input values
const defaultQuery: Partial<PromQuery> = { expr: '', legendFormat: '', interval: '', exemplar: true };
const defaultQuery: Partial<PromQuery> = {
expr: '',
legendFormat: '',
interval: '',
exemplar: true,
stepMode: DEFAULT_STEP_MODE.value,
};
const query = Object.assign({}, defaultQuery, props.query);
this.query = query;
// Query target properties that are fully controlled inputs

View File

@ -192,6 +192,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
"interval": "",
"legendFormat": "",
"refId": "A",
"stepMode": "min",
}
}
/>