Azure Monitor: Add support to migrate some queries with template variables (#51881)

This commit is contained in:
Andres Martinez Gotor 2022-07-07 14:44:53 +02:00 committed by GitHub
parent f6f017edc2
commit 438c76252a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 319 additions and 113 deletions

View File

@ -1,7 +1,7 @@
import { find, startsWith } from 'lodash'; import { find, startsWith } from 'lodash';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { resourceTypeDisplayNames, supportedMetricNamespaces } from '../azureMetadata'; import { resourceTypeDisplayNames, supportedMetricNamespaces } from '../azureMetadata';
@ -42,11 +42,13 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
declare resourceGroup: string; declare resourceGroup: string;
declare resourceName: string; declare resourceName: string;
timeSrv: TimeSrv; timeSrv: TimeSrv;
templateSrv: TemplateSrv;
constructor(private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) { constructor(private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
super(instanceSettings); super(instanceSettings);
this.timeSrv = getTimeSrv(); this.timeSrv = getTimeSrv();
this.templateSrv = getTemplateSrv();
this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId; this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId;
const cloud = getAzureCloud(instanceSettings); const cloud = getAzureCloud(instanceSettings);
@ -244,7 +246,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
this.resourcePath, this.resourcePath,
this.apiPreviewVersion, this.apiPreviewVersion,
this.replaceTemplateVariables(query) this.replaceTemplateVariables(query),
this.templateSrv
); );
return this.getResource(url) return this.getResource(url)
.then((result: AzureMonitorMetricNamespacesResponse) => { .then((result: AzureMonitorMetricNamespacesResponse) => {
@ -273,7 +276,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
this.resourcePath, this.resourcePath,
this.apiVersion, this.apiVersion,
this.replaceTemplateVariables(query) this.replaceTemplateVariables(query),
this.templateSrv
); );
return this.getResource(url).then((result: AzureMonitorMetricNamesResponse) => { return this.getResource(url).then((result: AzureMonitorMetricNamesResponse) => {
return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value'); return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
@ -285,7 +289,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
this.resourcePath, this.resourcePath,
this.apiVersion, this.apiVersion,
this.replaceTemplateVariables(query) this.replaceTemplateVariables(query),
this.templateSrv
); );
return this.getResource(url).then((result: AzureMonitorMetricsMetadataResponse) => { return this.getResource(url).then((result: AzureMonitorMetricsMetadataResponse) => {
return ResponseParser.parseMetadata(result, metricName); return ResponseParser.parseMetadata(result, metricName);

View File

@ -1,33 +1,96 @@
import { getTemplateSrv } from '@grafana/runtime';
import UrlBuilder from './url_builder'; import UrlBuilder from './url_builder';
let replaceMock = jest.fn().mockImplementation((s: string) => s);
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getTemplateSrv: () => ({
replace: replaceMock,
}),
};
});
describe('AzureMonitorUrlBuilder', () => { describe('AzureMonitorUrlBuilder', () => {
let templateSrv = getTemplateSrv();
describe('buildResourceUri', () => { describe('buildResourceUri', () => {
it('builds a resource uri when the required properties are provided', () => { it('builds a resource uri when the required properties are provided', () => {
expect(UrlBuilder.buildResourceUri('sub', 'group', 'Microsoft.NetApp/netAppAccounts', 'name')).toEqual( expect(
'/subscriptions/sub/resourceGroups/group/providers/Microsoft.NetApp/netAppAccounts/name' UrlBuilder.buildResourceUri('sub', 'group', 'Microsoft.NetApp/netAppAccounts', 'name', templateSrv)
); ).toEqual('/subscriptions/sub/resourceGroups/group/providers/Microsoft.NetApp/netAppAccounts/name');
}); });
it('builds a resource uri correctly when a template variable is used as namespace', () => { it('builds a resource uri correctly when a template variable is used as namespace', () => {
expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns', 'name')).toEqual( expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns', 'name', templateSrv)).toEqual(
'/subscriptions/sub/resourceGroups/group/providers/$ns/name' '/subscriptions/sub/resourceGroups/group/providers/$ns/name'
); );
}); });
it('builds a resource uri correctly when the namespace includes a storage sub-resource', () => { it('builds a resource uri correctly when the namespace includes a storage sub-resource', () => {
expect( expect(
UrlBuilder.buildResourceUri('sub', 'group', 'Microsoft.Storage/storageAccounts/tableServices', 'name') UrlBuilder.buildResourceUri(
'sub',
'group',
'Microsoft.Storage/storageAccounts/tableServices',
'name',
templateSrv
)
).toEqual( ).toEqual(
'/subscriptions/sub/resourceGroups/group/providers/Microsoft.Storage/storageAccounts/name/tableServices/default' '/subscriptions/sub/resourceGroups/group/providers/Microsoft.Storage/storageAccounts/name/tableServices/default'
); );
}); });
describe('when using template variables', () => {
replaceMock = jest
.fn()
.mockImplementation((s: string) =>
s
.replace('$ns', 'Microsoft.Storage/storageAccounts')
.replace('$ns2', 'tableServices')
.replace('$rs', 'name')
.replace('$rs2', 'default')
);
templateSrv = getTemplateSrv();
it('builds a resource uri without specifying a subresource (default)', () => {
expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns/tableServices', 'name', templateSrv)).toEqual(
'/subscriptions/sub/resourceGroups/group/providers/$ns/name/tableServices/default'
);
});
it('builds a resource uri specifying a subresource (default)', () => {
expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns/tableServices', 'name/default', templateSrv)).toEqual(
'/subscriptions/sub/resourceGroups/group/providers/$ns/name/tableServices/default'
);
});
it('builds a resource uri specifying a resource template variable', () => {
expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns/tableServices', '$rs/default', templateSrv)).toEqual(
'/subscriptions/sub/resourceGroups/group/providers/$ns/$rs/tableServices/default'
);
});
it('builds a resource uri specifying multiple template variables', () => {
expect(UrlBuilder.buildResourceUri('sub', 'group', '$ns/$ns2', '$rs/$rs2', templateSrv)).toEqual(
'/subscriptions/sub/resourceGroups/group/providers/$ns/$rs/$ns2/$rs2'
);
});
});
}); });
describe('when a resource uri is provided', () => { describe('when a resource uri is provided', () => {
it('builds a getMetricNamesnamespace url', () => { it('builds a getMetricNamesnamespace url', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
resourceUri: '/subscriptions/sub/resource-uri/resource', '',
}); '2017-05-01-preview',
{
resourceUri: '/subscriptions/sub/resource-uri/resource',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
); );
@ -36,10 +99,15 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when a resource uri and metric namespace is provided', () => { describe('when a resource uri and metric namespace is provided', () => {
it('builds a getMetricNames url', () => { it('builds a getMetricNames url', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
resourceUri: '/subscriptions/sub/resource-uri/resource', '',
metricNamespace: 'Microsoft.Sql/servers', '2017-05-01-preview',
}); {
resourceUri: '/subscriptions/sub/resource-uri/resource',
metricNamespace: 'Microsoft.Sql/servers',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=Microsoft.Sql%2Fservers' '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=Microsoft.Sql%2Fservers'
); );
@ -49,12 +117,17 @@ describe('AzureMonitorUrlBuilder', () => {
describe('Legacy query object', () => { describe('Legacy query object', () => {
describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => { describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => {
it('should build the getMetricNamespaces url in the even longer format', () => { it('should build the getMetricNamespaces url in the even longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes', {
resourceName: 'rn1/rn2/rn3', subscription: 'sub1',
}); resourceGroup: 'rg',
metricDefinition: 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
resourceName: 'rn1/rn2/rn3',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
@ -64,12 +137,17 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.Sql/servers/databases', () => { describe('when metric definition is Microsoft.Sql/servers/databases', () => {
it('should build the getMetricNamespaces url in the longer format', () => { it('should build the getMetricNamespaces url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.Sql/servers/databases', {
resourceName: 'rn1/rn2', subscription: 'sub1',
}); resourceGroup: 'rg',
metricDefinition: 'Microsoft.Sql/servers/databases',
resourceName: 'rn1/rn2',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
@ -79,12 +157,17 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.Sql/servers', () => { describe('when metric definition is Microsoft.Sql/servers', () => {
it('should build the getMetricNamespaces url in the shorter format', () => { it('should build the getMetricNamespaces url in the shorter format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.Sql/servers', {
resourceName: 'rn', subscription: 'sub1',
}); resourceGroup: 'rg',
metricDefinition: 'Microsoft.Sql/servers',
resourceName: 'rn',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
@ -94,13 +177,18 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => { describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => {
it('should build the getMetricNames url in the even longer format', () => { it('should build the getMetricNames url in the even longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes', {
resourceName: 'rn1/rn2/rn3', subscription: 'sub1',
metricNamespace: 'default', resourceGroup: 'rg',
}); metricDefinition: 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
resourceName: 'rn1/rn2/rn3',
metricNamespace: 'default',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
@ -110,13 +198,18 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.Sql/servers/databases and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Sql/servers/databases and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.Sql/servers/databases', {
resourceName: 'rn1/rn2', subscription: 'sub1',
metricNamespace: 'default', resourceGroup: 'rg',
}); metricDefinition: 'Microsoft.Sql/servers/databases',
resourceName: 'rn1/rn2',
metricNamespace: 'default',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
@ -126,13 +219,18 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.Sql/servers and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Sql/servers and the metricNamespace is default', () => {
it('should build the getMetricNames url in the shorter format', () => { it('should build the getMetricNames url in the shorter format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.Sql/servers', {
resourceName: 'rn', subscription: 'sub1',
metricNamespace: 'default', resourceGroup: 'rg',
}); metricDefinition: 'Microsoft.Sql/servers',
resourceName: 'rn',
metricNamespace: 'default',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
@ -142,13 +240,18 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.Storage/storageAccounts/blobServices', {
resourceName: 'rn1/default', subscription: 'sub1',
metricNamespace: 'default', resourceGroup: 'rg',
}); metricDefinition: 'Microsoft.Storage/storageAccounts/blobServices',
resourceName: 'rn1/default',
metricNamespace: 'default',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
@ -158,13 +261,18 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.Storage/storageAccounts/fileServices', {
resourceName: 'rn1/default', subscription: 'sub1',
metricNamespace: 'default', resourceGroup: 'rg',
}); metricDefinition: 'Microsoft.Storage/storageAccounts/fileServices',
resourceName: 'rn1/default',
metricNamespace: 'default',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
@ -174,13 +282,18 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.Storage/storageAccounts/tableServices', {
resourceName: 'rn1/default', subscription: 'sub1',
metricNamespace: 'default', resourceGroup: 'rg',
}); metricDefinition: 'Microsoft.Storage/storageAccounts/tableServices',
resourceName: 'rn1/default',
metricNamespace: 'default',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
@ -190,13 +303,18 @@ describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl('', '2017-05-01-preview', { const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
subscription: 'sub1', '',
resourceGroup: 'rg', '2017-05-01-preview',
metricDefinition: 'Microsoft.Storage/storageAccounts/queueServices', {
resourceName: 'rn1/default', subscription: 'sub1',
metricNamespace: 'default', resourceGroup: 'rg',
}); metricDefinition: 'Microsoft.Storage/storageAccounts/queueServices',
resourceName: 'rn1/default',
metricNamespace: 'default',
},
templateSrv
);
expect(url).toBe( expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'

View File

@ -1,3 +1,5 @@
import { TemplateSrv } from '@grafana/runtime';
import { GetMetricNamespacesQuery, GetMetricNamesQuery } from '../types'; import { GetMetricNamespacesQuery, GetMetricNamesQuery } from '../types';
export default class UrlBuilder { export default class UrlBuilder {
@ -5,40 +7,65 @@ export default class UrlBuilder {
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
metricDefinition: string, metricDefinition: string,
resourceName: string resourceName: string,
templateSrv: TemplateSrv
) { ) {
const metricDefinitionProcessed = templateSrv.replace(metricDefinition);
const metricDefinitionArray = metricDefinition.split('/'); const metricDefinitionArray = metricDefinition.split('/');
const resourceNameProcessed = templateSrv.replace(resourceName);
const resourceNameArray = resourceName.split('/'); const resourceNameArray = resourceName.split('/');
const provider = metricDefinitionArray.shift(); const provider = metricDefinitionArray.shift();
const urlArray = ['/subscriptions', subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider]; const urlArray = ['/subscriptions', subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider];
if (metricDefinition.startsWith('Microsoft.Storage/storageAccounts/') && resourceNameArray.at(-1) !== 'default') {
if (
metricDefinitionProcessed.startsWith('Microsoft.Storage/storageAccounts/') &&
!resourceNameProcessed.endsWith('default')
) {
resourceNameArray.push('default'); resourceNameArray.push('default');
} }
if (metricDefinitionArray.length > 0) {
for (const i in metricDefinitionArray) { if (resourceNameArray.length > metricDefinitionArray.length) {
urlArray.push(metricDefinitionArray[i]); const parentResource = resourceNameArray.shift();
urlArray.push(resourceNameArray[i]); urlArray.push(parentResource);
} }
} else {
urlArray.push(resourceNameArray[0]); for (const i in metricDefinitionArray) {
urlArray.push(metricDefinitionArray[i]);
urlArray.push(resourceNameArray[i]);
} }
return urlArray.join('/'); return urlArray.join('/');
} }
static buildAzureMonitorGetMetricNamespacesUrl(baseUrl: string, apiVersion: string, query: GetMetricNamespacesQuery) { static buildAzureMonitorGetMetricNamespacesUrl(
baseUrl: string,
apiVersion: string,
query: GetMetricNamespacesQuery,
templateSrv: TemplateSrv
) {
let resourceUri: string; let resourceUri: string;
if ('resourceUri' in query) { if ('resourceUri' in query) {
resourceUri = query.resourceUri; resourceUri = query.resourceUri;
} else { } else {
const { subscription, resourceGroup, metricDefinition, resourceName } = query; const { subscription, resourceGroup, metricDefinition, resourceName } = query;
resourceUri = UrlBuilder.buildResourceUri(subscription, resourceGroup, metricDefinition, resourceName); resourceUri = UrlBuilder.buildResourceUri(
subscription,
resourceGroup,
metricDefinition,
resourceName,
templateSrv
);
} }
return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}`; return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}`;
} }
static buildAzureMonitorGetMetricNamesUrl(baseUrl: string, apiVersion: string, query: GetMetricNamesQuery) { static buildAzureMonitorGetMetricNamesUrl(
baseUrl: string,
apiVersion: string,
query: GetMetricNamesQuery,
templateSrv: TemplateSrv
) {
let resourceUri: string; let resourceUri: string;
const { metricNamespace } = query; const { metricNamespace } = query;
@ -46,7 +73,13 @@ export default class UrlBuilder {
resourceUri = query.resourceUri; resourceUri = query.resourceUri;
} else { } else {
const { subscription, resourceGroup, metricDefinition, resourceName } = query; const { subscription, resourceGroup, metricDefinition, resourceName } = query;
resourceUri = UrlBuilder.buildResourceUri(subscription, resourceGroup, metricDefinition, resourceName); resourceUri = UrlBuilder.buildResourceUri(
subscription,
resourceGroup,
metricDefinition,
resourceName,
templateSrv
);
} }
return ( return (

View File

@ -2,6 +2,8 @@ import deepEqual from 'fast-deep-equal';
import { defaults } from 'lodash'; import { defaults } from 'lodash';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { getTemplateSrv } from '@grafana/runtime';
import { AzureMonitorQuery, AzureQueryType } from '../../types'; import { AzureMonitorQuery, AzureQueryType } from '../../types';
import migrateQuery from '../../utils/migrateQuery'; import migrateQuery from '../../utils/migrateQuery';
@ -12,7 +14,7 @@ const DEFAULT_QUERY = {
const prepareQuery = (query: AzureMonitorQuery) => { const prepareQuery = (query: AzureMonitorQuery) => {
// Note: _.defaults does not apply default values deeply. // Note: _.defaults does not apply default values deeply.
const withDefaults = defaults({}, query, DEFAULT_QUERY); const withDefaults = defaults({}, query, DEFAULT_QUERY);
const migratedQuery = migrateQuery(withDefaults); const migratedQuery = migrateQuery(withDefaults, getTemplateSrv());
// If we didn't make any changes to the object, then return the original object to keep the // If we didn't make any changes to the object, then return the original object to keep the
// identity the same, and not trigger any other useEffects or anything. // identity the same, and not trigger any other useEffects or anything.

View File

@ -70,7 +70,7 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
for (const baseTarget of options.targets) { for (const baseTarget of options.targets) {
// Migrate old query structures // Migrate old query structures
const target = datasourceMigrations(baseTarget); const target = datasourceMigrations(baseTarget, this.templateSrv);
// Skip hidden or invalid queries or ones without properties // Skip hidden or invalid queries or ones without properties
if (!target.queryType || target.hide || !hasQueryForType(target)) { if (!target.queryType || target.hide || !hasQueryForType(target)) {

View File

@ -1,7 +1,22 @@
import { getTemplateSrv } from '@grafana/runtime';
import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types'; import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types';
import migrateQuery from './migrateQuery'; import migrateQuery from './migrateQuery';
let replaceMock = jest.fn().mockImplementation((s: string) => s);
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getTemplateSrv: () => ({
replace: replaceMock,
}),
};
});
let templateSrv = getTemplateSrv();
const azureMonitorQueryV7 = { const azureMonitorQueryV7 = {
appInsights: { dimension: [], metricName: 'select', timeGrain: 'auto' }, appInsights: { dimension: [], metricName: 'select', timeGrain: 'auto' },
azureLogAnalytics: { azureLogAnalytics: {
@ -82,7 +97,7 @@ const modernMetricsQuery: AzureMonitorQuery = {
describe('AzureMonitor: migrateQuery', () => { describe('AzureMonitor: migrateQuery', () => {
it('modern queries should not change', () => { it('modern queries should not change', () => {
const result = migrateQuery(modernMetricsQuery); const result = migrateQuery(modernMetricsQuery, templateSrv);
// MUST use .toBe because we want to assert that the identity of unmigrated queries remains the same // MUST use .toBe because we want to assert that the identity of unmigrated queries remains the same
expect(modernMetricsQuery).toBe(result); expect(modernMetricsQuery).toBe(result);
@ -90,7 +105,7 @@ describe('AzureMonitor: migrateQuery', () => {
describe('migrating from a v7 query to the latest query version', () => { describe('migrating from a v7 query to the latest query version', () => {
it('should build a resource uri', () => { it('should build a resource uri', () => {
const result = migrateQuery(azureMonitorQueryV7); const result = migrateQuery(azureMonitorQueryV7, templateSrv);
expect(result).toMatchObject( expect(result).toMatchObject(
expect.objectContaining({ expect.objectContaining({
azureMonitor: expect.objectContaining({ azureMonitor: expect.objectContaining({
@ -104,7 +119,7 @@ describe('AzureMonitor: migrateQuery', () => {
describe('migrating from a v8 query to the latest query version', () => { describe('migrating from a v8 query to the latest query version', () => {
it('should build a resource uri', () => { it('should build a resource uri', () => {
const result = migrateQuery(azureMonitorQueryV8); const result = migrateQuery(azureMonitorQueryV8, templateSrv);
expect(result).toMatchObject( expect(result).toMatchObject(
expect.objectContaining({ expect.objectContaining({
azureMonitor: expect.objectContaining({ azureMonitor: expect.objectContaining({
@ -114,6 +129,20 @@ describe('AzureMonitor: migrateQuery', () => {
}) })
); );
}); });
it('should not build a resource uri with an unsupported template variable', () => {
replaceMock = jest.fn().mockImplementation((s: string) => s.replace('$ns', 'Microsoft.Storage/storageAccounts'));
templateSrv = getTemplateSrv();
const query = {
...azureMonitorQueryV8,
azureMonitor: {
...azureMonitorQueryV8,
metricDefinition: '$ns',
},
};
const result = migrateQuery(query, templateSrv);
expect(result.azureMonitor?.resourceUri).toBeUndefined();
});
}); });
describe('migrating from a v9 query to the latest query version', () => { describe('migrating from a v9 query to the latest query version', () => {
@ -121,7 +150,7 @@ describe('AzureMonitor: migrateQuery', () => {
const dimensionFilters: AzureMetricDimension[] = [ const dimensionFilters: AzureMetricDimension[] = [
{ dimension: 'TestDimension', operator: 'eq', filters: ['testFilter'] }, { dimension: 'TestDimension', operator: 'eq', filters: ['testFilter'] },
]; ];
const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }); const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }, templateSrv);
expect(result).toMatchObject( expect(result).toMatchObject(
expect.objectContaining({ expect.objectContaining({
azureMonitor: expect.objectContaining({ azureMonitor: expect.objectContaining({
@ -132,7 +161,7 @@ describe('AzureMonitor: migrateQuery', () => {
}); });
it('correctly updates old filter containing wildcard', () => { it('correctly updates old filter containing wildcard', () => {
const dimensionFilters: AzureMetricDimension[] = [{ dimension: 'TestDimension', operator: 'eq', filter: '*' }]; const dimensionFilters: AzureMetricDimension[] = [{ dimension: 'TestDimension', operator: 'eq', filter: '*' }];
const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }); const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }, templateSrv);
expect(result).toMatchObject( expect(result).toMatchObject(
expect.objectContaining({ expect.objectContaining({
azureMonitor: expect.objectContaining({ azureMonitor: expect.objectContaining({
@ -145,7 +174,7 @@ describe('AzureMonitor: migrateQuery', () => {
}); });
it('correctly updates old filter containing value', () => { it('correctly updates old filter containing value', () => {
const dimensionFilters: AzureMetricDimension[] = [{ dimension: 'TestDimension', operator: 'eq', filter: 'test' }]; const dimensionFilters: AzureMetricDimension[] = [{ dimension: 'TestDimension', operator: 'eq', filter: 'test' }];
const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }); const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }, templateSrv);
expect(result).toMatchObject( expect(result).toMatchObject(
expect.objectContaining({ expect.objectContaining({
azureMonitor: expect.objectContaining({ azureMonitor: expect.objectContaining({
@ -160,7 +189,7 @@ describe('AzureMonitor: migrateQuery', () => {
const dimensionFilters: AzureMetricDimension[] = [ const dimensionFilters: AzureMetricDimension[] = [
{ dimension: 'TestDimension', operator: 'eq', filter: '*', filters: ['testFilter'] }, { dimension: 'TestDimension', operator: 'eq', filter: '*', filters: ['testFilter'] },
]; ];
const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }); const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }, templateSrv);
expect(result).toMatchObject( expect(result).toMatchObject(
expect.objectContaining({ expect.objectContaining({
azureMonitor: expect.objectContaining({ azureMonitor: expect.objectContaining({
@ -179,7 +208,7 @@ describe('AzureMonitor: migrateQuery', () => {
const dimensionFilters: AzureMetricDimension[] = [ const dimensionFilters: AzureMetricDimension[] = [
{ dimension: 'TestDimension', operator: 'eq', filter: 'testFilter', filters: ['testFilter'] }, { dimension: 'TestDimension', operator: 'eq', filter: 'testFilter', filters: ['testFilter'] },
]; ];
const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }); const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } }, templateSrv);
expect(result).toMatchObject( expect(result).toMatchObject(
expect.objectContaining({ expect.objectContaining({
azureMonitor: expect.objectContaining({ azureMonitor: expect.objectContaining({

View File

@ -1,3 +1,5 @@
import { TemplateSrv } from '@grafana/runtime';
import UrlBuilder from '../azure_monitor/url_builder'; import UrlBuilder from '../azure_monitor/url_builder';
import { setKustoQuery } from '../components/LogsQueryEditor/setQueryValue'; import { setKustoQuery } from '../components/LogsQueryEditor/setQueryValue';
import { import {
@ -9,7 +11,7 @@ import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../type
const OLD_DEFAULT_DROPDOWN_VALUE = 'select'; const OLD_DEFAULT_DROPDOWN_VALUE = 'select';
export default function migrateQuery(query: AzureMonitorQuery): AzureMonitorQuery { export default function migrateQuery(query: AzureMonitorQuery, templateSrv: TemplateSrv): AzureMonitorQuery {
let workingQuery = query; let workingQuery = query;
// The old angular controller also had a `migrateApplicationInsightsKeys` migraiton that // The old angular controller also had a `migrateApplicationInsightsKeys` migraiton that
@ -21,7 +23,7 @@ export default function migrateQuery(query: AzureMonitorQuery): AzureMonitorQuer
workingQuery = migrateLogAnalyticsToFromTimes(workingQuery); workingQuery = migrateLogAnalyticsToFromTimes(workingQuery);
workingQuery = migrateToDefaultNamespace(workingQuery); workingQuery = migrateToDefaultNamespace(workingQuery);
workingQuery = migrateDimensionToDimensionFilter(workingQuery); workingQuery = migrateDimensionToDimensionFilter(workingQuery);
workingQuery = migrateResourceUri(workingQuery); workingQuery = migrateResourceUri(workingQuery, templateSrv);
workingQuery = migrateDimensionFilterToArray(workingQuery); workingQuery = migrateDimensionFilterToArray(workingQuery);
return workingQuery; return workingQuery;
@ -96,7 +98,7 @@ function migrateDimensionToDimensionFilter(query: AzureMonitorQuery): AzureMonit
// Azure Monitor metric queries prior to Grafana version 9 did not include a `resourceUri`. // Azure Monitor metric queries prior to Grafana version 9 did not include a `resourceUri`.
// The resourceUri was previously constructed with the subscription id, resource group, // The resourceUri was previously constructed with the subscription id, resource group,
// metric definition (a.k.a. resource type), and the resource name. // metric definition (a.k.a. resource type), and the resource name.
function migrateResourceUri(query: AzureMonitorQuery): AzureMonitorQuery { function migrateResourceUri(query: AzureMonitorQuery, templateSrv: TemplateSrv): AzureMonitorQuery {
const azureMonitorQuery = query.azureMonitor; const azureMonitorQuery = query.azureMonitor;
if (!azureMonitorQuery || azureMonitorQuery.resourceUri) { if (!azureMonitorQuery || azureMonitorQuery.resourceUri) {
@ -109,11 +111,28 @@ function migrateResourceUri(query: AzureMonitorQuery): AzureMonitorQuery {
return query; return query;
} }
if (metricDefinition.includes('$') || resourceName.includes('$')) { const metricDefinitionArray = metricDefinition.split('/');
if (metricDefinitionArray.some((p) => templateSrv.replace(p).split('/').length > 2)) {
// If a metric definition includes template variable with a subresource e.g.
// Microsoft.Storage/storageAccounts/libraries, it's not possible to generate a valid
// resource URI
return query; return query;
} }
const resourceUri = UrlBuilder.buildResourceUri(subscription, resourceGroup, metricDefinition, resourceName); const resourceNameArray = resourceName.split('/');
if (resourceNameArray.some((p) => templateSrv.replace(p).split('/').length > 1)) {
// If a resource name includes template variable with a subresource e.g.
// abc123/def456, it's not possible to generate a valid resource URI
return query;
}
const resourceUri = UrlBuilder.buildResourceUri(
subscription,
resourceGroup,
metricDefinition,
resourceName,
templateSrv
);
return { return {
...query, ...query,
@ -163,7 +182,7 @@ function migrateDimensionFilterToArray(query: AzureMonitorQuery): AzureMonitorQu
// datasource.ts also contains some migrations, which have been moved to here. Unsure whether // datasource.ts also contains some migrations, which have been moved to here. Unsure whether
// they should also do all the other migrations... // they should also do all the other migrations...
export function datasourceMigrations(query: AzureMonitorQuery): AzureMonitorQuery { export function datasourceMigrations(query: AzureMonitorQuery, templateSrv: TemplateSrv): AzureMonitorQuery {
let workingQuery = query; let workingQuery = query;
if (!workingQuery.queryType) { if (!workingQuery.queryType) {
@ -175,7 +194,7 @@ export function datasourceMigrations(query: AzureMonitorQuery): AzureMonitorQuer
if (workingQuery.queryType === AzureQueryType.AzureMonitor && workingQuery.azureMonitor) { if (workingQuery.queryType === AzureQueryType.AzureMonitor && workingQuery.azureMonitor) {
workingQuery = migrateDimensionToDimensionFilter(workingQuery); workingQuery = migrateDimensionToDimensionFilter(workingQuery);
workingQuery = migrateResourceUri(workingQuery); workingQuery = migrateResourceUri(workingQuery, templateSrv);
workingQuery = migrateDimensionFilterToArray(workingQuery); workingQuery = migrateDimensionFilterToArray(workingQuery);
} }