AzureMonitor: Improve resource picker efficiency (#93127)

* Parameterise region building metric namespace URL

- Add parameter for region (this parameter takes precedence over if global is set)
- Update tests
- Support this parameter on the data source method

* Refactor fetchAllNamespaces

- Use Set rather than an array for greater performance
- Request namespaces across WestEurope, EastUS, and JapanEast concurrently
- Update test

* Maintain existing behaviour
This commit is contained in:
Andreas Christou 2024-09-18 15:17:36 +01:00 committed by GitHub
parent 72bfa624ce
commit 6a3dbe7d41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 87 additions and 19 deletions

View File

@ -231,14 +231,15 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
return (await Promise.all(promises)).flat(); return (await Promise.all(promises)).flat();
} }
getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean) { getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean, region?: string) {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
this.resourcePath, this.resourcePath,
this.apiPreviewVersion, this.apiPreviewVersion,
// Only use the first query, as the metric namespaces should be the same for all queries // Only use the first query, as the metric namespaces should be the same for all queries
this.replaceSingleTemplateVariables(query), this.replaceSingleTemplateVariables(query),
globalRegion, globalRegion,
this.templateSrv this.templateSrv,
region
); );
return this.getResource(url) return this.getResource(url)
.then((result: AzureAPIResponse<Namespace>) => { .then((result: AzureAPIResponse<Namespace>) => {

View File

@ -142,6 +142,38 @@ describe('AzureMonitorUrlBuilder', () => {
'/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview&region=global' '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview&region=global'
); );
}); });
it('builds a getMetricNamesnamespace url with a specific region', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
'',
'2017-05-01-preview',
{
resourceUri: '/subscriptions/sub/resource-uri/resource',
},
false,
templateSrv,
'testregion'
);
expect(url).toBe(
'/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview&region=testregion'
);
});
it('builds a getMetricNamesnamespace url with a specific region (overriding global)', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
'',
'2017-05-01-preview',
{
resourceUri: '/subscriptions/sub/resource-uri/resource',
},
true,
templateSrv,
'testregion'
);
expect(url).toBe(
'/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview&region=testregion'
);
});
}); });
describe('when a resource uri and metric namespace is provided', () => { describe('when a resource uri and metric namespace is provided', () => {

View File

@ -51,7 +51,8 @@ export default class UrlBuilder {
apiVersion: string, apiVersion: string,
query: GetMetricNamespacesQuery, query: GetMetricNamespacesQuery,
globalRegion: boolean, globalRegion: boolean,
templateSrv: TemplateSrv templateSrv: TemplateSrv,
region?: string
) { ) {
let resourceUri: string; let resourceUri: string;
@ -68,7 +69,7 @@ export default class UrlBuilder {
} }
return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}${ return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}${
globalRegion ? '&region=global' : '' region ? `&region=${region}` : globalRegion ? '&region=global' : ''
}`; }`;
} }

View File

@ -312,9 +312,33 @@ describe('AzureMonitor resourcePickerData', () => {
}, },
], ],
}; };
const { resourcePickerData, postResource } = createResourcePickerData([mockSubscriptionsResponse, mockResponse]); const { resourcePickerData, postResource, mockDatasource } = createResourcePickerData([
mockSubscriptionsResponse,
mockResponse,
]);
const formattedResults = await resourcePickerData.search('vmname', 'metrics'); const formattedResults = await resourcePickerData.search('vmname', 'metrics');
expect(postResource).toBeCalledTimes(2); expect(postResource).toHaveBeenCalledTimes(2);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'westeurope'
);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'eastus'
);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'japaneast'
);
const secondCall = postResource.mock.calls[1]; const secondCall = postResource.mock.calls[1];
const [_, postBody] = secondCall; const [_, postBody] = secondCall;
expect(postBody.query).not.toContain('union resourcecontainers'); expect(postBody.query).not.toContain('union resourcecontainers');

View File

@ -1,5 +1,3 @@
import { uniq } from 'lodash';
import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceWithBackend, reportInteraction } from '@grafana/runtime'; import { DataSourceWithBackend, reportInteraction } from '@grafana/runtime';
@ -359,28 +357,40 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
private async fetchAllNamespaces() { private async fetchAllNamespaces() {
const subscriptions = await this.getSubscriptions(); const subscriptions = await this.getSubscriptions();
reportInteraction('grafana_ds_azuremonitor_subscriptions_loaded', { subscriptions: subscriptions.length }); reportInteraction('grafana_ds_azuremonitor_subscriptions_loaded', { subscriptions: subscriptions.length });
let supportedMetricNamespaces: string[] = [];
for await (const subscription of subscriptions) { let supportedMetricNamespaces: Set<string> = new Set();
// We make use of these three regions as they *should* contain every possible namespace
const regions = ['westeurope', 'eastus', 'japaneast'];
const getNamespacesForRegion = async (region: string) => {
const namespaces = await this.azureMonitorDatasource.getMetricNamespaces( const namespaces = await this.azureMonitorDatasource.getMetricNamespaces(
{ {
resourceUri: `/subscriptions/${subscription.id}`, // We only need to run this request against the first available subscription
resourceUri: `/subscriptions/${subscriptions[0].id}`,
}, },
true false,
region
); );
if (namespaces) { if (namespaces) {
const namespaceVals = namespaces.map((namespace) => `"${namespace.value.toLocaleLowerCase()}"`); for (const namespace of namespaces) {
supportedMetricNamespaces = supportedMetricNamespaces.concat(namespaceVals); supportedMetricNamespaces.add(`"${namespace.value.toLocaleLowerCase()}"`);
}
} }
} };
if (supportedMetricNamespaces.length === 0) { const promises = regions.map((region) => getNamespacesForRegion(region));
await Promise.all(promises);
if (supportedMetricNamespaces.size === 0) {
throw new Error( throw new Error(
'Unable to resolve a list of valid metric namespaces. Validate the datasource configuration is correct and required permissions have been granted for all subscriptions. Grafana requires at least the Reader role to be assigned.' 'Unable to resolve a list of valid metric namespaces. Validate the datasource configuration is correct and required permissions have been granted for all subscriptions. Grafana requires at least the Reader role to be assigned.'
); );
} }
this.supportedMetricNamespaces = uniq(
supportedMetricNamespaces.concat(resourceTypes.map((namespace) => `"${namespace}"`)) resourceTypes.forEach((namespace) => {
).join(','); supportedMetricNamespaces.add(`"${namespace}"`);
});
this.supportedMetricNamespaces = Array.from(supportedMetricNamespaces).join(',');
} }
parseRows(resources: Array<string | AzureMonitorResource>): ResourceRow[] { parseRows(resources: Array<string | AzureMonitorResource>): ResourceRow[] {