cache api calls when args are the same (#57082)

This commit is contained in:
Erik Sundell
2022-10-19 08:40:03 +02:00
committed by GitHub
parent a30885c523
commit 8949e574e6
6 changed files with 102 additions and 76 deletions
@@ -10,7 +10,9 @@ import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchData
export function setupMockedAPI({
variables,
response,
getMock,
}: {
getMock?: jest.Func;
response?: Array<{ text: string; label: string; value: string }>;
variables?: CustomVariableModel[];
mockGetVariableName?: boolean;
@@ -61,7 +61,9 @@ export const CloudWatchSettings: DataSourceInstanceSettings<CloudWatchJsonData>
export function setupMockedDataSource({
variables,
mockGetVariableName = true,
getMock = jest.fn(),
}: {
getMock?: jest.Func;
variables?: CustomVariableModel[];
mockGetVariableName?: boolean;
} = {}) {
@@ -84,7 +86,7 @@ export function setupMockedDataSource({
setBackendSrv({
...getBackendSrv(),
fetch: fetchMock,
get: jest.fn(),
get: getMock,
});
return { datasource, fetchMock, templateService, timeSrv };
@@ -1,55 +1,72 @@
import { setupMockedAPI } from './__mocks__/API';
describe('describeLogGroup', () => {
it('replaces region correctly in the query', async () => {
const { api, resourceRequestMock } = setupMockedAPI();
await api.describeLogGroups({ region: 'default' });
expect(resourceRequestMock.mock.calls[0][1].region).toBe('us-west-1');
describe('api', () => {
describe('describeLogGroup', () => {
it('replaces region correctly in the query', async () => {
const { api, resourceRequestMock } = setupMockedAPI();
await api.describeLogGroups({ region: 'default' });
expect(resourceRequestMock.mock.calls[0][1].region).toBe('us-west-1');
await api.describeLogGroups({ region: 'eu-east' });
expect(resourceRequestMock.mock.calls[1][1].region).toBe('eu-east');
await api.describeLogGroups({ region: 'eu-east' });
expect(resourceRequestMock.mock.calls[1][1].region).toBe('eu-east');
});
it('should return log groups as an array of options', async () => {
const response = [
{
text: '/aws/containerinsights/dev303-workshop/application',
value: '/aws/containerinsights/dev303-workshop/application',
label: '/aws/containerinsights/dev303-workshop/application',
},
{
text: '/aws/containerinsights/dev303-workshop/flowlogs',
value: '/aws/containerinsights/dev303-workshop/flowlogs',
label: '/aws/containerinsights/dev303-workshop/flowlogs',
},
{
text: '/aws/containerinsights/dev303-workshop/dataplane',
value: '/aws/containerinsights/dev303-workshop/dataplane',
label: '/aws/containerinsights/dev303-workshop/dataplane',
},
];
const { api } = setupMockedAPI({ response });
const expectedLogGroups = [
{
text: '/aws/containerinsights/dev303-workshop/application',
value: '/aws/containerinsights/dev303-workshop/application',
label: '/aws/containerinsights/dev303-workshop/application',
},
{
text: '/aws/containerinsights/dev303-workshop/flowlogs',
value: '/aws/containerinsights/dev303-workshop/flowlogs',
label: '/aws/containerinsights/dev303-workshop/flowlogs',
},
{
text: '/aws/containerinsights/dev303-workshop/dataplane',
value: '/aws/containerinsights/dev303-workshop/dataplane',
label: '/aws/containerinsights/dev303-workshop/dataplane',
},
];
const logGroups = await api.describeLogGroups({ region: 'default' });
expect(logGroups).toEqual(expectedLogGroups);
});
});
it('should return log groups as an array of options', async () => {
const response = [
{
text: '/aws/containerinsights/dev303-workshop/application',
value: '/aws/containerinsights/dev303-workshop/application',
label: '/aws/containerinsights/dev303-workshop/application',
},
{
text: '/aws/containerinsights/dev303-workshop/flowlogs',
value: '/aws/containerinsights/dev303-workshop/flowlogs',
label: '/aws/containerinsights/dev303-workshop/flowlogs',
},
{
text: '/aws/containerinsights/dev303-workshop/dataplane',
value: '/aws/containerinsights/dev303-workshop/dataplane',
label: '/aws/containerinsights/dev303-workshop/dataplane',
},
];
const { api } = setupMockedAPI({ response });
const expectedLogGroups = [
{
text: '/aws/containerinsights/dev303-workshop/application',
value: '/aws/containerinsights/dev303-workshop/application',
label: '/aws/containerinsights/dev303-workshop/application',
},
{
text: '/aws/containerinsights/dev303-workshop/flowlogs',
value: '/aws/containerinsights/dev303-workshop/flowlogs',
label: '/aws/containerinsights/dev303-workshop/flowlogs',
},
{
text: '/aws/containerinsights/dev303-workshop/dataplane',
value: '/aws/containerinsights/dev303-workshop/dataplane',
label: '/aws/containerinsights/dev303-workshop/dataplane',
},
];
const logGroups = await api.describeLogGroups({ region: 'default' });
expect(logGroups).toEqual(expectedLogGroups);
describe('memoization', () => {
it('should not initiate new api request in case a previous request had same args', async () => {
const getMock = jest.fn();
const { api, resourceRequestMock } = setupMockedAPI({ getMock });
await Promise.all([
api.getMetrics('AWS/EC2', 'us-east-1'),
api.getMetrics('AWS/EC2', 'us-east-1'),
api.getMetrics('AWS/EC2', 'us-east-2'),
api.getMetrics('AWS/EC2', 'us-east-2'),
api.getMetrics('AWS/EC2', 'us-east-2'),
]);
expect(resourceRequestMock).toHaveBeenCalledTimes(2);
});
});
});
+19 -15
View File
@@ -1,3 +1,5 @@
import { memoize } from 'lodash';
import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
@@ -10,37 +12,39 @@ export interface SelectableResourceValue extends SelectableValue<string> {
}
export class CloudWatchAPI extends CloudWatchRequest {
private memoizedGetRequest;
constructor(instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {
super(instanceSettings, templateSrv);
this.memoizedGetRequest = memoize(this.getRequest.bind(this), (path, parameters) =>
JSON.stringify({ path, parameters })
);
}
resourceRequest(
subtype: string,
parameters?: Record<string, string | string[] | number>
): Promise<SelectableResourceValue[]> {
private getRequest<T>(subtype: string, parameters?: Record<string, string | string[] | number>): Promise<T> {
return getBackendSrv().get(`/api/datasources/${this.instanceSettings.id}/resources/${subtype}`, parameters);
}
getRegions() {
return this.resourceRequest('regions').then((regions) => [
return this.memoizedGetRequest<SelectableResourceValue[]>('regions').then((regions) => [
{ label: 'default', value: 'default', text: 'default' },
...regions.filter((r) => r.value),
]);
}
getNamespaces() {
return this.resourceRequest('namespaces');
return this.memoizedGetRequest<SelectableResourceValue[]>('namespaces');
}
async describeLogGroups(params: DescribeLogGroupsRequest) {
return this.resourceRequest('log-groups', {
return this.memoizedGetRequest<SelectableResourceValue[]>('log-groups', {
...params,
region: this.templateSrv.replace(this.getActualRegion(params.region)),
});
}
async describeAllLogGroups(params: DescribeLogGroupsRequest) {
return this.resourceRequest('all-log-groups', {
return this.memoizedGetRequest<SelectableResourceValue[]>('all-log-groups', {
...params,
region: this.templateSrv.replace(this.getActualRegion(params.region)),
});
@@ -51,14 +55,14 @@ export class CloudWatchAPI extends CloudWatchRequest {
return [];
}
return this.resourceRequest('metrics', {
return this.memoizedGetRequest<SelectableResourceValue[]>('metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
});
}
async getAllMetrics(region: string): Promise<Array<{ metricName?: string; namespace: string }>> {
const values = await this.resourceRequest('all-metrics', {
const values = await this.memoizedGetRequest<SelectableResourceValue[]>('all-metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
});
@@ -75,7 +79,7 @@ export class CloudWatchAPI extends CloudWatchRequest {
return [];
}
return this.resourceRequest('dimension-keys', {
return this.memoizedGetRequest<SelectableResourceValue[]>('dimension-keys', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
dimensionFilters: JSON.stringify(this.convertDimensionFormat(dimensionFilters, {})),
@@ -94,7 +98,7 @@ export class CloudWatchAPI extends CloudWatchRequest {
return [];
}
const values = await this.resourceRequest('dimension-values', {
const values = await this.memoizedGetRequest<SelectableResourceValue[]>('dimension-values', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
metricName: this.templateSrv.replace(metricName.trim()),
@@ -106,14 +110,14 @@ export class CloudWatchAPI extends CloudWatchRequest {
}
getEbsVolumeIds(region: string, instanceId: string) {
return this.resourceRequest('ebs-volume-ids', {
return this.memoizedGetRequest<SelectableResourceValue[]>('ebs-volume-ids', {
region: this.templateSrv.replace(this.getActualRegion(region)),
instanceId: this.templateSrv.replace(instanceId),
});
}
getEc2InstanceAttribute(region: string, attributeName: string, filters: MultiFilters) {
return this.resourceRequest('ec2-instance-attribute', {
return this.memoizedGetRequest<SelectableResourceValue[]>('ec2-instance-attribute', {
region: this.templateSrv.replace(this.getActualRegion(region)),
attributeName: this.templateSrv.replace(attributeName),
filters: JSON.stringify(this.convertMultiFilterFormat(filters, 'filter key')),
@@ -121,7 +125,7 @@ export class CloudWatchAPI extends CloudWatchRequest {
}
getResourceARNs(region: string, resourceType: string, tags: MultiFilters) {
return this.resourceRequest('resource-arns', {
return this.memoizedGetRequest<SelectableResourceValue[]>('resource-arns', {
region: this.templateSrv.replace(this.getActualRegion(region)),
resourceType: this.templateSrv.replace(resourceType),
tags: JSON.stringify(this.convertMultiFilterFormat(tags, 'tag name')),
@@ -53,7 +53,7 @@ export const FilterItem: FunctionComponent<Props> = ({
return datasource.api
.getDimensionValues(region, namespace, metricName, filter.key, dimensionsExcludingCurrentKey)
.then((result: Array<SelectableValue<string>>) => {
if (result.length && !disableExpressions) {
if (result.length && !disableExpressions && !result.some((o) => o.value === wildcardOption.value)) {
result.unshift(wildcardOption);
}
return appendTemplateVariables(datasource, result);
@@ -203,17 +203,18 @@ describe('datasource', () => {
describe('resource requests', () => {
it('should map resource response to metric response', async () => {
const datasource = setupMockedDataSource().datasource;
datasource.api.resourceRequest = jest.fn().mockResolvedValue([
{
text: 'AWS/EC2',
value: 'CPUUtilization',
},
{
text: 'AWS/Redshift',
value: 'CPUPercentage',
},
]);
const datasource = setupMockedDataSource({
getMock: jest.fn().mockResolvedValue([
{
text: 'AWS/EC2',
value: 'CPUUtilization',
},
{
text: 'AWS/Redshift',
value: 'CPUPercentage',
},
]),
}).datasource;
const allMetrics = await datasource.api.getAllMetrics('us-east-2');
expect(allMetrics[0].metricName).toEqual('CPUUtilization');
expect(allMetrics[0].namespace).toEqual('AWS/EC2');