Files
ismail simsek 7ae2eed876 Prometheus: Make sure "Min Step" has precedence (#115941)
* set minStep value as final step value when set explicitly.

* enhance it with tests

* improve function readability

* a bit more improvement for readability
2026-01-15 00:50:10 +01:00

603 lines
15 KiB
Go

package models
import (
"context"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"github.com/grafana/grafana/pkg/promlib/intervalv2"
)
var (
testNow = time.Now()
testIntervalCalculator = intervalv2.NewCalculator()
testTracer = otel.Tracer("test/interval")
)
func TestCalculatePrometheusInterval(t *testing.T) {
_, span := testTracer.Start(context.Background(), "test")
defer span.End()
tests := []struct {
name string
queryInterval string
dsScrapeInterval string
intervalMs int64
intervalFactor int64
query backend.DataQuery
want time.Duration
wantErr bool
}{
{
name: "min step 2m with 300000 intervalMs",
queryInterval: "2m",
dsScrapeInterval: "",
intervalMs: 300000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 5 * time.Minute,
MaxDataPoints: 761,
},
want: 2 * time.Minute,
wantErr: false,
},
{
name: "min step 2m with 900000 intervalMs",
queryInterval: "2m",
dsScrapeInterval: "",
intervalMs: 900000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 15 * time.Minute,
MaxDataPoints: 175,
},
want: 2 * time.Minute,
wantErr: false,
},
{
name: "with step parameter",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(12 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 30 * time.Second,
wantErr: false,
},
{
name: "without step parameter",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with high intervalFactor",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 10,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 20 * time.Minute,
wantErr: false,
},
{
name: "with low intervalFactor",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 2 * time.Minute,
wantErr: false,
},
{
name: "with specified scrape-interval in data source",
queryInterval: "",
dsScrapeInterval: "240s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 4 * time.Minute,
wantErr: false,
},
{
name: "with zero intervalFactor defaults to 1",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 0,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with $__interval variable",
queryInterval: "$__interval",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with ${__interval} variable",
queryInterval: "${__interval}",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with ${__interval} variable and explicit interval",
queryInterval: "1m",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 1 * time.Minute,
wantErr: false,
},
{
name: "with $__rate_interval variable",
queryInterval: "$__rate_interval",
dsScrapeInterval: "30s",
intervalMs: 100000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 100 * time.Second,
MaxDataPoints: 12384,
},
want: 130 * time.Second,
wantErr: false,
},
{
name: "with ${__rate_interval} variable",
queryInterval: "${__rate_interval}",
dsScrapeInterval: "30s",
intervalMs: 100000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 100 * time.Second,
MaxDataPoints: 12384,
},
want: 130 * time.Second,
wantErr: false,
},
{
name: "intervalMs 100s, minStep override 150s and scrape interval 30s",
queryInterval: "150s",
dsScrapeInterval: "30s",
intervalMs: 100000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 100 * time.Second,
MaxDataPoints: 12384,
},
want: 150 * time.Second,
wantErr: false,
},
{
name: "intervalMs 120s, minStep override 150s and ds scrape interval 30s",
queryInterval: "150s",
dsScrapeInterval: "30s",
intervalMs: 120000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 120 * time.Second,
MaxDataPoints: 12384,
},
want: 150 * time.Second,
wantErr: false,
},
{
name: "intervalMs 120s, minStep auto (interval not overridden) and ds scrape interval 30s",
queryInterval: "120s",
dsScrapeInterval: "30s",
intervalMs: 120000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 120 * time.Second,
MaxDataPoints: 12384,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "interval and minStep are automatically calculated and ds scrape interval 30s and time range 1 hour",
queryInterval: "30s",
dsScrapeInterval: "30s",
intervalMs: 30000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 30 * time.Second,
MaxDataPoints: 12384,
},
want: 30 * time.Second,
wantErr: false,
},
{
name: "minStep is $__rate_interval and ds scrape interval 30s and time range 1 hour",
queryInterval: "$__rate_interval",
dsScrapeInterval: "30s",
intervalMs: 30000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 30 * time.Second,
MaxDataPoints: 12384,
},
want: 2 * time.Minute,
wantErr: false,
},
{
name: "minStep is $__rate_interval and ds scrape interval 30s and time range 2 days",
queryInterval: "$__rate_interval",
dsScrapeInterval: "30s",
intervalMs: 120000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 120 * time.Second,
MaxDataPoints: 12384,
},
want: 150 * time.Second,
wantErr: false,
},
{
name: "minStep is $__interval and ds scrape interval 15s and time range 2 days",
queryInterval: "$__interval",
dsScrapeInterval: "15s",
intervalMs: 120000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 120 * time.Second,
MaxDataPoints: 12384,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with empty dsScrapeInterval defaults to 15s",
queryInterval: "",
dsScrapeInterval: "",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with very short time range",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Minute),
},
Interval: 1 * time.Minute,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with very long time range",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(30 * 24 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 30 * time.Minute,
wantErr: false,
},
{
name: "with manual interval override",
queryInterval: "5m",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 5 * time.Minute,
wantErr: false,
},
{
name: "minStep is auto and ds scrape interval 30s and time range 1 hour",
queryInterval: "",
dsScrapeInterval: "30s",
intervalMs: 30000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 30 * time.Second,
MaxDataPoints: 1613,
},
want: 30 * time.Second,
wantErr: false,
},
{
name: "minStep is auto and ds scrape interval 15s and time range 5 minutes",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 15000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(5 * time.Minute),
},
Interval: 15 * time.Second,
MaxDataPoints: 1055,
},
want: 15 * time.Second,
wantErr: false,
},
// Additional test cases for better coverage
{
name: "with $__interval_ms variable",
queryInterval: "$__interval_ms",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with ${__interval_ms} variable",
queryInterval: "${__interval_ms}",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with MaxDataPoints zero",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
MaxDataPoints: 0,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with negative intervalFactor",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: -5,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: -10 * time.Minute,
wantErr: false,
},
{
name: "with invalid interval string that fails parsing",
queryInterval: "invalid-interval",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: time.Duration(0),
wantErr: true,
},
{
name: "with very small MaxDataPoints",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
MaxDataPoints: 10,
},
want: 5 * time.Minute,
wantErr: false,
},
{
name: "when safeInterval is larger than calculatedInterval",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
MaxDataPoints: 10000,
},
want: 15 * time.Second,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := calculatePrometheusInterval(
tt.queryInterval,
tt.dsScrapeInterval,
tt.intervalMs,
tt.intervalFactor,
tt.query,
testIntervalCalculator,
)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, got)
})
}
}