diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go index abbf6fd69b5..68c5fd5b7ae 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go @@ -32,7 +32,7 @@ type AzureMonitorDatasource struct { var ( // 1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d in milliseconds - allowedIntervalsMS = []int64{60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000} + defaultAllowedIntervalsMS = []int64{60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000} ) // executeTimeSeriesQuery does the following: @@ -99,13 +99,15 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * } azureURL := ub.Build() - alias := fmt.Sprintf("%v", azureMonitorTarget["alias"]) + alias := "" + if val, ok := azureMonitorTarget["alias"]; ok { + alias = fmt.Sprintf("%v", val) + } timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"]) + timeGrains := azureMonitorTarget["allowedTimeGrainsMs"] if timeGrain == "auto" { - autoInterval := e.findClosestAllowedIntervalMS(query.IntervalMs) - tg := &TimeGrain{} - timeGrain, err = tg.createISO8601DurationFromIntervalMS(autoInterval) + timeGrain, err = e.setAutoTimeGrain(query.IntervalMs, timeGrains) if err != nil { return nil, err } @@ -120,7 +122,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimension"])) dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimensionFilter"])) - if azureMonitorTarget["dimension"] != nil && azureMonitorTarget["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 { + if azureMonitorTarget["dimension"] != nil && azureMonitorTarget["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 && dimension != "None" { params.Add("$filter", fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter)) } @@ -143,6 +145,35 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * return azureMonitorQueries, nil } +// setAutoTimeGrain tries to find the closest interval to the query's intervalMs value +// if the metric has a limited set of possible intervals/time grains then use those +// instead of the default list of intervals +func (e *AzureMonitorDatasource) setAutoTimeGrain(intervalMs int64, timeGrains interface{}) (string, error) { + // parses array of numbers from the timeGrains json field + allowedTimeGrains := []int64{} + tgs, ok := timeGrains.([]interface{}) + if ok { + for _, v := range tgs { + jsonNumber, ok := v.(json.Number) + if ok { + tg, err := jsonNumber.Int64() + if err == nil { + allowedTimeGrains = append(allowedTimeGrains, tg) + } + } + } + } + + autoInterval := e.findClosestAllowedIntervalMS(intervalMs, allowedTimeGrains) + tg := &TimeGrain{} + autoTimeGrain, err := tg.createISO8601DurationFromIntervalMS(autoInterval) + if err != nil { + return "", err + } + + return autoTimeGrain, nil +} + func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, queries []*tsdb.Query, timeRange *tsdb.TimeRange) (*tsdb.QueryResult, AzureMonitorResponse, error) { queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: query.RefID} @@ -257,7 +288,7 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data metadataName = series.Metadatavalues[0].Name.LocalizedValue metadataValue = series.Metadatavalues[0].Value } - defaultMetricName := formatLegendKey(query.UrlComponents["resourceName"], data.Value[0].Name.LocalizedValue, metadataName, metadataValue) + metricName := formatLegendKey(query.Alias, query.UrlComponents["resourceName"], data.Value[0].Name.LocalizedValue, metadataName, metadataValue, data.Namespace, data.Value[0].ID) for _, point := range series.Data { var value float64 @@ -279,10 +310,11 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data } queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ - Name: defaultMetricName, + Name: metricName, Points: points, }) } + queryRes.Meta.Set("unit", data.Value[0].Unit) return nil } @@ -290,13 +322,21 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data // findClosestAllowedIntervalMs is used for the auto time grain setting. // It finds the closest time grain from the list of allowed time grains for Azure Monitor // using the Grafana interval in milliseconds -func (e *AzureMonitorDatasource) findClosestAllowedIntervalMS(intervalMs int64) int64 { - closest := allowedIntervalsMS[0] +// Some metrics only allow a limited list of time grains. The allowedTimeGrains parameter +// allows overriding the default list of allowed time grains. +func (e *AzureMonitorDatasource) findClosestAllowedIntervalMS(intervalMs int64, allowedTimeGrains []int64) int64 { + allowedIntervals := defaultAllowedIntervalsMS - for i, allowed := range allowedIntervalsMS { + if len(allowedTimeGrains) > 0 { + allowedIntervals = allowedTimeGrains + } + + closest := allowedIntervals[0] + + for i, allowed := range allowedIntervals { if intervalMs > allowed { - if i+1 < len(allowedIntervalsMS) { - closest = allowedIntervalsMS[i+1] + if i+1 < len(allowedIntervals) { + closest = allowedIntervals[i+1] } else { closest = allowed } @@ -306,9 +346,50 @@ func (e *AzureMonitorDatasource) findClosestAllowedIntervalMS(intervalMs int64) } // formatLegendKey builds the legend key or timeseries name -func formatLegendKey(resourceName string, metricName string, metadataName string, metadataValue string) string { - if len(metadataName) > 0 { - return fmt.Sprintf("%s{%s=%s}.%s", resourceName, metadataName, metadataValue, metricName) +// Alias patterns like {{resourcename}} are replaced with the appropriate data values. +func formatLegendKey(alias string, resourceName string, metricName string, metadataName string, metadataValue string, namespace string, seriesID string) string { + if alias == "" { + if len(metadataName) > 0 { + return fmt.Sprintf("%s{%s=%s}.%s", resourceName, metadataName, metadataValue, metricName) + } + return fmt.Sprintf("%s.%s", resourceName, metricName) } - return fmt.Sprintf("%s.%s", resourceName, metricName) + + startIndex := strings.Index(seriesID, "/resourceGroups/") + 16 + endIndex := strings.Index(seriesID, "/providers") + resourceGroup := seriesID[startIndex:endIndex] + + result := legendKeyFormat.ReplaceAllFunc([]byte(alias), func(in []byte) []byte { + metaPartName := strings.Replace(string(in), "{{", "", 1) + metaPartName = strings.Replace(metaPartName, "}}", "", 1) + metaPartName = strings.ToLower(strings.TrimSpace(metaPartName)) + + if metaPartName == "resourcegroup" { + return []byte(resourceGroup) + } + + if metaPartName == "namespace" { + return []byte(namespace) + } + + if metaPartName == "resourcename" { + return []byte(resourceName) + } + + if metaPartName == "metric" { + return []byte(metricName) + } + + if metaPartName == "dimensionname" { + return []byte(metadataName) + } + + if metaPartName == "dimensionvalue" { + return []byte(metadataValue) + } + + return in + }) + + return string(result) } diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go index 94c2aef6c03..51f7b3f3f86 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go @@ -67,6 +67,49 @@ func TestAzureMonitorDatasource(t *testing.T) { So(queries[0].Alias, ShouldEqual, "testalias") }) + Convey("and has a time grain set to auto", func() { + tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ + "azureMonitor": map[string]interface{}{ + "timeGrain": "auto", + "aggregation": "Average", + "resourceGroup": "grafanastaging", + "resourceName": "grafana", + "metricDefinition": "Microsoft.Compute/virtualMachines", + "metricName": "Percentage CPU", + "alias": "testalias", + "queryType": "Azure Monitor", + }, + }) + tsdbQuery.Queries[0].IntervalMs = 400000 + + queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange) + So(err, ShouldBeNil) + + So(queries[0].Params["interval"][0], ShouldEqual, "PT15M") + }) + + Convey("and has a time grain set to auto and the metric has a limited list of allowed time grains", func() { + tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ + "azureMonitor": map[string]interface{}{ + "timeGrain": "auto", + "aggregation": "Average", + "resourceGroup": "grafanastaging", + "resourceName": "grafana", + "metricDefinition": "Microsoft.Compute/virtualMachines", + "metricName": "Percentage CPU", + "alias": "testalias", + "queryType": "Azure Monitor", + "allowedTimeGrainsMs": []interface{}{"auto", json.Number("60000"), json.Number("300000")}, + }, + }) + tsdbQuery.Queries[0].IntervalMs = 400000 + + queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange) + So(err, ShouldBeNil) + + So(queries[0].Params["interval"][0], ShouldEqual, "PT5M") + }) + Convey("and has a dimension filter", func() { tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ "azureMonitor": map[string]interface{}{ @@ -89,6 +132,29 @@ func TestAzureMonitorDatasource(t *testing.T) { So(queries[0].Target, ShouldEqual, "%24filter=blob+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z") }) + + Convey("and has a dimension filter set to None", func() { + tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{ + "azureMonitor": map[string]interface{}{ + "timeGrain": "PT1M", + "aggregation": "Average", + "resourceGroup": "grafanastaging", + "resourceName": "grafana", + "metricDefinition": "Microsoft.Compute/virtualMachines", + "metricName": "Percentage CPU", + "alias": "testalias", + "queryType": "Azure Monitor", + "dimension": "None", + "dimensionFilter": "*", + }, + }) + + queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange) + So(err, ShouldBeNil) + + So(queries[0].Target, ShouldEqual, "aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z") + + }) }) Convey("Parse AzureMonitor API response in the time series format", func() { @@ -235,6 +301,48 @@ func TestAzureMonitorDatasource(t *testing.T) { So(res.Series[2].Name, ShouldEqual, "grafana{blobtype=Azure Data Lake Storage}.Blob Count") So(res.Series[2].Points[0][0].Float64, ShouldEqual, 0) }) + + Convey("when data from query has alias patterns", func() { + data, err := loadTestFile("./test-data/2-azure-monitor-response-total.json") + So(err, ShouldBeNil) + + res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} + query := &AzureMonitorQuery{ + Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}", + UrlComponents: map[string]string{ + "resourceName": "grafana", + }, + Params: url.Values{ + "aggregation": {"Total"}, + }, + } + err = datasource.parseResponse(res, data, query) + So(err, ShouldBeNil) + + So(res.Series[0].Name, ShouldEqual, "custom grafanastaging Microsoft.Compute/virtualMachines grafana Percentage CPU") + }) + + Convey("when data has dimension filters and alias patterns", func() { + data, err := loadTestFile("./test-data/6-azure-monitor-response-multi-dimension.json") + So(err, ShouldBeNil) + + res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} + query := &AzureMonitorQuery{ + Alias: "{{dimensionname}}={{DimensionValue}}", + UrlComponents: map[string]string{ + "resourceName": "grafana", + }, + Params: url.Values{ + "aggregation": {"Average"}, + }, + } + err = datasource.parseResponse(res, data, query) + So(err, ShouldBeNil) + + So(res.Series[0].Name, ShouldEqual, "blobtype=PageBlob") + So(res.Series[1].Name, ShouldEqual, "blobtype=BlockBlob") + So(res.Series[2].Name, ShouldEqual, "blobtype=Azure Data Lake Storage") + }) }) Convey("Find closest allowed interval for auto time grain", func() { @@ -247,13 +355,16 @@ func TestAzureMonitorDatasource(t *testing.T) { "2d": 172800000, } - closest := datasource.findClosestAllowedIntervalMS(intervals["3m"]) + closest := datasource.findClosestAllowedIntervalMS(intervals["3m"], []int64{}) So(closest, ShouldEqual, intervals["5m"]) - closest = datasource.findClosestAllowedIntervalMS(intervals["10m"]) + closest = datasource.findClosestAllowedIntervalMS(intervals["10m"], []int64{}) So(closest, ShouldEqual, intervals["15m"]) - closest = datasource.findClosestAllowedIntervalMS(intervals["2d"]) + closest = datasource.findClosestAllowedIntervalMS(intervals["2d"], []int64{}) + So(closest, ShouldEqual, intervals["1d"]) + + closest = datasource.findClosestAllowedIntervalMS(intervals["3m"], []int64{intervals["1d"]}) So(closest, ShouldEqual, intervals["1d"]) }) }) diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index 39014bf38da..67444806d39 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "regexp" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -11,7 +12,8 @@ import ( ) var ( - azlog log.Logger + azlog log.Logger + legendKeyFormat *regexp.Regexp ) // AzureMonitorExecutor executes queries for the Azure Monitor datasource - all four services @@ -36,6 +38,7 @@ func NewAzureMonitorExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, func init() { azlog = log.New("tsdb.azuremonitor") tsdb.RegisterTsdbQueryEndpoint("grafana-azure-monitor-datasource", NewAzureMonitorExecutor) + legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) } // Query takes in the frontend queries, parses them into the query format diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index e4e718755b7..64eb059c68d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -93,216 +93,48 @@ describe('AzureMonitorDatasource', () => { metricDefinition: 'Microsoft.Compute/virtualMachines', metricName: 'Percentage CPU', timeGrain: 'PT1H', - alias: '', + alias: '{{metric}}', }, }, ], }; - describe('and data field is average', () => { - const response = { - value: [ - { - timeseries: [ - { - data: [ - { - timeStamp: '2017-08-22T21:00:00Z', - average: 1.0503333333333331, - }, - { - timeStamp: '2017-08-22T22:00:00Z', - average: 1.045083333333333, - }, - { - timeStamp: '2017-08-22T23:00:00Z', - average: 1.0457499999999995, - }, - ], - }, - ], - id: - '/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' + - '/testRN/providers/Microsoft.Insights/metrics/Percentage CPU', - name: { - value: 'Percentage CPU', - localizedValue: 'Percentage CPU', - }, - type: 'Microsoft.Insights/metrics', + const response = { + results: { + A: { + refId: 'A', + meta: { + rawQuery: + 'aggregation=Average&api-version=2018-01-01&interval=PT1M' + + '&metricnames=Percentage+CPU×pan=2019-05-19T15%3A11%3A37Z%2F2019-05-19T21%3A11%3A37Z', unit: 'Percent', }, - ], + series: [ + { + name: 'Percentage CPU', + points: [[2.2075, 1558278660000], [2.29, 1558278720000]], + }, + ], + tables: null, + }, + }, + }; + + beforeEach(() => { + ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + expect(options.url).toContain('/api/tsdb/query'); + return ctx.$q.when({ data: response, status: 200 }); }; - - beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { - expect(options.url).toContain( - '/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics' - ); - return ctx.$q.when({ data: response, status: 200 }); - }; - }); - - it('should return a list of datapoints', () => { - return ctx.ds.query(options).then(results => { - expect(results.data.length).toBe(1); - expect(results.data[0].target).toEqual('testRN.Percentage CPU'); - expect(results.data[0].datapoints[0][1]).toEqual(1503435600000); - expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331); - expect(results.data[0].datapoints[2][1]).toEqual(1503442800000); - expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995); - }); - }); }); - describe('and data field is total', () => { - const response = { - value: [ - { - timeseries: [ - { - data: [ - { - timeStamp: '2017-08-22T21:00:00Z', - total: 1.0503333333333331, - }, - { - timeStamp: '2017-08-22T22:00:00Z', - total: 1.045083333333333, - }, - { - timeStamp: '2017-08-22T23:00:00Z', - total: 1.0457499999999995, - }, - ], - }, - ], - id: - '/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' + - '/testRN/providers/Microsoft.Insights/metrics/Percentage CPU', - name: { - value: 'Percentage CPU', - localizedValue: 'Percentage CPU', - }, - type: 'Microsoft.Insights/metrics', - unit: 'Percent', - }, - ], - }; - - beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { - expect(options.url).toContain( - '/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics' - ); - return ctx.$q.when({ data: response, status: 200 }); - }; - }); - - it('should return a list of datapoints', () => { - return ctx.ds.query(options).then(results => { - expect(results.data.length).toBe(1); - expect(results.data[0].target).toEqual('testRN.Percentage CPU'); - expect(results.data[0].datapoints[0][1]).toEqual(1503435600000); - expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331); - expect(results.data[0].datapoints[2][1]).toEqual(1503442800000); - expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995); - }); - }); - }); - - describe('and data has a dimension filter', () => { - const response = { - value: [ - { - timeseries: [ - { - data: [ - { - timeStamp: '2017-08-22T21:00:00Z', - total: 1.0503333333333331, - }, - { - timeStamp: '2017-08-22T22:00:00Z', - total: 1.045083333333333, - }, - { - timeStamp: '2017-08-22T23:00:00Z', - total: 1.0457499999999995, - }, - ], - metadatavalues: [ - { - name: { - value: 'blobtype', - localizedValue: 'blobtype', - }, - value: 'BlockBlob', - }, - ], - }, - ], - id: - '/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' + - '/testRN/providers/Microsoft.Insights/metrics/Percentage CPU', - name: { - value: 'Percentage CPU', - localizedValue: 'Percentage CPU', - }, - type: 'Microsoft.Insights/metrics', - unit: 'Percent', - }, - ], - }; - - describe('and with no alias specified', () => { - beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { - const expected = - '/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics'; - expect(options.url).toContain(expected); - return ctx.$q.when({ data: response, status: 200 }); - }; - }); - - it('should return a list of datapoints', () => { - return ctx.ds.query(options).then(results => { - expect(results.data.length).toBe(1); - expect(results.data[0].target).toEqual('testRN{blobtype=BlockBlob}.Percentage CPU'); - expect(results.data[0].datapoints[0][1]).toEqual(1503435600000); - expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331); - expect(results.data[0].datapoints[2][1]).toEqual(1503442800000); - expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995); - }); - }); - }); - - describe('and with an alias specified', () => { - beforeEach(() => { - options.targets[0].azureMonitor.alias = - '{{resourcegroup}} + {{namespace}} + {{resourcename}} + ' + - '{{metric}} + {{dimensionname}} + {{dimensionvalue}}'; - - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { - const expected = - '/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics'; - expect(options.url).toContain(expected); - return ctx.$q.when({ data: response, status: 200 }); - }; - }); - - it('should return a list of datapoints', () => { - return ctx.ds.query(options).then(results => { - expect(results.data.length).toBe(1); - const expected = - 'testRG + Microsoft.Compute/virtualMachines + testRN + Percentage CPU + blobtype + BlockBlob'; - expect(results.data[0].target).toEqual(expected); - expect(results.data[0].datapoints[0][1]).toEqual(1503435600000); - expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331); - expect(results.data[0].datapoints[2][1]).toEqual(1503442800000); - expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995); - }); - }); + it('should return a list of datapoints', () => { + return ctx.ds.query(options).then(results => { + expect(results.data.length).toBe(1); + expect(results.data[0].name).toEqual('Percentage CPU'); + expect(results.data[0].rows[0][1]).toEqual(1558278660000); + expect(results.data[0].rows[0][0]).toEqual(2.2075); + expect(results.data[0].rows[1][1]).toEqual(1558278720000); + expect(results.data[0].rows[1][0]).toEqual(2.29); }); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index f7e4b5f1171..3684802729b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -1,11 +1,21 @@ import _ from 'lodash'; -import AzureMonitorFilterBuilder from './azure_monitor_filter_builder'; import UrlBuilder from './url_builder'; import ResponseParser from './response_parser'; import SupportedNamespaces from './supported_namespaces'; import TimegrainConverter from '../time_grain_converter'; -import { AzureMonitorQuery, AzureDataSourceJsonData } from '../types'; -import { DataQueryRequest, DataSourceInstanceSettings } from '@grafana/ui/src/types'; +import { + AzureMonitorQuery, + AzureDataSourceJsonData, + AzureMonitorMetricDefinitionsResponse, + AzureMonitorResourceGroupsResponse, +} from '../types'; +import { + DataQueryRequest, + DataQueryResponseData, + DataSourceInstanceSettings, + TimeSeries, + toDataFrame, +} from '@grafana/ui'; import { BackendSrv } from 'app/core/services/backend_srv'; import { TemplateSrv } from 'app/features/templating/template_srv'; @@ -19,7 +29,7 @@ export default class AzureMonitorDatasource { url: string; defaultDropdownValue = 'select'; cloudName: string; - supportedMetricNamespaces: any[] = []; + supportedMetricNamespaces: string[] = []; /** @ngInject */ constructor( @@ -40,7 +50,7 @@ export default class AzureMonitorDatasource { return !!this.subscriptionId && this.subscriptionId.length > 0; } - async query(options: DataQueryRequest) { + async query(options: DataQueryRequest): Promise { const queries = _.filter(options.targets, item => { return ( item.hide !== true && @@ -56,6 +66,7 @@ export default class AzureMonitorDatasource { }).map(target => { const item = target.azureMonitor; + // fix for timeGrainUnit which is a deprecated/removed field name if (item.timeGrainUnit && item.timeGrain !== 'auto') { item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrain, item.timeGrainUnit); } @@ -65,78 +76,66 @@ export default class AzureMonitorDatasource { const resourceName = this.templateSrv.replace(item.resourceName, options.scopedVars); const metricDefinition = this.templateSrv.replace(item.metricDefinition, options.scopedVars); const timeGrain = this.templateSrv.replace((item.timeGrain || '').toString(), options.scopedVars); - - const filterBuilder = new AzureMonitorFilterBuilder( - item.metricName, - options.range.from, - options.range.to, - timeGrain, - options.interval - ); - - if (item.timeGrains) { - filterBuilder.setAllowedTimeGrains(item.timeGrains); - } - - if (item.aggregation) { - filterBuilder.setAggregation(item.aggregation); - } - - if (item.dimension && item.dimension !== 'None') { - filterBuilder.setDimensionFilter(item.dimension, item.dimensionFilter); - } - - const filter = this.templateSrv.replace(filterBuilder.generateFilter(), options.scopedVars); - - const url = UrlBuilder.buildAzureMonitorQueryUrl( - this.baseUrl, - subscriptionId, - resourceGroup, - metricDefinition, - resourceName, - this.apiVersion, - filter - ); + const aggregation = this.templateSrv.replace(item.aggregation, options.scopedVars); return { refId: target.refId, intervalMs: options.intervalMs, - maxDataPoints: options.maxDataPoints, datasourceId: this.id, - url: url, - format: target.format, - alias: item.alias, + subscription: subscriptionId, + queryType: 'Azure Monitor', + type: 'timeSeriesQuery', raw: false, + azureMonitor: { + resourceGroup: resourceGroup, + resourceName: resourceName, + metricDefinition: metricDefinition, + timeGrain: timeGrain, + allowedTimeGrainsMs: item.allowedTimeGrainsMs, + metricName: this.templateSrv.replace(item.metricName, options.scopedVars), + aggregation: aggregation, + dimension: this.templateSrv.replace(item.dimension, options.scopedVars), + dimensionFilter: this.templateSrv.replace(item.dimensionFilter, options.scopedVars), + alias: item.alias, + format: target.format, + }, }; }); if (!queries || queries.length === 0) { - return []; + return Promise.resolve([]); } - const promises = this.doQueries(queries); - - return Promise.all(promises).then(results => { - return new ResponseParser(results).parseQueryResult(); + const { data } = await this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + from: options.range.from.valueOf().toString(), + to: options.range.to.valueOf().toString(), + queries, + }, }); - } - doQueries(queries) { - return _.map(queries, query => { - return this.doRequest(query.url) - .then(result => { - return { - result: result, - query: query, - }; - }) - .catch(err => { - throw { - error: err, - query: query, + const result: DataQueryResponseData[] = []; + if (data.results) { + Object['values'](data.results).forEach((queryRes: any) => { + if (!queryRes.series) { + return; + } + queryRes.series.forEach((series: any) => { + const timeSerie: TimeSeries = { + target: series.name, + datapoints: series.points, + refId: queryRes.refId, + meta: queryRes.meta, }; + result.push(toDataFrame(timeSerie)); }); - }); + }); + return result; + } + + return Promise.resolve([]); } annotationQuery(options) {} @@ -217,14 +216,14 @@ export default class AzureMonitorDatasource { getSubscriptions(route?: string) { const url = `/${route || this.cloudName}/subscriptions?api-version=2019-03-01`; - return this.doRequest(url).then(result => { + return this.doRequest(url).then((result: any) => { return ResponseParser.parseSubscriptions(result); }); } getResourceGroups(subscriptionId: string) { const url = `${this.baseUrl}/${subscriptionId}/resourceGroups?api-version=${this.apiVersion}`; - return this.doRequest(url).then(result => { + return this.doRequest(url).then((result: AzureMonitorResourceGroupsResponse) => { return ResponseParser.parseResponseValues(result, 'name', 'name'); }); } @@ -234,7 +233,7 @@ export default class AzureMonitorDatasource { this.apiVersion }`; return this.doRequest(url) - .then(result => { + .then((result: AzureMonitorMetricDefinitionsResponse) => { return ResponseParser.parseResponseValues(result, 'type', 'type'); }) .then(result => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.test.ts deleted file mode 100644 index c848ed83c04..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -jest.mock('app/core/utils/kbn', () => { - return { - interval_to_ms: interval => { - if (interval.substring(interval.length - 1) === 's') { - return interval.substring(0, interval.length - 1) * 1000; - } - - if (interval.substring(interval.length - 1) === 'm') { - return interval.substring(0, interval.length - 1) * 1000 * 60; - } - - if (interval.substring(interval.length - 1) === 'd') { - return interval.substring(0, interval.length - 1) * 1000 * 60 * 24; - } - - return undefined; - }, - }; -}); - -import AzureMonitorFilterBuilder from './azure_monitor_filter_builder'; -import { toUtc } from '@grafana/ui/src/utils/moment_wrapper'; - -describe('AzureMonitorFilterBuilder', () => { - let builder: AzureMonitorFilterBuilder; - - const timefilter = 'timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z'; - const metricFilter = 'metricnames=Percentage CPU'; - - beforeEach(() => { - builder = new AzureMonitorFilterBuilder( - 'Percentage CPU', - toUtc('2017-08-22 06:00'), - toUtc('2017-08-22 07:00'), - 'PT1H', - '3m' - ); - }); - - describe('with a metric name and auto time grain of 3 minutes', () => { - beforeEach(() => { - builder.timeGrain = 'auto'; - }); - - it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => { - const filter = timefilter + '&interval=PT5M&' + metricFilter; - expect(builder.generateFilter()).toEqual(filter); - }); - }); - - describe('with a metric name and auto time grain of 30 seconds', () => { - beforeEach(() => { - builder.timeGrain = 'auto'; - builder.grafanaInterval = '30s'; - }); - - it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => { - const filter = timefilter + '&interval=PT1M&' + metricFilter; - expect(builder.generateFilter()).toEqual(filter); - }); - }); - - describe('with a metric name and auto time grain of 10 minutes', () => { - beforeEach(() => { - builder.timeGrain = 'auto'; - builder.grafanaInterval = '10m'; - }); - - it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => { - const filter = timefilter + '&interval=PT15M&' + metricFilter; - expect(builder.generateFilter()).toEqual(filter); - }); - }); - - describe('with a metric name and auto time grain of 2 day', () => { - beforeEach(() => { - builder.timeGrain = 'auto'; - builder.grafanaInterval = '2d'; - }); - - it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => { - const filter = timefilter + '&interval=P1D&' + metricFilter; - expect(builder.generateFilter()).toEqual(filter); - }); - }); - - describe('with a metric name and 1 hour time grain', () => { - it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => { - const filter = timefilter + '&interval=PT1H&' + metricFilter; - expect(builder.generateFilter()).toEqual(filter); - }); - }); - - describe('with a metric name and 1 minute time grain', () => { - beforeEach(() => { - builder.timeGrain = 'PT1M'; - }); - - it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => { - const filter = timefilter + '&interval=PT1M&' + metricFilter; - expect(builder.generateFilter()).toEqual(filter); - }); - }); - - describe('with a metric name and 1 day time grain and an aggregation', () => { - beforeEach(() => { - builder.timeGrain = 'P1D'; - builder.setAggregation('Maximum'); - }); - - it('should add time grain to the filter in ISO_8601 format', () => { - const filter = timefilter + '&interval=P1D&aggregation=Maximum&' + metricFilter; - expect(builder.generateFilter()).toEqual(filter); - }); - }); - - describe('with a metric name and 1 day time grain and an aggregation and a dimension', () => { - beforeEach(() => { - builder.setDimensionFilter('aDimension', 'aFilterValue'); - }); - - it('should add dimension to the filter', () => { - const filter = timefilter + '&interval=PT1H&' + metricFilter + `&$filter=aDimension eq 'aFilterValue'`; - expect(builder.generateFilter()).toEqual(filter); - }); - }); -}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.ts deleted file mode 100644 index 1b04f868877..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.ts +++ /dev/null @@ -1,72 +0,0 @@ -import TimegrainConverter from '../time_grain_converter'; - -export default class AzureMonitorFilterBuilder { - aggregation: string; - timeGrainInterval = ''; - dimension: string; - dimensionFilter: string; - allowedTimeGrains = ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d']; - - constructor( - private metricName: string, - private from, - private to, - public timeGrain: string, - public grafanaInterval: string - ) {} - - setAllowedTimeGrains(timeGrains) { - this.allowedTimeGrains = []; - timeGrains.forEach(tg => { - if (tg.value === 'auto') { - this.allowedTimeGrains.push(tg.value); - } else { - this.allowedTimeGrains.push(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value)); - } - }); - } - - setAggregation(agg) { - this.aggregation = agg; - } - - setDimensionFilter(dimension, dimensionFilter) { - this.dimension = dimension; - this.dimensionFilter = dimensionFilter; - } - - generateFilter() { - let filter = this.createDatetimeAndTimeGrainConditions(); - - if (this.aggregation) { - filter += `&aggregation=${this.aggregation}`; - } - - if (this.metricName && this.metricName.trim().length > 0) { - filter += `&metricnames=${this.metricName}`; - } - - if (this.dimension && this.dimensionFilter && this.dimensionFilter.trim().length > 0) { - filter += `&$filter=${this.dimension} eq '${this.dimensionFilter}'`; - } - - return filter; - } - - createDatetimeAndTimeGrainConditions() { - const dateTimeCondition = `timespan=${this.from.utc().format()}/${this.to.utc().format()}`; - - if (this.timeGrain === 'auto') { - this.timeGrain = this.calculateAutoTimeGrain(); - } - const timeGrainCondition = `&interval=${this.timeGrain}`; - - return dateTimeCondition + timeGrainCondition; - } - - calculateAutoTimeGrain() { - const roundedInterval = TimegrainConverter.findClosestTimeGrain(this.grafanaInterval, this.allowedTimeGrains); - - return TimegrainConverter.createISO8601DurationFromInterval(roundedInterval); - } -} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts index a6f89c8692e..bff5c1c45ec 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts @@ -1,115 +1,17 @@ import _ from 'lodash'; import TimeGrainConverter from '../time_grain_converter'; -import { dateTime } from '@grafana/ui/src/utils/moment_wrapper'; - export default class ResponseParser { - constructor(private results) {} + static parseResponseValues( + result: any, + textFieldName: string, + valueFieldName: string + ): Array<{ text: string; value: string }> { + const list: Array<{ text: string; value: string }> = []; - parseQueryResult() { - const data: any[] = []; - for (let i = 0; i < this.results.length; i++) { - for (let j = 0; j < this.results[i].result.data.value.length; j++) { - for (let k = 0; k < this.results[i].result.data.value[j].timeseries.length; k++) { - const alias = this.results[i].query.alias; - data.push({ - target: ResponseParser.createTarget( - this.results[i].result.data.value[j], - this.results[i].result.data.value[j].timeseries[k].metadatavalues, - alias - ), - datapoints: ResponseParser.convertDataToPoints(this.results[i].result.data.value[j].timeseries[k].data), - }); - } - } - } - return data; - } - - static createTarget(data, metadatavalues, alias: string) { - const resourceGroup = ResponseParser.parseResourceGroupFromId(data.id); - const resourceName = ResponseParser.parseResourceNameFromId(data.id); - const namespace = ResponseParser.parseNamespaceFromId(data.id, resourceName); - if (alias) { - const regex = /\{\{([\s\S]+?)\}\}/g; - return alias.replace(regex, (match, g1, g2) => { - const group = g1 || g2; - - if (group === 'resourcegroup') { - return resourceGroup; - } else if (group === 'namespace') { - return namespace; - } else if (group === 'resourcename') { - return resourceName; - } else if (group === 'metric') { - return data.name.value; - } else if (group === 'dimensionname') { - return metadatavalues && metadatavalues.length > 0 ? metadatavalues[0].name.value : ''; - } else if (group === 'dimensionvalue') { - return metadatavalues && metadatavalues.length > 0 ? metadatavalues[0].value : ''; - } - - return match; - }); + if (!result) { + return list; } - if (metadatavalues && metadatavalues.length > 0) { - return `${resourceName}{${metadatavalues[0].name.value}=${metadatavalues[0].value}}.${data.name.value}`; - } - - return `${resourceName}.${data.name.value}`; - } - - static parseResourceGroupFromId(id: string) { - const startIndex = id.indexOf('/resourceGroups/') + 16; - const endIndex = id.indexOf('/providers'); - - return id.substring(startIndex, endIndex); - } - - static parseNamespaceFromId(id: string, resourceName: string) { - const startIndex = id.indexOf('/providers/') + 11; - const endIndex = id.indexOf('/' + resourceName); - - return id.substring(startIndex, endIndex); - } - - static parseResourceNameFromId(id: string) { - const endIndex = id.lastIndexOf('/providers'); - const startIndex = id.slice(0, endIndex).lastIndexOf('/') + 1; - - return id.substring(startIndex, endIndex); - } - - static convertDataToPoints(timeDataFrame) { - const dataPoints: any[] = []; - - for (let k = 0; k < timeDataFrame.length; k++) { - const epoch = ResponseParser.dateTimeToEpoch(timeDataFrame[k].timeStamp); - const aggKey = ResponseParser.getKeyForAggregationField(timeDataFrame[k]); - - if (aggKey) { - dataPoints.push([timeDataFrame[k][aggKey], epoch]); - } - } - - return dataPoints; - } - - static dateTimeToEpoch(dateTimeValue) { - return dateTime(dateTimeValue).valueOf(); - } - - static getKeyForAggregationField(dataObj): string { - const keys = _.keys(dataObj); - if (keys.length < 2) { - return ''; - } - - return _.intersection(keys, ['total', 'average', 'maximum', 'minimum', 'count'])[0]; - } - - static parseResponseValues(result: any, textFieldName: string, valueFieldName: string) { - const list: any[] = []; for (let i = 0; i < result.data.value.length; i++) { if (!_.find(list, ['value', _.get(result.data.value[i], valueFieldName)])) { list.push({ @@ -121,8 +23,13 @@ export default class ResponseParser { return list; } - static parseResourceNames(result: any, metricDefinition: string) { - const list: any[] = []; + static parseResourceNames(result: any, metricDefinition: string): Array<{ text: string; value: string }> { + const list: Array<{ text: string; value: string }> = []; + + if (!result) { + return list; + } + for (let i = 0; i < result.data.value.length; i++) { if (result.data.value[i].type === metricDefinition) { list.push({ @@ -136,12 +43,21 @@ export default class ResponseParser { } static parseMetadata(result: any, metricName: string) { + const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count']; + + if (!result) { + return { + primaryAggType: '', + supportedAggTypes: defaultAggTypes, + supportedTimeGrains: [], + dimensions: [], + }; + } + const metricData: any = _.find(result.data.value, o => { return _.get(o, 'name.value') === metricName; }); - const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count']; - return { primaryAggType: metricData.primaryAggregationType, supportedAggTypes: metricData.supportedAggregationTypes || defaultAggTypes, @@ -150,8 +66,12 @@ export default class ResponseParser { }; } - static parseTimeGrains(metricAvailabilities) { + static parseTimeGrains(metricAvailabilities: any[]): Array<{ text: string; value: string }> { const timeGrains: any[] = []; + if (!metricAvailabilities) { + return timeGrains; + } + metricAvailabilities.forEach(avail => { if (avail.timeGrain) { timeGrains.push({ @@ -163,8 +83,8 @@ export default class ResponseParser { return timeGrains; } - static parseDimensions(metricData: any) { - const dimensions: any[] = []; + static parseDimensions(metricData: any): Array<{ text: string; value: string }> { + const dimensions: Array<{ text: string; value: string }> = []; if (!metricData.dimensions || metricData.dimensions.length === 0) { return dimensions; } @@ -182,10 +102,15 @@ export default class ResponseParser { return dimensions; } - static parseSubscriptions(result: any) { + static parseSubscriptions(result: any): Array<{ text: string; value: string }> { + const list: Array<{ text: string; value: string }> = []; + + if (!result) { + return list; + } + const valueFieldName = 'subscriptionId'; const textFieldName = 'displayName'; - const list: Array<{ text: string; value: string }> = []; for (let i = 0; i < result.data.value.length; i++) { if (!_.find(list, ['value', _.get(result.data.value[i], valueFieldName)])) { list.push({ diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts index 91a51a37f4a..c454a309fa2 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts @@ -235,7 +235,7 @@ export default class SupportedNamespaces { constructor(private cloudName: string) {} - get() { + get(): string[] { return this.supportedMetricNamespaces[this.cloudName]; } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts index 51883ee252d..59099cf050d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts @@ -52,24 +52,6 @@ describe('AzureMonitorUrlBuilder', () => { }); }); - describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices', () => { - it('should build the query url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorQueryUrl( - '', - 'sub1', - 'rg', - 'Microsoft.Storage/storageAccounts/blobServices', - 'rn1/default', - '2017-05-01-preview', - 'metricnames=aMetric' - ); - expect(url).toBe( - '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' + - 'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric' - ); - }); - }); - describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices', () => { it('should build the getMetricNames url in the longer format', () => { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( @@ -87,24 +69,6 @@ describe('AzureMonitorUrlBuilder', () => { }); }); - describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices', () => { - it('should build the query url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorQueryUrl( - '', - 'sub1', - 'rg', - 'Microsoft.Storage/storageAccounts/fileServices', - 'rn1/default', - '2017-05-01-preview', - 'metricnames=aMetric' - ); - expect(url).toBe( - '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' + - 'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric' - ); - }); - }); - describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices', () => { it('should build the getMetricNames url in the longer format', () => { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( @@ -122,24 +86,6 @@ describe('AzureMonitorUrlBuilder', () => { }); }); - describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices', () => { - it('should build the query url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorQueryUrl( - '', - 'sub1', - 'rg', - 'Microsoft.Storage/storageAccounts/tableServices', - 'rn1/default', - '2017-05-01-preview', - 'metricnames=aMetric' - ); - expect(url).toBe( - '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' + - 'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric' - ); - }); - }); - describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices', () => { it('should build the getMetricNames url in the longer format', () => { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( @@ -156,22 +102,4 @@ describe('AzureMonitorUrlBuilder', () => { ); }); }); - - describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices', () => { - it('should build the query url in the longer format', () => { - const url = UrlBuilder.buildAzureMonitorQueryUrl( - '', - 'sub1', - 'rg', - 'Microsoft.Storage/storageAccounts/queueServices', - 'rn1/default', - '2017-05-01-preview', - 'metricnames=aMetric' - ); - expect(url).toBe( - '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' + - 'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric' - ); - }); - }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts index cd84a68bf35..278a12ea98f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts @@ -1,29 +1,4 @@ export default class UrlBuilder { - static buildAzureMonitorQueryUrl( - baseUrl: string, - subscriptionId: string, - resourceGroup: string, - metricDefinition: string, - resourceName: string, - apiVersion: string, - filter: string - ) { - if ((metricDefinition.match(/\//g) || []).length > 1) { - const rn = resourceName.split('/'); - const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1); - const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/')); - return ( - `${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` + - `/providers/microsoft.insights/metrics?api-version=${apiVersion}&${filter}` - ); - } - - return ( - `${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${metricDefinition}/${resourceName}` + - `/providers/microsoft.insights/metrics?api-version=${apiVersion}&${filter}` - ); - } - static buildAzureMonitorGetMetricNamesUrl( baseUrl: string, subscriptionId: string, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts index e635be43902..64cc8e36790 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts @@ -3,6 +3,7 @@ import { QueryCtrl } from 'app/plugins/sdk'; // import './css/query_editor.css'; import TimegrainConverter from './time_grain_converter'; import './editor/editor_component'; +import kbn from 'app/core/utils/kbn'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { auto } from 'angular'; @@ -30,7 +31,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { dimensionFilter: string; timeGrain: string; timeGrainUnit: string; - timeGrains: any[]; + timeGrains: Array<{ text: string; value: string }>; + allowedTimeGrainsMs: number[]; dimensions: any[]; dimension: any; aggregation: string; @@ -175,6 +177,14 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { delete this.target.azureMonitor.timeGrainUnit; this.onMetricNameChange(); } + + if ( + this.target.azureMonitor.timeGrains && + this.target.azureMonitor.timeGrains.length > 0 && + (!this.target.azureMonitor.allowedTimeGrainsMs || this.target.azureMonitor.allowedTimeGrainsMs.length === 0) + ) { + this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.azureMonitor.timeGrains); + } } migrateToFromTimes() { @@ -312,6 +322,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { this.target.azureMonitor.timeGrain = ''; this.target.azureMonitor.dimensions = []; this.target.azureMonitor.dimension = ''; + this.refresh(); } onMetricDefinitionChange() { @@ -331,6 +342,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { this.target.azureMonitor.timeGrain = ''; this.target.azureMonitor.dimensions = []; this.target.azureMonitor.dimension = ''; + this.refresh(); } onMetricNameChange() { @@ -352,6 +364,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { this.target.azureMonitor.timeGrains = [{ text: 'auto', value: 'auto' }].concat(metadata.supportedTimeGrains); this.target.azureMonitor.timeGrain = 'auto'; + this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(metadata.supportedTimeGrains || []); + this.target.azureMonitor.dimensions = metadata.dimensions; if (metadata.dimensions.length > 0) { this.target.azureMonitor.dimension = metadata.dimensions[0].value; @@ -361,6 +375,16 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { .catch(this.handleQueryCtrlError.bind(this)); } + convertTimeGrainsToMs(timeGrains: Array<{ text: string; value: string }>) { + const allowedTimeGrainsMs: number[] = []; + timeGrains.forEach((tg: any) => { + if (tg.value !== 'auto') { + allowedTimeGrainsMs.push(kbn.interval_to_ms(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value))); + } + }); + return allowedTimeGrainsMs; + } + getAutoInterval() { if (this.target.azureMonitor.timeGrain === 'auto') { return TimegrainConverter.findClosestTimeGrain( diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts index 81d4fc50123..2f1b29204f7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts @@ -35,6 +35,7 @@ export interface AzureMetricQuery { timeGrainUnit: string; timeGrain: string; timeGrains: string[]; + allowedTimeGrainsMs: number[]; aggregation: string; dimension: string; dimensionFilter: string; @@ -47,6 +48,24 @@ export interface AzureLogsQuery { workspace: string; } +// Azure Monitor API Types + +export interface AzureMonitorMetricDefinitionsResponse { + data: { + value: Array<{ name: string; type: string; location?: string }>; + }; + status: number; + statusText: string; +} + +export interface AzureMonitorResourceGroupsResponse { + data: { + value: Array<{ name: string }>; + }; + status: number; + statusText: string; +} + // Azure Log Analytics types export interface KustoSchema { Databases: { [key: string]: KustoDatabase }; diff --git a/scripts/ci-frontend-metrics.sh b/scripts/ci-frontend-metrics.sh index 59f0b842c6d..b02ec38ab38 100755 --- a/scripts/ci-frontend-metrics.sh +++ b/scripts/ci-frontend-metrics.sh @@ -3,7 +3,7 @@ echo -e "Collecting code stats (typescript errors & more)" -ERROR_COUNT_LIMIT=3000 +ERROR_COUNT_LIMIT=2945 DIRECTIVES_LIMIT=172 CONTROLLERS_LIMIT=139