CloudWatch: Support request cancellation properly (#28865)

This commit is contained in:
Hugo Häggmark 2020-11-19 14:10:38 +01:00 committed by GitHub
parent f9281742d7
commit 32d4c8c6bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 425 additions and 379 deletions

View File

@ -1,88 +1,101 @@
import { DataQueryResponse, dateTime, DefaultTimeRange } from '@grafana/data';
import { of } from 'rxjs';
import { setBackendSrv } from '@grafana/runtime';
import { dateTime, DefaultTimeRange, observableTester } from '@grafana/data';
import { TemplateSrv } from '../../../features/templating/template_srv';
import { CloudWatchDatasource } from './datasource';
describe('datasource', () => {
describe('query', () => {
it('should return error if log query and log groups is not specified', async () => {
it('should return error if log query and log groups is not specified', done => {
const { datasource } = setup();
const response: DataQueryResponse = (await datasource
.query({
observableTester().subscribeAndExpectOnNext({
observable: datasource.query({
targets: [
{
queryMode: 'Logs' as 'Logs',
},
],
} as any)
.toPromise()) as any;
expect(response.error?.message).toBe('Log group is required');
} as any),
expect: response => {
expect(response.error?.message).toBe('Log group is required');
},
done,
});
});
it('should return empty response if queries are hidden', async () => {
it('should return empty response if queries are hidden', done => {
const { datasource } = setup();
const response: DataQueryResponse = (await datasource
.query({
observableTester().subscribeAndExpectOnNext({
observable: datasource.query({
targets: [
{
queryMode: 'Logs' as 'Logs',
hide: true,
},
],
} as any)
.toPromise()) as any;
expect(response.data).toEqual([]);
} as any),
expect: response => {
expect(response.data).toEqual([]);
},
done,
});
});
});
describe('performTimeSeriesQuery', () => {
it('should return the same length of data as result', async () => {
const { datasource } = setup();
const awsRequestMock = jest.spyOn(datasource, 'awsRequest');
const buildCloudwatchConsoleUrlMock = jest.spyOn(datasource, 'buildCloudwatchConsoleUrl');
buildCloudwatchConsoleUrlMock.mockImplementation(() => '');
awsRequestMock.mockImplementation(async () => {
return {
it('should return the same length of data as result', done => {
const { datasource } = setup({
data: {
results: {
a: { refId: 'a', series: [{ name: 'cpu', points: [1, 1] }], meta: { gmdMeta: '' } },
b: { refId: 'b', series: [{ name: 'memory', points: [2, 2] }], meta: { gmdMeta: '' } },
},
};
},
});
const buildCloudwatchConsoleUrlMock = jest.spyOn(datasource, 'buildCloudwatchConsoleUrl');
buildCloudwatchConsoleUrlMock.mockImplementation(() => '');
observableTester().subscribeAndExpectOnNext({
observable: datasource.performTimeSeriesQuery(
{
queries: [
{ datasourceId: 1, refId: 'a' },
{ datasourceId: 1, refId: 'b' },
],
} as any,
{ from: dateTime(), to: dateTime() } as any
),
expect: response => {
expect(response.data.length).toEqual(2);
},
done,
});
const response: DataQueryResponse = await datasource.performTimeSeriesQuery(
{
queries: [
{ datasourceId: 1, refId: 'a' },
{ datasourceId: 1, refId: 'b' },
],
} as any,
{ from: dateTime(), to: dateTime() } as any
);
expect(response.data.length).toEqual(2);
});
});
describe('describeLogGroup', () => {
it('replaces region correctly in the query', async () => {
const { datasource, datasourceRequestMock } = setup();
const { datasource, fetchMock } = setup();
await datasource.describeLogGroups({ region: 'default' });
expect(datasourceRequestMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
await datasource.describeLogGroups({ region: 'eu-east' });
expect(datasourceRequestMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
});
});
});
function setup() {
function setup({ data = [] }: { data?: any } = {}) {
const datasource = new CloudWatchDatasource({ jsonData: { defaultRegion: 'us-west-1' } } as any, new TemplateSrv(), {
timeRange() {
return DefaultTimeRange;
},
} as any);
const datasourceRequestMock = jest.fn();
datasourceRequestMock.mockResolvedValue({ data: [] });
setBackendSrv({ datasourceRequest: datasourceRequestMock } as any);
const fetchMock = jest.fn().mockReturnValue(of({ data }));
setBackendSrv({ fetch: fetchMock } as any);
return { datasource, datasourceRequestMock };
return { datasource, fetchMock };
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import angular from 'angular';
import _ from 'lodash';
import { from, merge, Observable, of, zip } from 'rxjs';
import { merge, Observable, of, throwError, zip } from 'rxjs';
import {
catchError,
concatMap,
@ -184,7 +184,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
queries: queryParams,
};
return from(this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams)).pipe(
return this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams).pipe(
mergeMap((response: TSDBResponse) => {
const channelName: string = response.results['A'].meta.channelName;
const channel = getGrafanaLiveSrv().getChannel({
@ -310,11 +310,17 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
queries: validMetricsQueries,
};
return from(this.performTimeSeriesQuery(request, options.range));
return this.performTimeSeriesQuery(request, options.range);
};
logsQuery(
queryParams: Array<{ queryId: string; refId: string; limit?: number; region: string; statsGroups?: string[] }>
queryParams: Array<{
queryId: string;
refId: string;
limit?: number;
region: string;
statsGroups?: string[];
}>
): Observable<DataQueryResponse> {
this.logQueries = {};
queryParams.forEach(param => {
@ -587,74 +593,76 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
)}`;
}
async performTimeSeriesQuery(request: MetricRequest, { from, to }: TimeRange): Promise<any> {
try {
const res: TSDBResponse = await this.awsRequest(TSDB_QUERY_ENDPOINT, request);
const dataframes: DataFrame[] = toDataQueryResponse({ data: res }).data;
if (!dataframes || dataframes.length <= 0) {
return { data: [] };
}
const data = dataframes.map(frame => {
const queryResult = res.results[frame.refId!];
const error = queryResult.error ? { message: queryResult.error } : null;
if (!queryResult) {
return { frame, error };
performTimeSeriesQuery(request: MetricRequest, { from, to }: TimeRange): Observable<any> {
return this.awsRequest(TSDB_QUERY_ENDPOINT, request).pipe(
map(res => {
const dataframes: DataFrame[] = toDataQueryResponse({ data: res }).data;
if (!dataframes || dataframes.length <= 0) {
return { data: [] };
}
const requestQuery = request.queries.find(q => q.refId === frame.refId!) as any;
const link = this.buildCloudwatchConsoleUrl(
requestQuery!,
from.toISOString(),
to.toISOString(),
frame.refId!,
queryResult.meta.gmdMeta
);
if (link) {
for (const field of frame.fields) {
field.config.links = [
{
url: link,
title: 'View in CloudWatch console',
targetBlank: true,
},
];
const data = dataframes.map(frame => {
const queryResult = res.results[frame.refId!];
const error = queryResult.error ? { message: queryResult.error } : null;
if (!queryResult) {
return { frame, error };
}
const requestQuery = request.queries.find(q => q.refId === frame.refId!) as any;
const link = this.buildCloudwatchConsoleUrl(
requestQuery!,
from.toISOString(),
to.toISOString(),
frame.refId!,
queryResult.meta.gmdMeta
);
if (link) {
for (const field of frame.fields) {
field.config.links = [
{
url: link,
title: 'View in CloudWatch console',
targetBlank: true,
},
];
}
}
return { frame, error };
});
return {
data: data.map(o => o.frame),
error: data
.map(o => o.error)
.reduce((err, error) => {
return err || error;
}, null),
};
}),
catchError(err => {
if (/^Throttling:.*/.test(err.data.message)) {
const failedRedIds = Object.keys(err.data.results);
const regionsAffected = Object.values(request.queries).reduce(
(res: string[], { refId, region }) =>
(refId && !failedRedIds.includes(refId)) || res.includes(region) ? res : [...res, region],
[]
) as string[];
regionsAffected.forEach(region => this.debouncedAlert(this.datasourceName, this.getActualRegion(region)));
}
return { frame, error };
});
return {
data: data.map(o => o.frame),
error: data
.map(o => o.error)
.reduce((err, error) => {
return err || error;
}, null),
};
} catch (err) {
if (/^Throttling:.*/.test(err.data.message)) {
const failedRedIds = Object.keys(err.data.results);
const regionsAffected = Object.values(request.queries).reduce(
(res: string[], { refId, region }) =>
(refId && !failedRedIds.includes(refId)) || res.includes(region) ? res : [...res, region],
[]
) as string[];
if (err.data && err.data.message === 'Metric request error' && err.data.error) {
err.data.message = err.data.error;
}
regionsAffected.forEach(region => this.debouncedAlert(this.datasourceName, this.getActualRegion(region)));
}
if (err.data && err.data.message === 'Metric request error' && err.data.error) {
err.data.message = err.data.error;
}
throw err;
}
return throwError(err);
})
);
}
transformSuggestDataFromTable(suggestData: TSDBResponse) {
transformSuggestDataFromTable(suggestData: TSDBResponse): Array<{ text: any; label: any; value: any }> {
return suggestData.results['metricFindQuery'].tables[0].rows.map(([text, value]) => ({
text,
value,
@ -662,7 +670,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
}));
}
doMetricQueryRequest(subtype: string, parameters: any) {
doMetricQueryRequest(subtype: string, parameters: any): Promise<Array<{ text: any; label: any; value: any }>> {
const range = this.timeSrv.timeRange();
return this.awsRequest(TSDB_QUERY_ENDPOINT, {
from: range.from.valueOf().toString(),
@ -678,9 +686,13 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
...parameters,
},
],
}).then((r: TSDBResponse) => {
return this.transformSuggestDataFromTable(r);
});
})
.pipe(
map(r => {
return this.transformSuggestDataFromTable(r);
})
)
.toPromise();
}
makeLogActionRequest(
@ -724,7 +736,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
const resultsToDataFrames = (val: any): DataFrame[] => toDataQueryResponse(val).data || [];
return from(this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams)).pipe(
return this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams).pipe(
map(response => resultsToDataFrames({ data: response })),
catchError(err => {
if (err.data?.error) {
@ -920,15 +932,19 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
...parameters,
},
],
}).then((r: TSDBResponse) => {
return r.results['annotationQuery'].tables[0].rows.map(v => ({
annotation: annotation,
time: Date.parse(v[0]),
title: v[1],
tags: [v[2]],
text: v[3],
}));
});
})
.pipe(
map(r => {
return r.results['annotationQuery'].tables[0].rows.map(v => ({
annotation: annotation,
time: Date.parse(v[0]),
title: v[1],
tags: [v[2]],
text: v[3],
}));
})
)
.toPromise();
}
targetContainsTemplate(target: any) {
@ -955,16 +971,16 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
}));
}
async awsRequest(url: string, data: MetricRequest) {
awsRequest(url: string, data: MetricRequest): Observable<TSDBResponse> {
const options = {
method: 'POST',
url,
data,
};
const result = await getBackendSrv().datasourceRequest(options);
return result.data;
return getBackendSrv()
.fetch<TSDBResponse>(options)
.pipe(map(result => result.data));
}
getDefaultRegion() {

View File

@ -3,22 +3,22 @@ import _ from 'lodash';
// Services & Utils
import syntax, {
QUERY_COMMANDS,
AGGREGATION_FUNCTIONS_STATS,
STRING_FUNCTIONS,
DATETIME_FUNCTIONS,
IP_FUNCTIONS,
BOOLEAN_FUNCTIONS,
NUMERIC_OPERATORS,
DATETIME_FUNCTIONS,
FIELD_AND_FILTER_FUNCTIONS,
IP_FUNCTIONS,
NUMERIC_OPERATORS,
QUERY_COMMANDS,
STRING_FUNCTIONS,
} from './syntax';
// Types
import { CloudWatchQuery } from './types';
import { AbsoluteTimeRange, LanguageProvider, HistoryItem } from '@grafana/data';
import { CloudWatchQuery, TSDBResponse } from './types';
import { AbsoluteTimeRange, HistoryItem, LanguageProvider } from '@grafana/data';
import { CloudWatchDatasource } from './datasource';
import { TypeaheadInput, TypeaheadOutput, Token } from '@grafana/ui';
import { Token, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import Prism, { Grammar } from 'prismjs';
export type CloudWatchHistoryItem = HistoryItem<CloudWatchQuery>;
@ -49,8 +49,8 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
return syntax;
}
request = (url: string, params?: any): Promise<{ data: { data: string[] } }> => {
return this.datasource.awsRequest(url, params);
request = (url: string, params?: any): Promise<TSDBResponse> => {
return this.datasource.awsRequest(url, params).toPromise();
};
start = () => {

View File

@ -1,6 +1,4 @@
import '../datasource';
import { CloudWatchDatasource, MAX_ATTEMPTS } from '../datasource';
import * as redux from 'app/store/store';
import { interval, of, throwError } from 'rxjs';
import {
DataFrame,
DataQueryErrorType,
@ -8,22 +6,20 @@ import {
DataSourceInstanceSettings,
dateMath,
getFrameDisplayName,
observableTester,
} from '@grafana/data';
import { FetchResponse } from '@grafana/runtime';
import * as redux from 'app/store/store';
import '../datasource';
import { CloudWatchDatasource, MAX_ATTEMPTS } from '../datasource';
import { TemplateSrv } from 'app/features/templating/template_srv';
import {
CloudWatchLogsQuery,
CloudWatchLogsQueryStatus,
CloudWatchMetricsQuery,
CloudWatchQuery,
LogAction,
} from '../types';
import { CloudWatchLogsQuery, CloudWatchLogsQueryStatus, CloudWatchMetricsQuery, LogAction } from '../types';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { convertToStoreState } from '../../../../../test/helpers/convertToStoreState';
import { getTemplateSrvDependencies } from 'test/helpers/getTemplateSrvDependencies';
import { interval, of } from 'rxjs';
import { CustomVariableModel, initialVariableModelState, VariableHide } from '../../../../features/variables/types';
import { TimeSrvStub } from '../../../../../test/specs/helpers';
import * as rxjsUtils from '../utils/rxjs/increasingInterval';
@ -38,18 +34,22 @@ jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => backendSrv,
}));
describe('CloudWatchDatasource', () => {
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
type Args = { response?: any; throws?: boolean; templateSrv?: TemplateSrv };
function getTestContext({ response = {}, throws = false, templateSrv = new TemplateSrv() }: Args = {}) {
jest.clearAllMocks();
const fetchMock = jest.spyOn(backendSrv, 'fetch');
throws
? fetchMock.mockImplementation(() => throwError(response))
: fetchMock.mockImplementation(() => of(createFetchResponse(response)));
const instanceSettings = {
jsonData: { defaultRegion: 'us-east-1' },
name: 'TestDatasource',
} as DataSourceInstanceSettings;
let templateSrv = new TemplateSrv();
const start = 1483196400 * 1000;
const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) };
const timeSrv = {
time: { from: '2016-12-31 15:00:00Z', to: '2016-12-31 16:00:00Z' },
timeRange: () => {
@ -60,35 +60,32 @@ describe('CloudWatchDatasource', () => {
},
} as TimeSrv;
const ctx = {
templateSrv,
} as any;
const ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv);
return { ds, fetchMock, instanceSettings };
}
describe('CloudWatchDatasource', () => {
const start = 1483196400 * 1000;
const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) };
beforeEach(() => {
ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv);
jest.clearAllMocks();
});
describe('When getting log groups', () => {
beforeEach(() => {
datasourceRequestMock.mockImplementation(() =>
Promise.resolve({
data: {
results: {
A: {
dataframes: [
'QVJST1cxAAD/////GAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFgAAAACAAAAKAAAAAQAAAB8////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAJz///8IAAAAFAAAAAkAAABsb2dHcm91cHMAAAAEAAAAbmFtZQAAAAABAAAAGAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABMAAAAUAAAAAAABQFMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAGAAAAAwAAABsb2dHcm91cE5hbWUAAAAABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAADAAAAGxvZ0dyb3VwTmFtZQAAAAD/////mAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAAGAGAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAEgAAAAhAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiAAAAAAAAACIAAAAAAAAANgFAAAAAAAAAAAAAAEAAAAhAAAAAAAAAAAAAAAAAAAAAAAAADIAAABiAAAAkQAAALwAAADuAAAAHwEAAFQBAACHAQAAtQEAAOoBAAAbAgAASgIAAHQCAAClAgAA1QIAABADAABEAwAAdgMAAKMDAADXAwAACQQAAEAEAAB3BAAAlwQAAK0EAAC8BAAA+wQAAEIFAABhBQAAeAUAAJIFAAC0BQAA1gUAAC9hd3MvY29udGFpbmVyaW5zaWdodHMvZGV2MzAzLXdvcmtzaG9wL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvZGF0YXBsYW5lL2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvZmxvd2xvZ3MvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2RldjMwMy13b3Jrc2hvcC9ob3N0L2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2RldjMwMy13b3Jrc2hvcC9wcm9tZXRoZXVzL2F3cy9jb250YWluZXJpbnNpZ2h0cy9lY29tbWVyY2Utc29ja3Nob3AvYXBwbGljYXRpb24vYXdzL2NvbnRhaW5lcmluc2lnaHRzL2Vjb21tZXJjZS1zb2Nrc2hvcC9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2Vjb21tZXJjZS1zb2Nrc2hvcC9ob3N0L2F3cy9jb250YWluZXJpbnNpZ2h0cy9lY29tbWVyY2Utc29ja3Nob3AvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcGVyZi9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL2hvc3QvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL3BlcmZvcm1hbmNlL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcGVyZi9wcm9tZXRoZXVzL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcHJvZC11cy1lYXN0LTEvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tc3RhZ2luZy9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL2hvc3QvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL3BlcmZvcm1hbmNlL2F3cy9lY3MvY29udGFpbmVyaW5zaWdodHMvYnVnYmFzaC1lYzIvcGVyZm9ybWFuY2UvYXdzL2Vjcy9jb250YWluZXJpbnNpZ2h0cy9lY3MtZGVtb3dvcmtzaG9wL3BlcmZvcm1hbmNlL2F3cy9lY3MvY29udGFpbmVyaW5zaWdodHMvZWNzLXdvcmtzaG9wLWRldi9wZXJmb3JtYW5jZS9hd3MvZWtzL2RldjMwMy13b3Jrc2hvcC9jbHVzdGVyL2F3cy9ldmVudHMvY2xvdWR0cmFpbC9hd3MvZXZlbnRzL2Vjcy9hd3MvbGFtYmRhL2N3c3luLW15Y2FuYXJ5LWZhYzk3ZGVkLWYxMzQtNDk5YS05ZDcxLTRjM2JlMWY2MzE4Mi9hd3MvbGFtYmRhL2N3c3luLXdhdGNoLWxpbmtjaGVja3MtZWY3ZWYyNzMtNWRhMi00NjYzLWFmNTQtZDJmNTJkNTViMDYwL2Vjcy9lY3MtY3dhZ2VudC1kYWVtb24tc2VydmljZS9lY3MvZWNzLWRlbW8tbGltaXRUYXNrQ2xvdWRUcmFpbC9EZWZhdWx0TG9nR3JvdXBjb250YWluZXItaW5zaWdodHMtcHJvbWV0aGV1cy1iZXRhY29udGFpbmVyLWluc2lnaHRzLXByb21ldGhldXMtZGVtbwAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAMAAQAAACgBAAAAAAAAoAAAAAAAAABgBgAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAWAAAAAIAAAAoAAAABAAAAHz///8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAnP///wgAAAAUAAAACQAAAGxvZ0dyb3VwcwAAAAQAAABuYW1lAAAAAAEAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAEwAAABQAAAAAAAFAUwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAYAAAADAAAAGxvZ0dyb3VwTmFtZQAAAAAEAAAAbmFtZQAAAAAAAAAABAAEAAQAAAAMAAAAbG9nR3JvdXBOYW1lAAAAAEgBAABBUlJPVzE=',
],
refId: 'A',
},
},
},
})
);
});
it('should return log groups as an array of strings', async () => {
const logGroups = await ctx.ds.describeLogGroups();
const response = {
results: {
A: {
dataframes: [
'QVJST1cxAAD/////GAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFgAAAACAAAAKAAAAAQAAAB8////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAJz///8IAAAAFAAAAAkAAABsb2dHcm91cHMAAAAEAAAAbmFtZQAAAAABAAAAGAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABMAAAAUAAAAAAABQFMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAGAAAAAwAAABsb2dHcm91cE5hbWUAAAAABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAADAAAAGxvZ0dyb3VwTmFtZQAAAAD/////mAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAAGAGAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAEgAAAAhAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiAAAAAAAAACIAAAAAAAAANgFAAAAAAAAAAAAAAEAAAAhAAAAAAAAAAAAAAAAAAAAAAAAADIAAABiAAAAkQAAALwAAADuAAAAHwEAAFQBAACHAQAAtQEAAOoBAAAbAgAASgIAAHQCAAClAgAA1QIAABADAABEAwAAdgMAAKMDAADXAwAACQQAAEAEAAB3BAAAlwQAAK0EAAC8BAAA+wQAAEIFAABhBQAAeAUAAJIFAAC0BQAA1gUAAC9hd3MvY29udGFpbmVyaW5zaWdodHMvZGV2MzAzLXdvcmtzaG9wL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvZGF0YXBsYW5lL2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvZmxvd2xvZ3MvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2RldjMwMy13b3Jrc2hvcC9ob3N0L2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2RldjMwMy13b3Jrc2hvcC9wcm9tZXRoZXVzL2F3cy9jb250YWluZXJpbnNpZ2h0cy9lY29tbWVyY2Utc29ja3Nob3AvYXBwbGljYXRpb24vYXdzL2NvbnRhaW5lcmluc2lnaHRzL2Vjb21tZXJjZS1zb2Nrc2hvcC9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2Vjb21tZXJjZS1zb2Nrc2hvcC9ob3N0L2F3cy9jb250YWluZXJpbnNpZ2h0cy9lY29tbWVyY2Utc29ja3Nob3AvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcGVyZi9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL2hvc3QvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL3BlcmZvcm1hbmNlL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcGVyZi9wcm9tZXRoZXVzL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcHJvZC11cy1lYXN0LTEvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tc3RhZ2luZy9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL2hvc3QvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL3BlcmZvcm1hbmNlL2F3cy9lY3MvY29udGFpbmVyaW5zaWdodHMvYnVnYmFzaC1lYzIvcGVyZm9ybWFuY2UvYXdzL2Vjcy9jb250YWluZXJpbnNpZ2h0cy9lY3MtZGVtb3dvcmtzaG9wL3BlcmZvcm1hbmNlL2F3cy9lY3MvY29udGFpbmVyaW5zaWdodHMvZWNzLXdvcmtzaG9wLWRldi9wZXJmb3JtYW5jZS9hd3MvZWtzL2RldjMwMy13b3Jrc2hvcC9jbHVzdGVyL2F3cy9ldmVudHMvY2xvdWR0cmFpbC9hd3MvZXZlbnRzL2Vjcy9hd3MvbGFtYmRhL2N3c3luLW15Y2FuYXJ5LWZhYzk3ZGVkLWYxMzQtNDk5YS05ZDcxLTRjM2JlMWY2MzE4Mi9hd3MvbGFtYmRhL2N3c3luLXdhdGNoLWxpbmtjaGVja3MtZWY3ZWYyNzMtNWRhMi00NjYzLWFmNTQtZDJmNTJkNTViMDYwL2Vjcy9lY3MtY3dhZ2VudC1kYWVtb24tc2VydmljZS9lY3MvZWNzLWRlbW8tbGltaXRUYXNrQ2xvdWRUcmFpbC9EZWZhdWx0TG9nR3JvdXBjb250YWluZXItaW5zaWdodHMtcHJvbWV0aGV1cy1iZXRhY29udGFpbmVyLWluc2lnaHRzLXByb21ldGhldXMtZGVtbwAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAMAAQAAACgBAAAAAAAAoAAAAAAAAABgBgAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAWAAAAAIAAAAoAAAABAAAAHz///8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAnP///wgAAAAUAAAACQAAAGxvZ0dyb3VwcwAAAAQAAABuYW1lAAAAAAEAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAEwAAABQAAAAAAAFAUwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAYAAAADAAAAGxvZ0dyb3VwTmFtZQAAAAAEAAAAbmFtZQAAAAAAAAAABAAEAAQAAAAMAAAAbG9nR3JvdXBOYW1lAAAAAEgBAABBUlJPVzE=',
],
refId: 'A',
},
},
};
const { ds } = getTestContext({ response });
const expectedLogGroups = [
'/aws/containerinsights/dev303-workshop/application',
'/aws/containerinsights/dev303-workshop/dataplane',
@ -124,6 +121,9 @@ describe('CloudWatchDatasource', () => {
'container-insights-prometheus-beta',
'container-insights-prometheus-demo',
];
const logGroups = await ds.describeLogGroups({});
expect(logGroups).toEqual(expectedLogGroups);
});
});
@ -134,6 +134,7 @@ describe('CloudWatchDatasource', () => {
});
it('should add data links to response', () => {
const { ds } = getTestContext();
const mockResponse: DataQueryResponse = {
data: [
{
@ -149,7 +150,7 @@ describe('CloudWatchDatasource', () => {
],
};
const mockOptions = {
const mockOptions: any = {
targets: [
{
refId: 'A',
@ -160,7 +161,7 @@ describe('CloudWatchDatasource', () => {
],
};
const saturatedResponse = ctx.ds.addDataLinksToLogsResponse(mockResponse, mockOptions);
const saturatedResponse = ds['addDataLinksToLogsResponse'](mockResponse, mockOptions);
expect(saturatedResponse).toMatchObject({
data: [
{
@ -185,6 +186,7 @@ describe('CloudWatchDatasource', () => {
});
it('should stop querying when no more data received a number of times in a row', async () => {
const { ds } = getTestContext();
const fakeFrames = genMockFrames(20);
const initialRecordsMatched = fakeFrames[0].meta!.stats!.find(stat => stat.displayName === 'Records scanned')!
.value!;
@ -209,7 +211,7 @@ describe('CloudWatchDatasource', () => {
}
let i = 0;
jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
if (subtype === 'GetQueryResults') {
const mockObservable = of([fakeFrames[i]]);
i++;
@ -219,7 +221,7 @@ describe('CloudWatchDatasource', () => {
}
});
const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise();
const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise();
const expectedData = [
{
@ -246,10 +248,11 @@ describe('CloudWatchDatasource', () => {
});
it('should continue querying as long as new data is being received', async () => {
const { ds } = getTestContext();
const fakeFrames = genMockFrames(15);
let i = 0;
jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
if (subtype === 'GetQueryResults') {
const mockObservable = of([fakeFrames[i]]);
i++;
@ -259,7 +262,7 @@ describe('CloudWatchDatasource', () => {
}
});
const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise();
const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise();
expect(myResponse).toEqual({
data: [fakeFrames[fakeFrames.length - 1]],
key: 'test-key',
@ -269,9 +272,10 @@ describe('CloudWatchDatasource', () => {
});
it('should stop querying when results come back with status "Complete"', async () => {
const { ds } = getTestContext();
const fakeFrames = genMockFrames(3);
let i = 0;
jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
if (subtype === 'GetQueryResults') {
const mockObservable = of([fakeFrames[i]]);
i++;
@ -281,7 +285,7 @@ describe('CloudWatchDatasource', () => {
}
});
const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise();
const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise();
expect(myResponse).toEqual({
data: [fakeFrames[2]],
@ -292,8 +296,9 @@ describe('CloudWatchDatasource', () => {
});
it('should call the replace method on provided log groups', () => {
const replaceSpy = jest.spyOn(ctx.ds, 'replace').mockImplementation((target: string) => target);
ctx.ds.makeLogActionRequest('StartQuery', [
const { ds } = getTestContext();
const replaceSpy = jest.spyOn(ds, 'replace').mockImplementation((target: string) => target);
ds.makeLogActionRequest('StartQuery', [
{
queryString: 'test query string',
region: 'default',
@ -308,7 +313,7 @@ describe('CloudWatchDatasource', () => {
});
describe('When performing CloudWatch metrics query', () => {
const query = {
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -353,28 +358,29 @@ describe('CloudWatchDatasource', () => {
},
};
beforeEach(() => {
datasourceRequestMock.mockImplementation(() => {
return Promise.resolve({ data: response });
it('should generate the correct query', done => {
const { ds, fetchMock } = getTestContext({ response });
observableTester().subscribeAndExpectOnComplete({
observable: ds.query(query),
expect: () => {
expect(fetchMock.mock.calls[0][0].data.queries).toMatchObject(
expect.arrayContaining([
expect.objectContaining({
namespace: query.targets[0].namespace,
metricName: query.targets[0].metricName,
dimensions: { InstanceId: ['i-12345678'] },
statistics: query.targets[0].statistics,
period: query.targets[0].period,
}),
])
);
},
done,
});
});
it('should generate the correct query', async () => {
await ctx.ds.query(query).toPromise();
expect(datasourceRequestMock.mock.calls[0][0].data.queries).toMatchObject(
expect.arrayContaining([
expect.objectContaining({
namespace: query.targets[0].namespace,
metricName: query.targets[0].metricName,
dimensions: { InstanceId: ['i-12345678'] },
statistics: query.targets[0].statistics,
period: query.targets[0].period,
}),
])
);
});
it('should generate the correct query with interval variable', async () => {
it('should generate the correct query with interval variable', done => {
const period: CustomVariableModel = {
...initialVariableModelState,
id: 'period',
@ -388,9 +394,10 @@ describe('CloudWatchDatasource', () => {
hide: VariableHide.dontHide,
type: 'custom',
};
const templateSrv = new TemplateSrv();
templateSrv.init([period]);
const query = {
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -409,11 +416,19 @@ describe('CloudWatchDatasource', () => {
],
};
await ctx.ds.query(query).toPromise();
expect(datasourceRequestMock.mock.calls[0][0].data.queries[0].period).toEqual('600');
const { ds, fetchMock } = getTestContext({ response, templateSrv });
observableTester().subscribeAndExpectOnComplete({
observable: ds.query(query),
expect: () => {
expect(fetchMock.mock.calls[0][0].data.queries[0].period).toEqual('600');
},
done,
});
});
it.each(['pNN.NN', 'p9', 'p99.', 'p99.999'])('should cancel query for invalid extended statistics (%s)', stat => {
const { ds } = getTestContext({ response });
const query = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
@ -432,85 +447,94 @@ describe('CloudWatchDatasource', () => {
},
],
};
expect(ctx.ds.query.bind(ctx.ds, query)).toThrow(/Invalid extended statistics/);
expect(ds.query.bind(ds, query)).toThrow(/Invalid extended statistics/);
});
it('should return series list', done => {
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
const { ds } = getTestContext({ response });
observableTester().subscribeAndExpectOnNext({
observable: ds.query(query),
expect: result => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done();
});
},
done,
});
});
describe('a correct cloudwatch url should be built for each time series in the response', () => {
beforeEach(() => {
datasourceRequestMock.mockImplementation(() => {
return Promise.resolve({ data: response });
});
});
it('should be built correctly if theres one search expressions returned in meta for a given query row', done => {
const { ds } = getTestContext({ response });
response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }];
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
observableTester().subscribeAndExpectOnNext({
observable: ds.query(query),
expect: result => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[1].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}`
);
done();
});
},
done,
});
});
it('should be built correctly if theres two search expressions returned in meta for a given query row', done => {
const { ds } = getTestContext({ response });
response.results['A'].meta.gmdMeta = [
{ Expression: `REMOVE_EMPTY(SEARCH('first expression'))` },
{ Expression: `REMOVE_EMPTY(SEARCH('second expression'))` },
];
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
observableTester().subscribeAndExpectOnNext({
observable: ds.query(query),
expect: result => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}`
);
done();
});
},
done,
});
});
it('should be built correctly if the query is a metric stat query', done => {
const { ds } = getTestContext({ response });
response.results['A'].meta.gmdMeta = [{ Period: '300' }];
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
observableTester().subscribeAndExpectOnNext({
observable: ds.query(query),
expect: result => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}`
);
done();
});
},
done,
});
});
it('should not be added at all if query is a math expression', done => {
const { ds } = getTestContext({ response });
query.targets[0].expression = 'a * 2';
response.results['A'].meta.searchExpressions = [];
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
observableTester().subscribeAndExpectOnNext({
observable: ds.query(query),
expect: result => {
expect(result.data[0].fields[1].config.links).toBeUndefined();
done();
});
},
done,
});
});
});
@ -527,7 +551,7 @@ describe('CloudWatchDatasource', () => {
expression: '',
};
const query = {
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -576,55 +600,50 @@ describe('CloudWatchDatasource', () => {
redux.setStore({
dispatch: jest.fn(),
} as any);
datasourceRequestMock.mockImplementation(() => {
return Promise.reject(backendErrorResponse);
});
});
it('should display one alert error message per region+datasource combination', done => {
const memoizedDebounceSpy = jest.spyOn(ctx.ds, 'debouncedAlert');
ctx.ds
.query(query)
.toPromise()
.catch(() => {
const { ds } = getTestContext({ response: backendErrorResponse, throws: true });
const memoizedDebounceSpy = jest.spyOn(ds, 'debouncedAlert');
observableTester().subscribeAndExpectOnError({
observable: ds.query(query),
expect: err => {
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-1');
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-2');
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'eu-north-1');
expect(memoizedDebounceSpy).toBeCalledTimes(3);
done();
});
},
done,
});
});
});
describe('when regions query is used', () => {
beforeEach(() => {
datasourceRequestMock.mockImplementation(() => {
return Promise.resolve({});
});
ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv);
ctx.ds.doMetricQueryRequest = jest.fn(() => []);
});
describe('and region param is left out', () => {
it('should use the default region', done => {
ctx.ds.metricFindQuery('metrics(testNamespace)').then(() => {
expect(ctx.ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', {
namespace: 'testNamespace',
region: instanceSettings.jsonData.defaultRegion,
});
done();
it('should use the default region', async () => {
const { ds, instanceSettings } = getTestContext();
ds.doMetricQueryRequest = jest.fn().mockResolvedValue([]);
await ds.metricFindQuery('metrics(testNamespace)');
expect(ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', {
namespace: 'testNamespace',
region: instanceSettings.jsonData.defaultRegion,
});
});
});
describe('and region param is defined by user', () => {
it('should use the user defined region', done => {
ctx.ds.metricFindQuery('metrics(testNamespace2, custom-region)').then(() => {
expect(ctx.ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', {
namespace: 'testNamespace2',
region: 'custom-region',
});
done();
it('should use the user defined region', async () => {
const { ds } = getTestContext();
ds.doMetricQueryRequest = jest.fn().mockResolvedValue([]);
await ds.metricFindQuery('metrics(testNamespace2, custom-region)');
expect(ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', {
namespace: 'testNamespace2',
region: 'custom-region',
});
});
});
@ -633,27 +652,25 @@ describe('CloudWatchDatasource', () => {
describe('When query region is "default"', () => {
it('should return the datasource region if empty or "default"', () => {
const { ds, instanceSettings } = getTestContext();
const defaultRegion = instanceSettings.jsonData.defaultRegion;
expect(ctx.ds.getActualRegion()).toBe(defaultRegion);
expect(ctx.ds.getActualRegion('')).toBe(defaultRegion);
expect(ctx.ds.getActualRegion('default')).toBe(defaultRegion);
expect(ds.getActualRegion()).toBe(defaultRegion);
expect(ds.getActualRegion('')).toBe(defaultRegion);
expect(ds.getActualRegion('default')).toBe(defaultRegion);
});
it('should return the specified region if specified', () => {
expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1');
});
const { ds } = getTestContext();
let requestParams: { queries: CloudWatchQuery[] };
beforeEach(() => {
ctx.ds.performTimeSeriesQuery = jest.fn(request => {
requestParams = request;
return Promise.resolve({ data: {} });
});
expect(ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1');
});
it('should query for the datasource region if empty or "default"', done => {
const query = {
const { ds, instanceSettings } = getTestContext();
const performTimeSeriesQueryMock = jest.spyOn(ds, 'performTimeSeriesQuery').mockReturnValue(of({}));
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -672,36 +689,29 @@ describe('CloudWatchDatasource', () => {
],
};
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion);
done();
});
observableTester().subscribeAndExpectOnComplete({
observable: ds.query(query),
expect: () => {
expect(performTimeSeriesQueryMock.mock.calls[0][0].queries[0].region).toBe(
instanceSettings.jsonData.defaultRegion
);
},
done,
});
});
});
describe('When interpolating variables', () => {
beforeEach(() => {
jest.clearAllMocks();
ctx.mockedTemplateSrv = {
replace: jest.fn(),
};
ctx.ds = new CloudWatchDatasource(
instanceSettings,
ctx.mockedTemplateSrv,
(new TimeSrvStub() as unknown) as TimeSrv
);
});
it('should return an empty array if no queries are provided', () => {
expect(ctx.ds.interpolateVariablesInQueries([], {})).toHaveLength(0);
const templateSrv: any = { replace: jest.fn() };
const { ds } = getTestContext({ templateSrv });
expect(ds.interpolateVariablesInQueries([], {})).toHaveLength(0);
});
it('should replace correct variables in CloudWatchLogsQuery', () => {
const templateSrv: any = { replace: jest.fn() };
const { ds } = getTestContext({ templateSrv });
const variableName = 'someVar';
const logQuery: CloudWatchLogsQuery = {
id: 'someId',
@ -711,14 +721,16 @@ describe('CloudWatchDatasource', () => {
region: `$${variableName}`,
};
ctx.ds.interpolateVariablesInQueries([logQuery], {});
ds.interpolateVariablesInQueries([logQuery], {});
// We interpolate `expression` and `region` in CloudWatchLogsQuery
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(2);
expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
expect(templateSrv.replace).toHaveBeenCalledTimes(2);
});
it('should replace correct variables in CloudWatchMetricsQuery', () => {
const templateSrv: any = { replace: jest.fn() };
const { ds } = getTestContext({ templateSrv });
const variableName = 'someVar';
const logQuery: CloudWatchMetricsQuery = {
id: 'someId',
@ -737,16 +749,16 @@ describe('CloudWatchDatasource', () => {
statistics: [],
};
ctx.ds.interpolateVariablesInQueries([logQuery], {});
ds.interpolateVariablesInQueries([logQuery], {});
// We interpolate `expression`, `region`, `period`, `alias`, `metricName`, `nameSpace` and `dimensions` in CloudWatchMetricsQuery
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(8);
expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
expect(templateSrv.replace).toHaveBeenCalledTimes(8);
});
});
describe('When performing CloudWatch query for extended statistics', () => {
const query = {
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -797,26 +809,22 @@ describe('CloudWatchDatasource', () => {
},
};
beforeEach(() => {
datasourceRequestMock.mockImplementation(params => {
return Promise.resolve({ data: response });
});
});
it('should return series list', done => {
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
const { ds } = getTestContext({ response });
observableTester().subscribeAndExpectOnNext({
observable: ds.query(query),
expect: result => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done();
});
},
done,
});
});
});
describe('When performing CloudWatch query with template variables', () => {
let requestParams: { queries: CloudWatchMetricsQuery[] };
let templateSrv: TemplateSrv;
beforeEach(() => {
const var1: CustomVariableModel = {
...initialVariableModelState,
@ -880,18 +888,13 @@ describe('CloudWatchDatasource', () => {
};
const variables = [var1, var2, var3, var4];
const state = convertToStoreState(variables);
const _templateSrv = new TemplateSrv(getTemplateSrvDependencies(state));
_templateSrv.init(variables);
ctx.ds = new CloudWatchDatasource(instanceSettings, _templateSrv, timeSrv);
datasourceRequestMock.mockImplementation(params => {
requestParams = params.data;
return Promise.resolve({ data: {} });
});
templateSrv = new TemplateSrv(getTemplateSrvDependencies(state));
templateSrv.init(variables);
});
it('should generate the correct query for single template variable', done => {
const query = {
const { ds, fetchMock } = getTestContext({ templateSrv });
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -910,17 +913,18 @@ describe('CloudWatchDatasource', () => {
],
};
ctx.ds
.query(query)
.toPromise()
.then(() => {
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
done();
});
observableTester().subscribeAndExpectOnComplete({
observable: ds.query(query),
expect: () => {
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
},
done,
});
});
it('should generate the correct query in the case of one multilple template variables', done => {
const query = {
const { ds, fetchMock } = getTestContext({ templateSrv });
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -945,19 +949,20 @@ describe('CloudWatchDatasource', () => {
},
};
ctx.ds
.query(query)
.toPromise()
.then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
done();
});
observableTester().subscribeAndExpectOnComplete({
observable: ds.query(query),
expect: () => {
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
},
done,
});
});
it('should generate the correct query in the case of multilple multi template variables', done => {
const query = {
const { ds, fetchMock } = getTestContext({ templateSrv });
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -978,19 +983,20 @@ describe('CloudWatchDatasource', () => {
],
};
ctx.ds
.query(query)
.toPromise()
.then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
expect(requestParams.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']);
done();
});
observableTester().subscribeAndExpectOnComplete({
observable: ds.query(query),
expect: () => {
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']);
},
done,
});
});
it('should generate the correct query for multilple template variables, lack scopedVars', done => {
const query = {
const { ds, fetchMock } = getTestContext({ templateSrv });
const query: any = {
range: defaultTimeRange,
rangeRaw: { from: 1483228800, to: 1483232400 },
targets: [
@ -1014,15 +1020,15 @@ describe('CloudWatchDatasource', () => {
},
};
ctx.ds
.query(query)
.toPromise()
.then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
done();
});
observableTester().subscribeAndExpectOnComplete({
observable: ds.query(query),
expect: () => {
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
},
done,
});
});
});
@ -1032,11 +1038,8 @@ describe('CloudWatchDatasource', () => {
scenario.setup = async (setupCallback: any) => {
beforeEach(async () => {
await setupCallback();
datasourceRequestMock.mockImplementation(args => {
scenario.request = args.data;
return Promise.resolve({ data: scenario.requestResponse });
});
ctx.ds.metricFindQuery(query).then((args: any) => {
const { ds } = getTestContext({ response: scenario.requestResponse });
ds.metricFindQuery(query).then((args: any) => {
scenario.result = args;
});
});
@ -1210,3 +1213,17 @@ function genMockFrames(numResponses: number): DataFrame[] {
return mockFrames;
}
function createFetchResponse<T>(data: T): FetchResponse<T> {
return {
data,
status: 200,
url: 'http://localhost:3000/api/query',
config: { url: 'http://localhost:3000/api/query' },
type: 'basic',
statusText: 'Ok',
redirected: false,
headers: ({} as unknown) as Headers,
ok: true,
};
}