datasource.test.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  1. import { interval, lastValueFrom, of, throwError } from 'rxjs';
  2. import { createFetchResponse } from 'test/helpers/createFetchResponse';
  3. import { getTemplateSrvDependencies } from 'test/helpers/getTemplateSrvDependencies';
  4. import {
  5. DataFrame,
  6. DataQueryErrorType,
  7. DataSourceInstanceSettings,
  8. dateMath,
  9. getFrameDisplayName,
  10. } from '@grafana/data';
  11. import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
  12. import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  13. import { TemplateSrv } from 'app/features/templating/template_srv';
  14. import * as redux from 'app/store/store';
  15. import { convertToStoreState } from '../../../../../test/helpers/convertToStoreState';
  16. import { CustomVariableModel, initialVariableModelState, VariableHide } from '../../../../features/variables/types';
  17. import { CloudWatchDatasource } from '../datasource';
  18. import {
  19. CloudWatchJsonData,
  20. CloudWatchLogsQuery,
  21. CloudWatchLogsQueryStatus,
  22. CloudWatchMetricsQuery,
  23. LogAction,
  24. MetricEditorMode,
  25. MetricQueryType,
  26. } from '../types';
  27. import * as rxjsUtils from '../utils/rxjs/increasingInterval';
  28. jest.mock('@grafana/runtime', () => ({
  29. ...(jest.requireActual('@grafana/runtime') as unknown as object),
  30. getBackendSrv: () => backendSrv,
  31. }));
  32. type Args = { response?: any; throws?: boolean; templateSrv?: TemplateSrv };
  33. function getTestContext({ response = {}, throws = false, templateSrv = new TemplateSrv() }: Args = {}) {
  34. jest.clearAllMocks();
  35. const fetchMock = jest.spyOn(backendSrv, 'fetch');
  36. throws
  37. ? fetchMock.mockImplementation(() => throwError(response))
  38. : fetchMock.mockImplementation(() => of(createFetchResponse(response)));
  39. const instanceSettings = {
  40. jsonData: { defaultRegion: 'us-east-1' },
  41. name: 'TestDatasource',
  42. } as DataSourceInstanceSettings<CloudWatchJsonData>;
  43. const timeSrv = {
  44. time: { from: '2016-12-31 15:00:00Z', to: '2016-12-31 16:00:00Z' },
  45. timeRange: () => {
  46. return {
  47. from: dateMath.parse(timeSrv.time.from, false),
  48. to: dateMath.parse(timeSrv.time.to, true),
  49. };
  50. },
  51. } as TimeSrv;
  52. const ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv);
  53. return { ds, fetchMock, instanceSettings };
  54. }
  55. describe('CloudWatchDatasource', () => {
  56. const start = 1483196400 * 1000;
  57. const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) };
  58. beforeEach(() => {
  59. jest.clearAllMocks();
  60. });
  61. describe('When getting log groups', () => {
  62. it('should return log groups as an array of strings', async () => {
  63. const response = {
  64. results: {
  65. A: {
  66. frames: [
  67. {
  68. schema: {
  69. name: 'logGroups',
  70. refId: 'A',
  71. fields: [{ name: 'logGroupName', type: 'string', typeInfo: { frame: 'string', nullable: true } }],
  72. },
  73. data: {
  74. values: [
  75. [
  76. '/aws/containerinsights/dev303-workshop/application',
  77. '/aws/containerinsights/dev303-workshop/dataplane',
  78. '/aws/containerinsights/dev303-workshop/flowlogs',
  79. '/aws/containerinsights/dev303-workshop/host',
  80. '/aws/containerinsights/dev303-workshop/performance',
  81. '/aws/containerinsights/dev303-workshop/prometheus',
  82. '/aws/containerinsights/ecommerce-sockshop/application',
  83. '/aws/containerinsights/ecommerce-sockshop/dataplane',
  84. '/aws/containerinsights/ecommerce-sockshop/host',
  85. '/aws/containerinsights/ecommerce-sockshop/performance',
  86. '/aws/containerinsights/watchdemo-perf/application',
  87. '/aws/containerinsights/watchdemo-perf/dataplane',
  88. '/aws/containerinsights/watchdemo-perf/host',
  89. '/aws/containerinsights/watchdemo-perf/performance',
  90. '/aws/containerinsights/watchdemo-perf/prometheus',
  91. '/aws/containerinsights/watchdemo-prod-us-east-1/performance',
  92. '/aws/containerinsights/watchdemo-staging/application',
  93. '/aws/containerinsights/watchdemo-staging/dataplane',
  94. '/aws/containerinsights/watchdemo-staging/host',
  95. '/aws/containerinsights/watchdemo-staging/performance',
  96. '/aws/ecs/containerinsights/bugbash-ec2/performance',
  97. '/aws/ecs/containerinsights/ecs-demoworkshop/performance',
  98. '/aws/ecs/containerinsights/ecs-workshop-dev/performance',
  99. '/aws/eks/dev303-workshop/cluster',
  100. '/aws/events/cloudtrail',
  101. '/aws/events/ecs',
  102. '/aws/lambda/cwsyn-mycanary-fac97ded-f134-499a-9d71-4c3be1f63182',
  103. '/aws/lambda/cwsyn-watch-linkchecks-ef7ef273-5da2-4663-af54-d2f52d55b060',
  104. '/ecs/ecs-cwagent-daemon-service',
  105. '/ecs/ecs-demo-limitTask',
  106. 'CloudTrail/DefaultLogGroup',
  107. 'container-insights-prometheus-beta',
  108. 'container-insights-prometheus-demo',
  109. ],
  110. ],
  111. },
  112. },
  113. ],
  114. },
  115. },
  116. };
  117. const { ds } = getTestContext({ response });
  118. const expectedLogGroups = [
  119. '/aws/containerinsights/dev303-workshop/application',
  120. '/aws/containerinsights/dev303-workshop/dataplane',
  121. '/aws/containerinsights/dev303-workshop/flowlogs',
  122. '/aws/containerinsights/dev303-workshop/host',
  123. '/aws/containerinsights/dev303-workshop/performance',
  124. '/aws/containerinsights/dev303-workshop/prometheus',
  125. '/aws/containerinsights/ecommerce-sockshop/application',
  126. '/aws/containerinsights/ecommerce-sockshop/dataplane',
  127. '/aws/containerinsights/ecommerce-sockshop/host',
  128. '/aws/containerinsights/ecommerce-sockshop/performance',
  129. '/aws/containerinsights/watchdemo-perf/application',
  130. '/aws/containerinsights/watchdemo-perf/dataplane',
  131. '/aws/containerinsights/watchdemo-perf/host',
  132. '/aws/containerinsights/watchdemo-perf/performance',
  133. '/aws/containerinsights/watchdemo-perf/prometheus',
  134. '/aws/containerinsights/watchdemo-prod-us-east-1/performance',
  135. '/aws/containerinsights/watchdemo-staging/application',
  136. '/aws/containerinsights/watchdemo-staging/dataplane',
  137. '/aws/containerinsights/watchdemo-staging/host',
  138. '/aws/containerinsights/watchdemo-staging/performance',
  139. '/aws/ecs/containerinsights/bugbash-ec2/performance',
  140. '/aws/ecs/containerinsights/ecs-demoworkshop/performance',
  141. '/aws/ecs/containerinsights/ecs-workshop-dev/performance',
  142. '/aws/eks/dev303-workshop/cluster',
  143. '/aws/events/cloudtrail',
  144. '/aws/events/ecs',
  145. '/aws/lambda/cwsyn-mycanary-fac97ded-f134-499a-9d71-4c3be1f63182',
  146. '/aws/lambda/cwsyn-watch-linkchecks-ef7ef273-5da2-4663-af54-d2f52d55b060',
  147. '/ecs/ecs-cwagent-daemon-service',
  148. '/ecs/ecs-demo-limitTask',
  149. 'CloudTrail/DefaultLogGroup',
  150. 'container-insights-prometheus-beta',
  151. 'container-insights-prometheus-demo',
  152. ];
  153. const logGroups = await ds.describeLogGroups({ region: 'default' });
  154. expect(logGroups).toEqual(expectedLogGroups);
  155. });
  156. });
  157. describe('When performing CloudWatch logs query', () => {
  158. beforeEach(() => {
  159. jest.spyOn(rxjsUtils, 'increasingInterval').mockImplementation(() => interval(100));
  160. });
  161. it('should stop querying when timed out', async () => {
  162. const { ds } = getTestContext();
  163. const fakeFrames = genMockFrames(20);
  164. const initialRecordsMatched = fakeFrames[0].meta!.stats!.find((stat) => stat.displayName === 'Records scanned')!
  165. .value!;
  166. for (let i = 1; i < 4; i++) {
  167. fakeFrames[i].meta!.stats = [
  168. {
  169. displayName: 'Records scanned',
  170. value: initialRecordsMatched,
  171. },
  172. ];
  173. }
  174. const finalRecordsMatched = fakeFrames[9].meta!.stats!.find((stat) => stat.displayName === 'Records scanned')!
  175. .value!;
  176. for (let i = 10; i < fakeFrames.length; i++) {
  177. fakeFrames[i].meta!.stats = [
  178. {
  179. displayName: 'Records scanned',
  180. value: finalRecordsMatched,
  181. },
  182. ];
  183. }
  184. let i = 0;
  185. jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
  186. if (subtype === 'GetQueryResults') {
  187. const mockObservable = of([fakeFrames[i]]);
  188. i++;
  189. return mockObservable;
  190. } else {
  191. return of([]);
  192. }
  193. });
  194. const iterations = 15;
  195. // Times out after 15 passes for consistent testing
  196. const timeoutFunc = () => {
  197. return i >= iterations;
  198. };
  199. const myResponse = await lastValueFrom(
  200. ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
  201. );
  202. const expectedData = [
  203. {
  204. ...fakeFrames[14],
  205. meta: {
  206. custom: {
  207. Status: 'Cancelled',
  208. },
  209. stats: fakeFrames[14].meta!.stats,
  210. },
  211. },
  212. ];
  213. expect(myResponse).toEqual({
  214. data: expectedData,
  215. key: 'test-key',
  216. state: 'Done',
  217. error: {
  218. type: DataQueryErrorType.Timeout,
  219. message: `error: query timed out after 5 attempts`,
  220. },
  221. });
  222. expect(i).toBe(iterations);
  223. });
  224. it('should continue querying as long as new data is being received', async () => {
  225. const { ds } = getTestContext();
  226. const fakeFrames = genMockFrames(15);
  227. let i = 0;
  228. jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
  229. if (subtype === 'GetQueryResults') {
  230. const mockObservable = of([fakeFrames[i]]);
  231. i++;
  232. return mockObservable;
  233. } else {
  234. return of([]);
  235. }
  236. });
  237. const startTime = new Date();
  238. const timeoutFunc = () => {
  239. return Date.now() >= startTime.valueOf() + 6000;
  240. };
  241. const myResponse = await lastValueFrom(
  242. ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
  243. );
  244. expect(myResponse).toEqual({
  245. data: [fakeFrames[fakeFrames.length - 1]],
  246. key: 'test-key',
  247. state: 'Done',
  248. });
  249. expect(i).toBe(15);
  250. });
  251. it('should stop querying when results come back with status "Complete"', async () => {
  252. const { ds } = getTestContext();
  253. const fakeFrames = genMockFrames(3);
  254. let i = 0;
  255. jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => {
  256. if (subtype === 'GetQueryResults') {
  257. const mockObservable = of([fakeFrames[i]]);
  258. i++;
  259. return mockObservable;
  260. } else {
  261. return of([]);
  262. }
  263. });
  264. const startTime = new Date();
  265. const timeoutFunc = () => {
  266. return Date.now() >= startTime.valueOf() + 6000;
  267. };
  268. const myResponse = await lastValueFrom(
  269. ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }], timeoutFunc)
  270. );
  271. expect(myResponse).toEqual({
  272. data: [fakeFrames[2]],
  273. key: 'test-key',
  274. state: 'Done',
  275. });
  276. expect(i).toBe(3);
  277. });
  278. });
  279. describe('When performing CloudWatch metrics query', () => {
  280. const query: any = {
  281. range: defaultTimeRange,
  282. rangeRaw: { from: 1483228800, to: 1483232400 },
  283. targets: [
  284. {
  285. metricQueryType: MetricQueryType.Search,
  286. metricEditorMode: MetricEditorMode.Builder,
  287. type: 'Metrics',
  288. expression: '',
  289. refId: 'A',
  290. region: 'us-east-1',
  291. namespace: 'AWS/EC2',
  292. metricName: 'CPUUtilization',
  293. dimensions: {
  294. InstanceId: 'i-12345678',
  295. },
  296. statistic: 'Average',
  297. period: '300',
  298. },
  299. ],
  300. };
  301. const response: any = {
  302. timings: [null],
  303. results: {
  304. A: {
  305. type: 'Metrics',
  306. error: '',
  307. refId: 'A',
  308. meta: {},
  309. series: [
  310. {
  311. name: 'CPUUtilization_Average',
  312. points: [
  313. [1, 1483228800000],
  314. [2, 1483229100000],
  315. [5, 1483229700000],
  316. ],
  317. tags: {
  318. InstanceId: 'i-12345678',
  319. },
  320. },
  321. ],
  322. },
  323. },
  324. };
  325. it('should generate the correct query', async () => {
  326. const { ds, fetchMock } = getTestContext({ response });
  327. await expect(ds.query(query)).toEmitValuesWith(() => {
  328. expect(fetchMock.mock.calls[0][0].data.queries).toMatchObject(
  329. expect.arrayContaining([
  330. expect.objectContaining({
  331. namespace: query.targets[0].namespace,
  332. metricName: query.targets[0].metricName,
  333. dimensions: { InstanceId: ['i-12345678'] },
  334. statistic: query.targets[0].statistic,
  335. period: query.targets[0].period,
  336. }),
  337. ])
  338. );
  339. });
  340. });
  341. it('should generate the correct query with interval variable', async () => {
  342. const period: CustomVariableModel = {
  343. ...initialVariableModelState,
  344. id: 'period',
  345. name: 'period',
  346. index: 0,
  347. current: { value: '10m', text: '10m', selected: true },
  348. options: [{ value: '10m', text: '10m', selected: true }],
  349. multi: false,
  350. includeAll: false,
  351. query: '',
  352. hide: VariableHide.dontHide,
  353. type: 'custom',
  354. };
  355. const templateSrv = new TemplateSrv();
  356. templateSrv.init([period]);
  357. const query: any = {
  358. range: defaultTimeRange,
  359. rangeRaw: { from: 1483228800, to: 1483232400 },
  360. targets: [
  361. {
  362. metricQueryType: MetricQueryType.Search,
  363. metricEditorMode: MetricEditorMode.Builder,
  364. type: 'Metrics',
  365. refId: 'A',
  366. region: 'us-east-1',
  367. namespace: 'AWS/EC2',
  368. metricName: 'CPUUtilization',
  369. dimensions: {
  370. InstanceId: 'i-12345678',
  371. },
  372. statistic: 'Average',
  373. period: '[[period]]',
  374. },
  375. ],
  376. };
  377. const { ds, fetchMock } = getTestContext({ response, templateSrv });
  378. await expect(ds.query(query)).toEmitValuesWith(() => {
  379. expect(fetchMock.mock.calls[0][0].data.queries[0].period).toEqual('600');
  380. });
  381. });
  382. it('should return series list', async () => {
  383. const { ds } = getTestContext({ response });
  384. await expect(ds.query(query)).toEmitValuesWith((received) => {
  385. const result = received[0];
  386. expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
  387. expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
  388. });
  389. });
  390. describe('and throttling exception is thrown', () => {
  391. const partialQuery = {
  392. metricQueryType: MetricQueryType.Search,
  393. metricEditorMode: MetricEditorMode.Builder,
  394. type: 'Metrics',
  395. namespace: 'AWS/EC2',
  396. metricName: 'CPUUtilization',
  397. dimensions: {
  398. InstanceId: 'i-12345678',
  399. },
  400. statistic: 'Average',
  401. period: '300',
  402. expression: '',
  403. };
  404. const query: any = {
  405. range: defaultTimeRange,
  406. rangeRaw: { from: 1483228800, to: 1483232400 },
  407. targets: [
  408. { ...partialQuery, refId: 'A', region: 'us-east-1' },
  409. { ...partialQuery, refId: 'B', region: 'us-east-2' },
  410. { ...partialQuery, refId: 'C', region: 'us-east-1' },
  411. { ...partialQuery, refId: 'D', region: 'us-east-2' },
  412. { ...partialQuery, refId: 'E', region: 'eu-north-1' },
  413. ],
  414. };
  415. const backendErrorResponse = {
  416. data: {
  417. message: 'Throttling: exception',
  418. results: {
  419. A: {
  420. error: 'Throttling: exception',
  421. refId: 'A',
  422. meta: {},
  423. },
  424. B: {
  425. error: 'Throttling: exception',
  426. refId: 'B',
  427. meta: {},
  428. },
  429. C: {
  430. error: 'Throttling: exception',
  431. refId: 'C',
  432. meta: {},
  433. },
  434. D: {
  435. error: 'Throttling: exception',
  436. refId: 'D',
  437. meta: {},
  438. },
  439. E: {
  440. error: 'Throttling: exception',
  441. refId: 'E',
  442. meta: {},
  443. },
  444. },
  445. },
  446. };
  447. beforeEach(() => {
  448. redux.setStore({
  449. dispatch: jest.fn(),
  450. } as any);
  451. });
  452. it('should display one alert error message per region+datasource combination', async () => {
  453. const { ds } = getTestContext({ response: backendErrorResponse, throws: true });
  454. const memoizedDebounceSpy = jest.spyOn(ds, 'debouncedAlert');
  455. await expect(ds.query(query)).toEmitValuesWith((received) => {
  456. expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-1');
  457. expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-2');
  458. expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'eu-north-1');
  459. expect(memoizedDebounceSpy).toBeCalledTimes(3);
  460. });
  461. });
  462. });
  463. });
  464. describe('When query region is "default"', () => {
  465. it('should return the datasource region if empty or "default"', () => {
  466. const { ds, instanceSettings } = getTestContext();
  467. const defaultRegion = instanceSettings.jsonData.defaultRegion;
  468. expect(ds.getActualRegion()).toBe(defaultRegion);
  469. expect(ds.getActualRegion('')).toBe(defaultRegion);
  470. expect(ds.getActualRegion('default')).toBe(defaultRegion);
  471. });
  472. it('should return the specified region if specified', () => {
  473. const { ds } = getTestContext();
  474. expect(ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1');
  475. });
  476. it('should query for the datasource region if empty or "default"', async () => {
  477. const { ds, instanceSettings } = getTestContext();
  478. const performTimeSeriesQueryMock = jest.spyOn(ds, 'performTimeSeriesQuery').mockReturnValue(of({}));
  479. const query: any = {
  480. range: defaultTimeRange,
  481. rangeRaw: { from: 1483228800, to: 1483232400 },
  482. targets: [
  483. {
  484. metricQueryType: MetricQueryType.Search,
  485. metricEditorMode: MetricEditorMode.Builder,
  486. type: 'Metrics',
  487. refId: 'A',
  488. region: 'default',
  489. namespace: 'AWS/EC2',
  490. metricName: 'CPUUtilization',
  491. dimensions: {
  492. InstanceId: 'i-12345678',
  493. },
  494. statistic: 'Average',
  495. period: '300s',
  496. },
  497. ],
  498. };
  499. await expect(ds.query(query)).toEmitValuesWith(() => {
  500. expect(performTimeSeriesQueryMock.mock.calls[0][0].queries[0].region).toBe(
  501. instanceSettings.jsonData.defaultRegion
  502. );
  503. });
  504. });
  505. });
  506. describe('When interpolating variables', () => {
  507. it('should return an empty array if no queries are provided', () => {
  508. const templateSrv: any = { replace: jest.fn(), getVariables: () => [] };
  509. const { ds } = getTestContext({ templateSrv });
  510. expect(ds.interpolateVariablesInQueries([], {})).toHaveLength(0);
  511. });
  512. it('should replace correct variables in CloudWatchLogsQuery', () => {
  513. const templateSrv: any = { replace: jest.fn(), getVariables: () => [] };
  514. const { ds } = getTestContext({ templateSrv });
  515. const variableName = 'someVar';
  516. const logQuery: CloudWatchLogsQuery = {
  517. id: 'someId',
  518. refId: 'someRefId',
  519. queryMode: 'Logs',
  520. expression: `$${variableName}`,
  521. region: `$${variableName}`,
  522. };
  523. ds.interpolateVariablesInQueries([logQuery], {});
  524. // We interpolate `region` in CloudWatchLogsQuery
  525. expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
  526. expect(templateSrv.replace).toHaveBeenCalledTimes(1);
  527. });
  528. it('should replace correct variables in CloudWatchMetricsQuery', () => {
  529. const templateSrv: any = {
  530. replace: jest.fn(),
  531. getVariables: () => [],
  532. getVariableName: jest.fn((name: string) => name),
  533. };
  534. const { ds } = getTestContext({ templateSrv });
  535. const variableName = 'someVar';
  536. const logQuery: CloudWatchMetricsQuery = {
  537. queryMode: 'Metrics',
  538. id: 'someId',
  539. refId: 'someRefId',
  540. expression: `$${variableName}`,
  541. region: `$${variableName}`,
  542. period: `$${variableName}`,
  543. alias: `$${variableName}`,
  544. metricName: `$${variableName}`,
  545. namespace: `$${variableName}`,
  546. dimensions: {
  547. [`$${variableName}`]: `$${variableName}`,
  548. },
  549. matchExact: false,
  550. statistic: '',
  551. sqlExpression: `$${variableName}`,
  552. };
  553. ds.interpolateVariablesInQueries([logQuery], {});
  554. // We interpolate `expression`, `region`, `period`, `alias`, `metricName`, and `nameSpace` in CloudWatchMetricsQuery
  555. expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
  556. expect(templateSrv.replace).toHaveBeenCalledTimes(7);
  557. expect(templateSrv.getVariableName).toHaveBeenCalledWith(`$${variableName}`);
  558. expect(templateSrv.getVariableName).toHaveBeenCalledTimes(1);
  559. });
  560. });
  561. describe('When performing CloudWatch query for extended statistic', () => {
  562. const query: any = {
  563. range: defaultTimeRange,
  564. rangeRaw: { from: 1483228800, to: 1483232400 },
  565. targets: [
  566. {
  567. metricQueryType: MetricQueryType.Search,
  568. metricEditorMode: MetricEditorMode.Builder,
  569. type: 'Metrics',
  570. refId: 'A',
  571. region: 'us-east-1',
  572. namespace: 'AWS/ApplicationELB',
  573. metricName: 'TargetResponseTime',
  574. dimensions: {
  575. LoadBalancer: 'lb',
  576. TargetGroup: 'tg',
  577. },
  578. statistic: 'p90.00',
  579. period: '300s',
  580. },
  581. ],
  582. };
  583. const response: any = {
  584. timings: [null],
  585. results: {
  586. A: {
  587. error: '',
  588. refId: 'A',
  589. meta: {},
  590. series: [
  591. {
  592. name: 'TargetResponseTime_p90.00',
  593. points: [
  594. [1, 1483228800000],
  595. [2, 1483229100000],
  596. [5, 1483229700000],
  597. ],
  598. tags: {
  599. LoadBalancer: 'lb',
  600. TargetGroup: 'tg',
  601. },
  602. },
  603. ],
  604. },
  605. },
  606. };
  607. it('should return series list', async () => {
  608. const { ds } = getTestContext({ response });
  609. await expect(ds.query(query)).toEmitValuesWith((received) => {
  610. const result = received[0];
  611. expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
  612. expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
  613. });
  614. });
  615. });
  616. describe('When performing CloudWatch query with template variables', () => {
  617. let templateSrv: TemplateSrv;
  618. beforeEach(() => {
  619. const key = 'key';
  620. const var1: CustomVariableModel = {
  621. ...initialVariableModelState,
  622. id: 'var1',
  623. rootStateKey: key,
  624. name: 'var1',
  625. index: 0,
  626. current: { value: 'var1-foo', text: 'var1-foo', selected: true },
  627. options: [{ value: 'var1-foo', text: 'var1-foo', selected: true }],
  628. multi: false,
  629. includeAll: false,
  630. query: '',
  631. hide: VariableHide.dontHide,
  632. type: 'custom',
  633. };
  634. const var2: CustomVariableModel = {
  635. ...initialVariableModelState,
  636. id: 'var2',
  637. rootStateKey: key,
  638. name: 'var2',
  639. index: 1,
  640. current: { value: 'var2-foo', text: 'var2-foo', selected: true },
  641. options: [{ value: 'var2-foo', text: 'var2-foo', selected: true }],
  642. multi: false,
  643. includeAll: false,
  644. query: '',
  645. hide: VariableHide.dontHide,
  646. type: 'custom',
  647. };
  648. const var3: CustomVariableModel = {
  649. ...initialVariableModelState,
  650. id: 'var3',
  651. rootStateKey: key,
  652. name: 'var3',
  653. index: 2,
  654. current: { value: ['var3-foo', 'var3-baz'], text: 'var3-foo + var3-baz', selected: true },
  655. options: [
  656. { selected: true, value: 'var3-foo', text: 'var3-foo' },
  657. { selected: false, value: 'var3-bar', text: 'var3-bar' },
  658. { selected: true, value: 'var3-baz', text: 'var3-baz' },
  659. ],
  660. multi: true,
  661. includeAll: false,
  662. query: '',
  663. hide: VariableHide.dontHide,
  664. type: 'custom',
  665. };
  666. const var4: CustomVariableModel = {
  667. ...initialVariableModelState,
  668. id: 'var4',
  669. rootStateKey: key,
  670. name: 'var4',
  671. index: 3,
  672. options: [
  673. { selected: true, value: 'var4-foo', text: 'var4-foo' },
  674. { selected: false, value: 'var4-bar', text: 'var4-bar' },
  675. { selected: true, value: 'var4-baz', text: 'var4-baz' },
  676. ],
  677. current: { value: ['var4-foo', 'var4-baz'], text: 'var4-foo + var4-baz', selected: true },
  678. multi: true,
  679. includeAll: false,
  680. query: '',
  681. hide: VariableHide.dontHide,
  682. type: 'custom',
  683. };
  684. const variables = [var1, var2, var3, var4];
  685. const state = convertToStoreState(key, variables);
  686. templateSrv = new TemplateSrv(getTemplateSrvDependencies(state));
  687. templateSrv.init(variables);
  688. });
  689. it('should generate the correct query for single template variable', async () => {
  690. const { ds, fetchMock } = getTestContext({ templateSrv });
  691. const query: any = {
  692. range: defaultTimeRange,
  693. rangeRaw: { from: 1483228800, to: 1483232400 },
  694. targets: [
  695. {
  696. metricQueryType: MetricQueryType.Search,
  697. metricEditorMode: MetricEditorMode.Builder,
  698. type: 'Metrics',
  699. refId: 'A',
  700. region: 'us-east-1',
  701. namespace: 'TestNamespace',
  702. metricName: 'TestMetricName',
  703. dimensions: {
  704. dim2: '[[var2]]',
  705. },
  706. statistic: 'Average',
  707. period: '300s',
  708. },
  709. ],
  710. };
  711. await expect(ds.query(query)).toEmitValuesWith(() => {
  712. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
  713. });
  714. });
  715. it('should generate the correct query in the case of one multilple template variables', async () => {
  716. const { ds, fetchMock } = getTestContext({ templateSrv });
  717. const query: any = {
  718. range: defaultTimeRange,
  719. rangeRaw: { from: 1483228800, to: 1483232400 },
  720. targets: [
  721. {
  722. metricQueryType: MetricQueryType.Search,
  723. metricEditorMode: MetricEditorMode.Builder,
  724. type: 'Metrics',
  725. refId: 'A',
  726. region: 'us-east-1',
  727. namespace: 'TestNamespace',
  728. metricName: 'TestMetricName',
  729. dimensions: {
  730. dim1: '[[var1]]',
  731. dim2: '[[var2]]',
  732. dim3: '[[var3]]',
  733. },
  734. statistic: 'Average',
  735. period: '300s',
  736. },
  737. ],
  738. scopedVars: {
  739. var1: { selected: true, value: 'var1-foo' },
  740. var2: { selected: true, value: 'var2-foo' },
  741. },
  742. };
  743. await expect(ds.query(query)).toEmitValuesWith(() => {
  744. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
  745. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
  746. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
  747. });
  748. });
  749. it('should generate the correct query in the case of multilple multi template variables', async () => {
  750. const { ds, fetchMock } = getTestContext({ templateSrv });
  751. const query: any = {
  752. range: defaultTimeRange,
  753. rangeRaw: { from: 1483228800, to: 1483232400 },
  754. targets: [
  755. {
  756. metricQueryType: MetricQueryType.Search,
  757. metricEditorMode: MetricEditorMode.Builder,
  758. type: 'Metrics',
  759. refId: 'A',
  760. region: 'us-east-1',
  761. namespace: 'TestNamespace',
  762. metricName: 'TestMetricName',
  763. dimensions: {
  764. dim1: '[[var1]]',
  765. dim3: '[[var3]]',
  766. dim4: '[[var4]]',
  767. },
  768. statistic: 'Average',
  769. period: '300s',
  770. },
  771. ],
  772. };
  773. await expect(ds.query(query)).toEmitValuesWith(() => {
  774. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
  775. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
  776. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']);
  777. });
  778. });
  779. it('should generate the correct query for multilple template variables, lack scopedVars', async () => {
  780. const { ds, fetchMock } = getTestContext({ templateSrv });
  781. const query: any = {
  782. range: defaultTimeRange,
  783. rangeRaw: { from: 1483228800, to: 1483232400 },
  784. targets: [
  785. {
  786. metricQueryType: MetricQueryType.Search,
  787. metricEditorMode: MetricEditorMode.Builder,
  788. type: 'Metrics',
  789. refId: 'A',
  790. region: 'us-east-1',
  791. namespace: 'TestNamespace',
  792. metricName: 'TestMetricName',
  793. dimensions: {
  794. dim1: '[[var1]]',
  795. dim2: '[[var2]]',
  796. dim3: '[[var3]]',
  797. },
  798. statistic: 'Average',
  799. period: '300',
  800. },
  801. ],
  802. scopedVars: {
  803. var1: { selected: true, value: 'var1-foo' },
  804. },
  805. };
  806. await expect(ds.query(query)).toEmitValuesWith(() => {
  807. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
  808. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
  809. expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
  810. });
  811. });
  812. });
  813. });
  814. function genMockFrames(numResponses: number): DataFrame[] {
  815. const recordIncrement = 50;
  816. const mockFrames: DataFrame[] = [];
  817. for (let i = 0; i < numResponses; i++) {
  818. mockFrames.push({
  819. fields: [],
  820. meta: {
  821. custom: {
  822. Status: i === numResponses - 1 ? CloudWatchLogsQueryStatus.Complete : CloudWatchLogsQueryStatus.Running,
  823. },
  824. stats: [
  825. {
  826. displayName: 'Records scanned',
  827. value: (i + 1) * recordIncrement,
  828. },
  829. ],
  830. },
  831. refId: 'A',
  832. length: 0,
  833. });
  834. }
  835. return mockFrames;
  836. }