datasource.test.ts 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. import { map } from 'lodash';
  2. import { Observable, of, throwError } from 'rxjs';
  3. import {
  4. ArrayVector,
  5. CoreApp,
  6. DataLink,
  7. DataQueryRequest,
  8. DataSourceInstanceSettings,
  9. DataSourcePluginMeta,
  10. dateMath,
  11. DateTime,
  12. dateTime,
  13. Field,
  14. MutableDataFrame,
  15. TimeRange,
  16. toUtc,
  17. } from '@grafana/data';
  18. import { BackendSrvRequest, FetchResponse } from '@grafana/runtime';
  19. import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
  20. import { createFetchResponse } from '../../../../test/helpers/createFetchResponse';
  21. import { Filters } from './components/QueryEditor/BucketAggregationsEditor/aggregations';
  22. import { ElasticDatasource, enhanceDataFrame } from './datasource';
  23. import { ElasticsearchOptions, ElasticsearchQuery } from './types';
  24. const ELASTICSEARCH_MOCK_URL = 'http://elasticsearch.local';
  25. jest.mock('@grafana/runtime', () => ({
  26. ...(jest.requireActual('@grafana/runtime') as unknown as object),
  27. getBackendSrv: () => backendSrv,
  28. getDataSourceSrv: () => {
  29. return {
  30. getInstanceSettings: () => {
  31. return { name: 'elastic25' };
  32. },
  33. };
  34. },
  35. }));
  36. const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({
  37. from,
  38. to,
  39. raw: {
  40. from,
  41. to,
  42. },
  43. });
  44. interface Args {
  45. data?: any;
  46. from?: string;
  47. jsonData?: any;
  48. database?: string;
  49. mockImplementation?: (options: BackendSrvRequest) => Observable<FetchResponse>;
  50. }
  51. function getTestContext({
  52. data = {},
  53. from = 'now-5m',
  54. jsonData = {},
  55. database = '[asd-]YYYY.MM.DD',
  56. mockImplementation = undefined,
  57. }: Args = {}) {
  58. jest.clearAllMocks();
  59. const defaultMock = (options: BackendSrvRequest) => of(createFetchResponse(data));
  60. const fetchMock = jest.spyOn(backendSrv, 'fetch');
  61. fetchMock.mockImplementation(mockImplementation ?? defaultMock);
  62. const templateSrv: any = {
  63. replace: jest.fn((text?: string) => {
  64. if (text?.startsWith('$')) {
  65. return `resolvedVariable`;
  66. } else {
  67. return text;
  68. }
  69. }),
  70. getAdhocFilters: jest.fn(() => []),
  71. };
  72. const timeSrv: any = {
  73. time: { from, to: 'now' },
  74. };
  75. timeSrv.timeRange = jest.fn(() => {
  76. return {
  77. from: dateMath.parse(timeSrv.time.from, false),
  78. to: dateMath.parse(timeSrv.time.to, true),
  79. };
  80. });
  81. timeSrv.setTime = jest.fn((time) => {
  82. timeSrv.time = time;
  83. });
  84. const instanceSettings: DataSourceInstanceSettings<ElasticsearchOptions> = {
  85. id: 1,
  86. meta: {} as DataSourcePluginMeta,
  87. name: 'test-elastic',
  88. type: 'type',
  89. uid: 'uid',
  90. access: 'proxy',
  91. url: ELASTICSEARCH_MOCK_URL,
  92. database,
  93. jsonData,
  94. };
  95. const ds = new ElasticDatasource(instanceSettings, templateSrv);
  96. return { timeSrv, ds, fetchMock };
  97. }
  98. describe('ElasticDatasource', function (this: any) {
  99. describe('When testing datasource with index pattern', () => {
  100. it('should translate index pattern to current day', () => {
  101. const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily', esVersion: '7.10.0' } });
  102. ds.testDatasource();
  103. const today = toUtc().format('YYYY.MM.DD');
  104. expect(fetchMock).toHaveBeenCalledTimes(1);
  105. expect(fetchMock.mock.calls[0][0].url).toBe(`${ELASTICSEARCH_MOCK_URL}/asd-${today}/_mapping`);
  106. });
  107. });
  108. describe('When issuing metric query with interval pattern', () => {
  109. async function runScenario() {
  110. const range = { from: toUtc([2015, 4, 30, 10]), to: toUtc([2015, 5, 1, 10]) };
  111. const targets = [
  112. {
  113. alias: '$varAlias',
  114. bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
  115. metrics: [{ type: 'count', id: '1' }],
  116. query: 'escape\\:test',
  117. },
  118. ];
  119. const query: any = { range, targets };
  120. const data = {
  121. responses: [
  122. {
  123. aggregations: {
  124. '1': {
  125. buckets: [
  126. {
  127. doc_count: 10,
  128. key: 1000,
  129. },
  130. ],
  131. },
  132. },
  133. },
  134. ],
  135. };
  136. const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily', esVersion: '7.10.0' }, data });
  137. let result: any = {};
  138. await expect(ds.query(query)).toEmitValuesWith((received) => {
  139. expect(received.length).toBe(1);
  140. expect(received[0]).toEqual({
  141. data: [
  142. {
  143. datapoints: [[10, 1000]],
  144. metric: 'count',
  145. props: {},
  146. refId: undefined,
  147. target: 'resolvedVariable',
  148. },
  149. ],
  150. });
  151. result = received[0];
  152. });
  153. expect(fetchMock).toHaveBeenCalledTimes(1);
  154. const requestOptions = fetchMock.mock.calls[0][0];
  155. const parts = requestOptions.data.split('\n');
  156. const header = JSON.parse(parts[0]);
  157. const body = JSON.parse(parts[1]);
  158. return { result, body, header, query };
  159. }
  160. it('should translate index pattern to current day', async () => {
  161. const { header } = await runScenario();
  162. expect(header.index).toEqual(['asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']);
  163. });
  164. it('should not resolve the variable in the original alias field in the query', async () => {
  165. const { query } = await runScenario();
  166. expect(query.targets[0].alias).toEqual('$varAlias');
  167. });
  168. it('should resolve the alias variable for the alias/target in the result', async () => {
  169. const { result } = await runScenario();
  170. expect(result.data[0].target).toEqual('resolvedVariable');
  171. });
  172. it('should json escape lucene query', async () => {
  173. const { body } = await runScenario();
  174. expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test');
  175. });
  176. });
  177. describe('When issuing logs query with interval pattern', () => {
  178. async function setupDataSource(jsonData?: Partial<ElasticsearchOptions>) {
  179. jsonData = {
  180. interval: 'Daily',
  181. esVersion: '7.10.0',
  182. timeField: '@timestamp',
  183. ...(jsonData || {}),
  184. };
  185. const { ds } = getTestContext({
  186. jsonData,
  187. data: logsResponse.data,
  188. database: 'mock-index',
  189. });
  190. const query: DataQueryRequest<ElasticsearchQuery> = {
  191. range: createTimeRange(toUtc([2015, 4, 30, 10]), toUtc([2019, 7, 1, 10])),
  192. targets: [
  193. {
  194. alias: '$varAlias',
  195. refId: 'A',
  196. bucketAggs: [
  197. {
  198. type: 'date_histogram',
  199. settings: { interval: 'auto' },
  200. id: '2',
  201. },
  202. ],
  203. metrics: [{ type: 'logs', id: '1' }],
  204. query: 'escape\\:test',
  205. timeField: '@timestamp',
  206. },
  207. ],
  208. } as DataQueryRequest<ElasticsearchQuery>;
  209. const queryBuilderSpy = jest.spyOn(ds.queryBuilder, 'getLogsQuery');
  210. let response: any = {};
  211. await expect(ds.query(query)).toEmitValuesWith((received) => {
  212. expect(received.length).toBe(1);
  213. response = received[0];
  214. });
  215. return { queryBuilderSpy, response };
  216. }
  217. it('should call getLogsQuery()', async () => {
  218. const { queryBuilderSpy } = await setupDataSource();
  219. expect(queryBuilderSpy).toHaveBeenCalled();
  220. });
  221. it('should enhance fields with links', async () => {
  222. const { response } = await setupDataSource({
  223. dataLinks: [
  224. {
  225. field: 'host',
  226. url: 'http://localhost:3000/${__value.raw}',
  227. urlDisplayLabel: 'Custom Label',
  228. },
  229. ],
  230. });
  231. expect(response.data.length).toBe(1);
  232. const links: DataLink[] = response.data[0].fields.find((field: Field) => field.name === 'host').config.links;
  233. expect(links.length).toBe(1);
  234. expect(links[0].url).toBe('http://localhost:3000/${__value.raw}');
  235. expect(links[0].title).toBe('Custom Label');
  236. });
  237. });
  238. describe('When issuing document query', () => {
  239. async function runScenario() {
  240. const range = createTimeRange(dateTime([2015, 4, 30, 10]), dateTime([2015, 5, 1, 10]));
  241. const targets = [{ refId: 'A', metrics: [{ type: 'raw_document', id: '1' }], query: 'test' }];
  242. const query: any = { range, targets };
  243. const data = { responses: [] };
  244. const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: '7.10.0' }, data, database: 'test' });
  245. await expect(ds.query(query)).toEmitValuesWith((received) => {
  246. expect(received.length).toBe(1);
  247. expect(received[0]).toEqual({ data: [] });
  248. });
  249. expect(fetchMock).toHaveBeenCalledTimes(1);
  250. const requestOptions = fetchMock.mock.calls[0][0];
  251. const parts = requestOptions.data.split('\n');
  252. const header = JSON.parse(parts[0]);
  253. const body = JSON.parse(parts[1]);
  254. return { body, header };
  255. }
  256. it('should set search type to query_then_fetch', async () => {
  257. const { header } = await runScenario();
  258. expect(header.search_type).toEqual('query_then_fetch');
  259. });
  260. it('should set size', async () => {
  261. const { body } = await runScenario();
  262. expect(body.size).toBe(500);
  263. });
  264. });
  265. describe('When getting an error on response', () => {
  266. const query: DataQueryRequest<ElasticsearchQuery> = {
  267. range: createTimeRange(toUtc([2020, 1, 1, 10]), toUtc([2020, 2, 1, 10])),
  268. targets: [
  269. {
  270. refId: 'A',
  271. alias: '$varAlias',
  272. bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
  273. metrics: [{ type: 'count', id: '1' }],
  274. query: 'escape\\:test',
  275. },
  276. ],
  277. } as DataQueryRequest<ElasticsearchQuery>;
  278. it('should process it properly', async () => {
  279. const { ds } = getTestContext({
  280. jsonData: { interval: 'Daily', esVersion: '7.10.0' },
  281. data: {
  282. took: 1,
  283. responses: [
  284. {
  285. error: {
  286. reason: 'all shards failed',
  287. },
  288. status: 400,
  289. },
  290. ],
  291. },
  292. });
  293. const errObject = {
  294. data: '{\n "reason": "all shards failed"\n}',
  295. message: 'all shards failed',
  296. config: {
  297. url: 'http://localhost:3000/api/ds/query',
  298. },
  299. };
  300. await expect(ds.query(query)).toEmitValuesWith((received) => {
  301. expect(received.length).toBe(1);
  302. expect(received[0]).toEqual(errObject);
  303. });
  304. });
  305. it('should properly throw an error with just a message', async () => {
  306. const response: FetchResponse = {
  307. data: {
  308. error: 'Bad Request',
  309. message: 'Authentication to data source failed',
  310. },
  311. status: 400,
  312. url: 'http://localhost:3000/api/ds/query',
  313. config: { url: 'http://localhost:3000/api/ds/query' },
  314. type: 'basic',
  315. statusText: 'Bad Request',
  316. redirected: false,
  317. headers: {} as unknown as Headers,
  318. ok: false,
  319. };
  320. const { ds } = getTestContext({
  321. mockImplementation: () => throwError(response),
  322. from: undefined,
  323. jsonData: { esVersion: '7.10.0' },
  324. });
  325. const errObject = {
  326. error: 'Bad Request',
  327. message: 'Elasticsearch error: Authentication to data source failed',
  328. };
  329. await expect(ds.query(query)).toEmitValuesWith((received) => {
  330. expect(received.length).toBe(1);
  331. expect(received[0]).toEqual(errObject);
  332. });
  333. });
  334. it('should properly throw an unknown error', async () => {
  335. const { ds } = getTestContext({
  336. jsonData: { interval: 'Daily', esVersion: '7.10.0' },
  337. data: {
  338. took: 1,
  339. responses: [
  340. {
  341. error: {},
  342. status: 400,
  343. },
  344. ],
  345. },
  346. });
  347. const errObject = {
  348. data: '{}',
  349. message: 'Unknown elastic error response',
  350. config: {
  351. url: 'http://localhost:3000/api/ds/query',
  352. },
  353. };
  354. await expect(ds.query(query)).toEmitValuesWith((received) => {
  355. expect(received.length).toBe(1);
  356. expect(received[0]).toEqual(errObject);
  357. });
  358. });
  359. });
  360. // describe('When getting fields', () => {
  361. // const data = {
  362. // metricbeat: {
  363. // mappings: {
  364. // metricsets: {
  365. // _all: {},
  366. // _meta: {
  367. // test: 'something',
  368. // },
  369. // properties: {
  370. // '@timestamp': { type: 'date' },
  371. // __timestamp: { type: 'date' },
  372. // '@timestampnano': { type: 'date_nanos' },
  373. // beat: {
  374. // properties: {
  375. // name: {
  376. // fields: { raw: { type: 'keyword' } },
  377. // type: 'string',
  378. // },
  379. // hostname: { type: 'string' },
  380. // },
  381. // },
  382. // system: {
  383. // properties: {
  384. // cpu: {
  385. // properties: {
  386. // system: { type: 'float' },
  387. // user: { type: 'float' },
  388. // },
  389. // },
  390. // process: {
  391. // properties: {
  392. // cpu: {
  393. // properties: {
  394. // total: { type: 'float' },
  395. // },
  396. // },
  397. // name: { type: 'string' },
  398. // },
  399. // },
  400. // },
  401. // },
  402. // },
  403. // },
  404. // },
  405. // },
  406. // };
  407. // it('should return nested fields', async () => {
  408. // const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' });
  409. // await expect(ds.getFields()).toEmitValuesWith((received) => {
  410. // expect(received.length).toBe(1);
  411. // const fieldObjects = received[0];
  412. // const fields = map(fieldObjects, 'text');
  413. // expect(fields).toEqual([
  414. // '@timestamp',
  415. // '__timestamp',
  416. // '@timestampnano',
  417. // 'beat.name.raw',
  418. // 'beat.name',
  419. // 'beat.hostname',
  420. // 'system.cpu.system',
  421. // 'system.cpu.user',
  422. // 'system.process.cpu.total',
  423. // 'system.process.name',
  424. // ]);
  425. // });
  426. // });
  427. // it('should return number fields', async () => {
  428. // const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' });
  429. // await expect(ds.getFields(['number'])).toEmitValuesWith((received) => {
  430. // expect(received.length).toBe(1);
  431. // const fieldObjects = received[0];
  432. // const fields = map(fieldObjects, 'text');
  433. // expect(fields).toEqual(['system.cpu.system', 'system.cpu.user', 'system.process.cpu.total']);
  434. // });
  435. // });
  436. // it('should return date fields', async () => {
  437. // const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' });
  438. // await expect(ds.getFields(['date'])).toEmitValuesWith((received) => {
  439. // expect(received.length).toBe(1);
  440. // const fieldObjects = received[0];
  441. // const fields = map(fieldObjects, 'text');
  442. // expect(fields).toEqual(['@timestamp', '__timestamp', '@timestampnano']);
  443. // });
  444. // });
  445. // });
  446. describe('When getting field mappings on indices with gaps', () => {
  447. const basicResponse = {
  448. metricbeat: {
  449. mappings: {
  450. metricsets: {
  451. _all: {},
  452. properties: {
  453. '@timestamp': { type: 'date' },
  454. beat: {
  455. properties: {
  456. hostname: { type: 'string' },
  457. },
  458. },
  459. },
  460. },
  461. },
  462. },
  463. };
  464. // const alternateResponse = {
  465. // metricbeat: {
  466. // mappings: {
  467. // metricsets: {
  468. // _all: {},
  469. // properties: {
  470. // '@timestamp': { type: 'date' },
  471. // },
  472. // },
  473. // },
  474. // },
  475. // };
  476. // it('should return fields of the newest available index', async () => {
  477. // const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD');
  478. // const threeDaysBefore = toUtc().subtract(3, 'day').format('YYYY.MM.DD');
  479. // const baseUrl = `${ELASTICSEARCH_MOCK_URL}/asd-${twoDaysBefore}/_mapping`;
  480. // const alternateUrl = `${ELASTICSEARCH_MOCK_URL}/asd-${threeDaysBefore}/_mapping`;
  481. // const { ds, timeSrv } = getTestContext({
  482. // from: 'now-2w',
  483. // jsonData: { interval: 'Daily', esVersion: 50 },
  484. // mockImplementation: (options) => {
  485. // if (options.url === baseUrl) {
  486. // return of(createFetchResponse(basicResponse));
  487. // } else if (options.url === alternateUrl) {
  488. // return of(createFetchResponse(alternateResponse));
  489. // }
  490. // return throwError({ status: 404 });
  491. // },
  492. // });
  493. // const range = timeSrv.timeRange();
  494. // await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => {
  495. // expect(received.length).toBe(1);
  496. // const fieldObjects = received[0];
  497. // const fields = map(fieldObjects, 'text');
  498. // expect(fields).toEqual(['@timestamp', 'beat.hostname']);
  499. // });
  500. // });
  501. it('should not retry when ES is down', async () => {
  502. const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD');
  503. const { ds, timeSrv, fetchMock } = getTestContext({
  504. from: 'now-2w',
  505. jsonData: { interval: 'Daily', esVersion: '7.10.0' },
  506. mockImplementation: (options) => {
  507. if (options.url === `${ELASTICSEARCH_MOCK_URL}/asd-${twoDaysBefore}/_mapping`) {
  508. return of(createFetchResponse(basicResponse));
  509. }
  510. return throwError({ status: 500 });
  511. },
  512. });
  513. const range = timeSrv.timeRange();
  514. await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => {
  515. expect(received.length).toBe(1);
  516. expect(received[0]).toStrictEqual({ status: 500 });
  517. expect(fetchMock).toBeCalledTimes(1);
  518. });
  519. });
  520. it('should not retry more than 7 indices', async () => {
  521. const { ds, timeSrv, fetchMock } = getTestContext({
  522. from: 'now-2w',
  523. jsonData: { interval: 'Daily', esVersion: '7.10.0' },
  524. mockImplementation: (options) => {
  525. return throwError({ status: 404 });
  526. },
  527. });
  528. const range = timeSrv.timeRange();
  529. await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => {
  530. expect(received.length).toBe(1);
  531. expect(received[0]).toStrictEqual('Could not find an available index for this time range.');
  532. expect(fetchMock).toBeCalledTimes(7);
  533. });
  534. });
  535. });
  536. describe('When getting fields from ES 7.0', () => {
  537. const data = {
  538. 'genuine.es7._mapping.response': {
  539. mappings: {
  540. properties: {
  541. '@timestamp_millis': {
  542. type: 'date',
  543. format: 'epoch_millis',
  544. },
  545. classification_terms: {
  546. type: 'keyword',
  547. },
  548. domains: {
  549. type: 'keyword',
  550. },
  551. ip_address: {
  552. type: 'ip',
  553. },
  554. justification_blob: {
  555. properties: {
  556. criterion: {
  557. type: 'text',
  558. fields: {
  559. keyword: {
  560. type: 'keyword',
  561. ignore_above: 256,
  562. },
  563. },
  564. },
  565. overall_vote_score: {
  566. type: 'float',
  567. },
  568. shallow: {
  569. properties: {
  570. jsi: {
  571. properties: {
  572. sdb: {
  573. properties: {
  574. dsel2: {
  575. properties: {
  576. 'bootlegged-gille': {
  577. properties: {
  578. botness: {
  579. type: 'float',
  580. },
  581. general_algorithm_score: {
  582. type: 'float',
  583. },
  584. },
  585. },
  586. 'uncombed-boris': {
  587. properties: {
  588. botness: {
  589. type: 'float',
  590. },
  591. general_algorithm_score: {
  592. type: 'float',
  593. },
  594. },
  595. },
  596. },
  597. },
  598. },
  599. },
  600. },
  601. },
  602. },
  603. },
  604. },
  605. },
  606. overall_vote_score: {
  607. type: 'float',
  608. },
  609. ua_terms_long: {
  610. type: 'keyword',
  611. },
  612. ua_terms_short: {
  613. type: 'keyword',
  614. },
  615. },
  616. },
  617. },
  618. };
  619. const dateFields = ['@timestamp_millis'];
  620. const numberFields = [
  621. 'justification_blob.overall_vote_score',
  622. 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness',
  623. 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score',
  624. 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness',
  625. 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score',
  626. 'overall_vote_score',
  627. ];
  628. it('should return nested fields', async () => {
  629. const { ds } = getTestContext({
  630. data,
  631. database: 'genuine.es7._mapping.response',
  632. jsonData: { esVersion: '7.10.0' },
  633. });
  634. await expect(ds.getFields()).toEmitValuesWith((received) => {
  635. expect(received.length).toBe(1);
  636. const fieldObjects = received[0];
  637. const fields = map(fieldObjects, 'text');
  638. expect(fields).toEqual([
  639. '@timestamp_millis',
  640. 'classification_terms',
  641. 'domains',
  642. 'ip_address',
  643. 'justification_blob.criterion.keyword',
  644. 'justification_blob.criterion',
  645. 'justification_blob.overall_vote_score',
  646. 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness',
  647. 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score',
  648. 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness',
  649. 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score',
  650. 'overall_vote_score',
  651. 'ua_terms_long',
  652. 'ua_terms_short',
  653. ]);
  654. });
  655. });
  656. it('should return number fields', async () => {
  657. const { ds } = getTestContext({
  658. data,
  659. database: 'genuine.es7._mapping.response',
  660. jsonData: { esVersion: '7.10.0' },
  661. });
  662. await expect(ds.getFields(['number'])).toEmitValuesWith((received) => {
  663. expect(received.length).toBe(1);
  664. const fieldObjects = received[0];
  665. const fields = map(fieldObjects, 'text');
  666. expect(fields).toEqual(numberFields);
  667. });
  668. });
  669. it('should return date fields', async () => {
  670. const { ds } = getTestContext({
  671. data,
  672. database: 'genuine.es7._mapping.response',
  673. jsonData: { esVersion: '7.10.0' },
  674. });
  675. await expect(ds.getFields(['date'])).toEmitValuesWith((received) => {
  676. expect(received.length).toBe(1);
  677. const fieldObjects = received[0];
  678. const fields = map(fieldObjects, 'text');
  679. expect(fields).toEqual(dateFields);
  680. });
  681. });
  682. });
  683. describe('When issuing aggregation query on es5.x', () => {
  684. async function runScenario() {
  685. const range = createTimeRange(dateTime([2015, 4, 30, 10]), dateTime([2015, 5, 1, 10]));
  686. const targets = [
  687. {
  688. refId: 'A',
  689. bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
  690. metrics: [{ type: 'count', id: '1' }],
  691. query: 'test',
  692. },
  693. ];
  694. const query: any = { range, targets };
  695. const data = { responses: [] };
  696. const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: '7.10.0' }, data, database: 'test' });
  697. await expect(ds.query(query)).toEmitValuesWith((received) => {
  698. expect(received.length).toBe(1);
  699. expect(received[0]).toEqual({ data: [] });
  700. });
  701. expect(fetchMock).toHaveBeenCalledTimes(1);
  702. const requestOptions = fetchMock.mock.calls[0][0];
  703. const parts = requestOptions.data.split('\n');
  704. const header = JSON.parse(parts[0]);
  705. const body = JSON.parse(parts[1]);
  706. return { body, header };
  707. }
  708. it('should not set search type to count', async () => {
  709. const { header } = await runScenario();
  710. expect(header.search_type).not.toEqual('count');
  711. });
  712. it('should set size to 0', async () => {
  713. const { body } = await runScenario();
  714. expect(body.size).toBe(0);
  715. });
  716. });
  717. describe('When issuing metricFind query on es5.x', () => {
  718. async function runScenario() {
  719. const data = {
  720. responses: [
  721. {
  722. aggregations: {
  723. '1': {
  724. buckets: [
  725. { doc_count: 1, key: 'test' },
  726. {
  727. doc_count: 2,
  728. key: 'test2',
  729. key_as_string: 'test2_as_string',
  730. },
  731. ],
  732. },
  733. },
  734. },
  735. ],
  736. };
  737. const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: '7.10.0' }, data, database: 'test' });
  738. const results = await ds.metricFindQuery('{"find": "terms", "field": "test"}');
  739. expect(fetchMock).toHaveBeenCalledTimes(1);
  740. const requestOptions = fetchMock.mock.calls[0][0];
  741. const parts = requestOptions.data.split('\n');
  742. const header = JSON.parse(parts[0]);
  743. const body = JSON.parse(parts[1]);
  744. return { results, body, header };
  745. }
  746. it('should get results', async () => {
  747. const { results } = await runScenario();
  748. expect(results.length).toEqual(2);
  749. });
  750. it('should use key or key_as_string', async () => {
  751. const { results } = await runScenario();
  752. expect(results[0].text).toEqual('test');
  753. expect(results[1].text).toEqual('test2_as_string');
  754. });
  755. it('should not set search type to count', async () => {
  756. const { header } = await runScenario();
  757. expect(header.search_type).not.toEqual('count');
  758. });
  759. it('should set size to 0', async () => {
  760. const { body } = await runScenario();
  761. expect(body.size).toBe(0);
  762. });
  763. it('should not set terms aggregation size to 0', async () => {
  764. const { body } = await runScenario();
  765. expect(body['aggs']['1']['terms'].size).not.toBe(0);
  766. });
  767. });
  768. describe('query', () => {
  769. it('should replace range as integer not string', async () => {
  770. const { ds } = getTestContext({ jsonData: { interval: 'Daily', esVersion: '7.10.0', timeField: '@time' } });
  771. const postMock = jest.fn((url: string, data: any) => of(createFetchResponse({ responses: [] })));
  772. ds['post'] = postMock;
  773. await expect(ds.query(createElasticQuery())).toEmitValuesWith((received) => {
  774. expect(postMock).toHaveBeenCalledTimes(1);
  775. const query = postMock.mock.calls[0][1];
  776. expect(typeof JSON.parse(query.split('\n')[1]).query.bool.filter[0].range['@time'].gte).toBe('number');
  777. });
  778. });
  779. });
  780. it('should correctly interpolate variables in query', () => {
  781. const { ds } = getTestContext();
  782. const query: ElasticsearchQuery = {
  783. refId: 'A',
  784. bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '$var', label: '' }] }, id: '1' }],
  785. metrics: [{ type: 'count', id: '1' }],
  786. query: '$var',
  787. };
  788. const interpolatedQuery = ds.interpolateVariablesInQueries([query], {})[0];
  789. expect(interpolatedQuery.query).toBe('resolvedVariable');
  790. expect((interpolatedQuery.bucketAggs![0] as Filters).settings!.filters![0].query).toBe('resolvedVariable');
  791. });
  792. it('should correctly handle empty query strings in filters bucket aggregation', () => {
  793. const { ds } = getTestContext();
  794. const query: ElasticsearchQuery = {
  795. refId: 'A',
  796. bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '', label: '' }] }, id: '1' }],
  797. metrics: [{ type: 'count', id: '1' }],
  798. query: '',
  799. };
  800. const interpolatedQuery = ds.interpolateVariablesInQueries([query], {})[0];
  801. expect((interpolatedQuery.bucketAggs![0] as Filters).settings!.filters![0].query).toBe('*');
  802. });
  803. });
  804. describe('getMultiSearchUrl', () => {
  805. describe('When esVersion >= 7.10.0', () => {
  806. it('Should add correct params to URL if "includeFrozen" is enabled', () => {
  807. const { ds } = getTestContext({ jsonData: { esVersion: '7.10.0', includeFrozen: true, xpack: true } });
  808. expect(ds.getMultiSearchUrl()).toMatch(/ignore_throttled=false/);
  809. });
  810. it('Should NOT add ignore_throttled if "includeFrozen" is disabled', () => {
  811. const { ds } = getTestContext({ jsonData: { esVersion: '7.10.0', includeFrozen: false, xpack: true } });
  812. expect(ds.getMultiSearchUrl()).not.toMatch(/ignore_throttled=false/);
  813. });
  814. it('Should NOT add ignore_throttled if "xpack" is disabled', () => {
  815. const { ds } = getTestContext({ jsonData: { esVersion: '7.10.0', includeFrozen: true, xpack: false } });
  816. expect(ds.getMultiSearchUrl()).not.toMatch(/ignore_throttled=false/);
  817. });
  818. });
  819. });
  820. describe('enhanceDataFrame', () => {
  821. it('adds links to dataframe', () => {
  822. const df = new MutableDataFrame({
  823. fields: [
  824. {
  825. name: 'urlField',
  826. values: new ArrayVector([]),
  827. },
  828. {
  829. name: 'traceField',
  830. values: new ArrayVector([]),
  831. },
  832. ],
  833. });
  834. enhanceDataFrame(df, [
  835. {
  836. field: 'urlField',
  837. url: 'someUrl',
  838. },
  839. {
  840. field: 'urlField',
  841. url: 'someOtherUrl',
  842. },
  843. {
  844. field: 'traceField',
  845. url: 'query',
  846. datasourceUid: 'ds1',
  847. },
  848. {
  849. field: 'traceField',
  850. url: 'otherQuery',
  851. datasourceUid: 'ds2',
  852. },
  853. ]);
  854. expect(df.fields[0].config.links).toHaveLength(2);
  855. expect(df.fields[0].config.links).toContainEqual({
  856. title: '',
  857. url: 'someUrl',
  858. });
  859. expect(df.fields[0].config.links).toContainEqual({
  860. title: '',
  861. url: 'someOtherUrl',
  862. });
  863. expect(df.fields[1].config.links).toHaveLength(2);
  864. expect(df.fields[1].config.links).toContainEqual(
  865. expect.objectContaining({
  866. title: '',
  867. url: '',
  868. internal: expect.objectContaining({
  869. query: { query: 'query' },
  870. datasourceUid: 'ds1',
  871. }),
  872. })
  873. );
  874. expect(df.fields[1].config.links).toContainEqual(
  875. expect.objectContaining({
  876. title: '',
  877. url: '',
  878. internal: expect.objectContaining({
  879. query: { query: 'otherQuery' },
  880. datasourceUid: 'ds2',
  881. }),
  882. })
  883. );
  884. });
  885. it('adds limit to dataframe', () => {
  886. const df = new MutableDataFrame({
  887. fields: [
  888. {
  889. name: 'someField',
  890. values: new ArrayVector([]),
  891. },
  892. ],
  893. });
  894. enhanceDataFrame(df, [], 10);
  895. expect(df.meta?.limit).toBe(10);
  896. });
  897. });
  898. const createElasticQuery = (): DataQueryRequest<ElasticsearchQuery> => {
  899. return {
  900. requestId: '',
  901. dashboardId: 0,
  902. interval: '',
  903. panelId: 0,
  904. intervalMs: 1,
  905. scopedVars: {},
  906. timezone: '',
  907. app: CoreApp.Dashboard,
  908. startTime: 0,
  909. range: {
  910. from: dateTime([2015, 4, 30, 10]),
  911. to: dateTime([2015, 5, 1, 10]),
  912. } as any,
  913. targets: [
  914. {
  915. refId: '',
  916. bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
  917. metrics: [{ type: 'count', id: '' }],
  918. query: 'test',
  919. },
  920. ],
  921. };
  922. };
  923. const logsResponse = {
  924. data: {
  925. responses: [
  926. {
  927. aggregations: {
  928. '2': {
  929. buckets: [
  930. {
  931. doc_count: 10,
  932. key: 1000,
  933. },
  934. {
  935. doc_count: 15,
  936. key: 2000,
  937. },
  938. ],
  939. },
  940. },
  941. hits: {
  942. hits: [
  943. {
  944. '@timestamp': ['2019-06-24T09:51:19.765Z'],
  945. _id: 'fdsfs',
  946. _type: '_doc',
  947. _index: 'mock-index',
  948. _source: {
  949. '@timestamp': '2019-06-24T09:51:19.765Z',
  950. host: 'djisaodjsoad',
  951. message: 'hello, i am a message',
  952. },
  953. fields: {
  954. '@timestamp': ['2019-06-24T09:51:19.765Z'],
  955. },
  956. },
  957. {
  958. '@timestamp': ['2019-06-24T09:52:19.765Z'],
  959. _id: 'kdospaidopa',
  960. _type: '_doc',
  961. _index: 'mock-index',
  962. _source: {
  963. '@timestamp': '2019-06-24T09:52:19.765Z',
  964. host: 'dsalkdakdop',
  965. message: 'hello, i am also message',
  966. },
  967. fields: {
  968. '@timestamp': ['2019-06-24T09:52:19.765Z'],
  969. },
  970. },
  971. ],
  972. },
  973. },
  974. ],
  975. },
  976. };