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();
}
getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean) {
getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean, region?: string) {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
this.resourcePath,
this.apiPreviewVersion,
// Only use the first query, as the metric namespaces should be the same for all queries
this.replaceSingleTemplateVariables(query),
globalRegion,
this.templateSrv
this.templateSrv,
region
);
return this.getResource(url)
.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'
);
});
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', () => {

View File

@ -51,7 +51,8 @@ export default class UrlBuilder {
apiVersion: string,
query: GetMetricNamespacesQuery,
globalRegion: boolean,
templateSrv: TemplateSrv
templateSrv: TemplateSrv,
region?: string
) {
let resourceUri: string;
@ -68,7 +69,7 @@ export default class UrlBuilder {
}
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');
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 [_, postBody] = secondCall;
expect(postBody.query).not.toContain('union resourcecontainers');

View File

@ -1,5 +1,3 @@
import { uniq } from 'lodash';
import { DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceWithBackend, reportInteraction } from '@grafana/runtime';
@ -359,28 +357,40 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
private async fetchAllNamespaces() {
const subscriptions = await this.getSubscriptions();
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(
{
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) {
const namespaceVals = namespaces.map((namespace) => `"${namespace.value.toLocaleLowerCase()}"`);
supportedMetricNamespaces = supportedMetricNamespaces.concat(namespaceVals);
for (const namespace of namespaces) {
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(
'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}"`))
).join(',');
resourceTypes.forEach((namespace) => {
supportedMetricNamespaces.add(`"${namespace}"`);
});
this.supportedMetricNamespaces = Array.from(supportedMetricNamespaces).join(',');
}
parseRows(resources: Array<string | AzureMonitorResource>): ResourceRow[] {