mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
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:
parent
d49ce5ad47
commit
1083bef030
@ -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
|
||||
|
||||
|
@ -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}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -192,6 +192,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A",
|
||||
"stepMode": "min",
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user