diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go index 2da97514dd8..3405c3bbd1f 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go @@ -30,6 +30,11 @@ type AzureMonitorDatasource struct { dsInfo *models.DataSource } +var ( + // 1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d in milliseconds + allowedIntervalsMS = []int64{60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000} +) + // executeTimeSeriesQuery does the following: // 1. build the AzureMonitor url and querystring for each query // 2. executes each query by calling the Azure Monitor API @@ -49,7 +54,7 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori if err != nil { return nil, err } - azlog.Debug("AzureMonitor", "Response", resp) + // azlog.Debug("AzureMonitor", "Response", resp) err = e.parseResponse(queryRes, resp, query) if err != nil { @@ -93,10 +98,20 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * alias := fmt.Sprintf("%v", azureMonitorTarget["alias"]) + timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"]) + if timeGrain == "auto" { + autoInSeconds := e.findClosestAllowedIntervalMs(query.IntervalMs) / 1000 + tg := &TimeGrain{} + timeGrain, err = tg.createISO8601DurationFromInterval(fmt.Sprintf("%vs", autoInSeconds)) + if err != nil { + return nil, err + } + } + params := url.Values{} params.Add("api-version", "2018-01-01") params.Add("timespan", fmt.Sprintf("%v/%v", startTime.UTC().Format(time.RFC3339), endTime.UTC().Format(time.RFC3339))) - params.Add("interval", fmt.Sprintf("%v", azureMonitorTarget["timeGrain"])) + params.Add("interval", timeGrain) params.Add("aggregation", fmt.Sprintf("%v", azureMonitorTarget["aggregation"])) params.Add("metricnames", fmt.Sprintf("%v", azureMonitorTarget["metricName"])) @@ -269,3 +284,18 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data return nil } + +func (e *AzureMonitorDatasource) findClosestAllowedIntervalMs(intervalMs int64) int64 { + closest := allowedIntervalsMS[0] + + for i, allowed := range allowedIntervalsMS { + if intervalMs > allowed { + if i+1 < len(allowedIntervalsMS) { + closest = allowedIntervalsMS[i+1] + } else { + closest = allowed + } + } + } + return closest +} diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go index 0c95cabb3ec..9aba4eb617b 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go @@ -229,6 +229,26 @@ func TestAzureMonitorDatasource(t *testing.T) { So(res.Series[2].Points[0][0].Float64, ShouldEqual, 0) }) }) + + Convey("Find closest allowed interval for auto time grain", func() { + intervals := map[string]int64{ + "3m": 180000, + "5m": 300000, + "10m": 600000, + "15m": 900000, + "1d": 86400000, + "2d": 172800000, + } + + closest := datasource.findClosestAllowedIntervalMs(intervals["3m"]) + So(closest, ShouldEqual, intervals["5m"]) + + closest = datasource.findClosestAllowedIntervalMs(intervals["10m"]) + So(closest, ShouldEqual, intervals["15m"]) + + closest = datasource.findClosestAllowedIntervalMs(intervals["2d"]) + So(closest, ShouldEqual, intervals["1d"]) + }) }) } diff --git a/pkg/tsdb/azuremonitor/time-grain.go b/pkg/tsdb/azuremonitor/time-grain.go new file mode 100644 index 00000000000..22da2872bab --- /dev/null +++ b/pkg/tsdb/azuremonitor/time-grain.go @@ -0,0 +1,53 @@ +package azuremonitor + +import ( + "fmt" + "strconv" + "strings" +) + +// TimeGrain handles convertions between +// the ISO 8601 Duration format (PT1H), Kbn units (1h) and Time Grains (1 hour) +// Also handles using the automatic Grafana interval to calculate a ISO 8601 Duration. +type TimeGrain struct{} + +var ( + smallTimeUnits = []string{"hour", "minute", "h", "m"} +) + +func (tg *TimeGrain) createISO8601DurationFromInterval(interval string) (string, error) { + if strings.Contains(interval, "ms") { + return "PT1M", nil + } + + timeValueString := interval[0 : len(interval)-1] + timeValue, err := strconv.Atoi(timeValueString) + if err != nil { + return "", fmt.Errorf("Could not parse interval %v to an ISO 8061 duration", interval) + } + + unit := interval[len(interval)-1:] + + if unit == "s" { + toMinutes := (timeValue * 60) % 60 + + // mimumum interval is 1m for Azure Monitor + if toMinutes < 1 { + toMinutes = 1 + } + + return tg.createISO8601Duration(toMinutes, "m"), nil + } + + return tg.createISO8601Duration(timeValue, unit), nil +} + +func (tg *TimeGrain) createISO8601Duration(timeValue int, timeUnit string) string { + for _, smallTimeUnit := range smallTimeUnits { + if timeUnit == smallTimeUnit { + return fmt.Sprintf("PT%v%v", timeValue, strings.ToUpper(timeUnit[0:1])) + } + } + + return fmt.Sprintf("P%v%v", timeValue, strings.ToUpper(timeUnit[0:1])) +} diff --git a/pkg/tsdb/azuremonitor/time-grain_test.go b/pkg/tsdb/azuremonitor/time-grain_test.go new file mode 100644 index 00000000000..be8d0b10a0c --- /dev/null +++ b/pkg/tsdb/azuremonitor/time-grain_test.go @@ -0,0 +1,60 @@ +package azuremonitor + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestTimeGrain(t *testing.T) { + Convey("TimeGrain", t, func() { + tgc := &TimeGrain{} + + Convey("create ISO 8601 Duration", func() { + Convey("when given a time unit smaller than a day", func() { + minuteKbnDuration := tgc.createISO8601Duration(1, "m") + hourKbnDuration := tgc.createISO8601Duration(2, "h") + minuteDuration := tgc.createISO8601Duration(1, "minute") + hourDuration := tgc.createISO8601Duration(2, "hour") + + Convey("should convert it to a time duration", func() { + So(minuteKbnDuration, ShouldEqual, "PT1M") + So(hourKbnDuration, ShouldEqual, "PT2H") + + So(minuteDuration, ShouldEqual, "PT1M") + So(hourDuration, ShouldEqual, "PT2H") + }) + }) + + Convey("when given the day time unit", func() { + kbnDuration := tgc.createISO8601Duration(1, "d") + duration := tgc.createISO8601Duration(2, "day") + + Convey("should convert it to a date duration", func() { + So(kbnDuration, ShouldEqual, "P1D") + So(duration, ShouldEqual, "P2D") + }) + }) + }) + + Convey("create ISO 8601 Duration from Grafana interval", func() { + Convey("and interval is less than a minute", func() { + durationMS, _ := tgc.createISO8601DurationFromInterval("100ms") + durationS, _ := tgc.createISO8601DurationFromInterval("59s") + Convey("should be rounded up to a minute as is the minimum interval for Azure Monitor", func() { + So(durationMS, ShouldEqual, "PT1M") + So(durationS, ShouldEqual, "PT1M") + }) + }) + + Convey("and interval is more than a minute", func() { + durationM, _ := tgc.createISO8601DurationFromInterval("10m") + durationD, _ := tgc.createISO8601DurationFromInterval("2d") + Convey("should be rounded up to a minute as is the minimum interval for Azure Monitor", func() { + So(durationM, ShouldEqual, "PT10M") + So(durationD, ShouldEqual, "P2D") + }) + }) + }) + }) +}