datasource.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. import angular, { IQService } from 'angular';
  2. import { dateMath } from '@grafana/data';
  3. import _ from 'lodash';
  4. import { ElasticDatasource, getMaxConcurrenShardRequestOrDefault } from '../datasource';
  5. import { toUtc, dateTime } from '@grafana/data';
  6. import { BackendSrv } from 'app/core/services/backend_srv';
  7. import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  8. import { TemplateSrv } from 'app/features/templating/template_srv';
  9. import { DataSourceInstanceSettings } from '@grafana/ui';
  10. import { ElasticsearchOptions } from '../types';
  11. describe('ElasticDatasource', function(this: any) {
  12. const backendSrv: any = {
  13. datasourceRequest: jest.fn(),
  14. };
  15. const $rootScope: any = {
  16. $on: jest.fn(),
  17. appEvent: jest.fn(),
  18. };
  19. const templateSrv: any = {
  20. replace: jest.fn(text => {
  21. if (text.startsWith('$')) {
  22. return `resolvedVariable`;
  23. } else {
  24. return text;
  25. }
  26. }),
  27. getAdhocFilters: jest.fn(() => []),
  28. };
  29. const timeSrv: any = {
  30. time: { from: 'now-1h', to: 'now' },
  31. timeRange: jest.fn(() => {
  32. return {
  33. from: dateMath.parse(timeSrv.time.from, false),
  34. to: dateMath.parse(timeSrv.time.to, true),
  35. };
  36. }),
  37. setTime: jest.fn(time => {
  38. this.time = time;
  39. }),
  40. };
  41. const ctx = {
  42. $rootScope,
  43. backendSrv,
  44. } as any;
  45. function createDatasource(instanceSettings: DataSourceInstanceSettings<ElasticsearchOptions>) {
  46. instanceSettings.jsonData = instanceSettings.jsonData || ({} as ElasticsearchOptions);
  47. ctx.ds = new ElasticDatasource(
  48. instanceSettings,
  49. {} as IQService,
  50. backendSrv as BackendSrv,
  51. templateSrv as TemplateSrv,
  52. timeSrv as TimeSrv
  53. );
  54. }
  55. describe('When testing datasource with index pattern', () => {
  56. beforeEach(() => {
  57. createDatasource({
  58. url: 'http://es.com',
  59. database: '[asd-]YYYY.MM.DD',
  60. jsonData: { interval: 'Daily', esVersion: 2 } as ElasticsearchOptions,
  61. } as DataSourceInstanceSettings<ElasticsearchOptions>);
  62. });
  63. it('should translate index pattern to current day', () => {
  64. let requestOptions: any;
  65. ctx.backendSrv.datasourceRequest = jest.fn(options => {
  66. requestOptions = options;
  67. return Promise.resolve({ data: {} });
  68. });
  69. ctx.ds.testDatasource();
  70. const today = toUtc().format('YYYY.MM.DD');
  71. expect(requestOptions.url).toBe('http://es.com/asd-' + today + '/_mapping');
  72. });
  73. });
  74. describe('When issuing metric query with interval pattern', () => {
  75. let requestOptions: any, parts: any, header: any, query: any, result: any;
  76. beforeEach(async () => {
  77. createDatasource({
  78. url: 'http://es.com',
  79. database: '[asd-]YYYY.MM.DD',
  80. jsonData: { interval: 'Daily', esVersion: 2 } as ElasticsearchOptions,
  81. } as DataSourceInstanceSettings<ElasticsearchOptions>);
  82. ctx.backendSrv.datasourceRequest = jest.fn(options => {
  83. requestOptions = options;
  84. return Promise.resolve({
  85. data: {
  86. responses: [
  87. {
  88. aggregations: {
  89. '1': {
  90. buckets: [
  91. {
  92. doc_count: 10,
  93. key: 1000,
  94. },
  95. ],
  96. },
  97. },
  98. },
  99. ],
  100. },
  101. });
  102. });
  103. query = {
  104. range: {
  105. from: toUtc([2015, 4, 30, 10]),
  106. to: toUtc([2015, 5, 1, 10]),
  107. },
  108. targets: [
  109. {
  110. alias: '$varAlias',
  111. bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
  112. metrics: [{ type: 'count', id: '1' }],
  113. query: 'escape\\:test',
  114. },
  115. ],
  116. };
  117. result = await ctx.ds.query(query);
  118. parts = requestOptions.data.split('\n');
  119. header = angular.fromJson(parts[0]);
  120. });
  121. it('should translate index pattern to current day', () => {
  122. expect(header.index).toEqual(['asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']);
  123. });
  124. it('should not resolve the variable in the original alias field in the query', () => {
  125. expect(query.targets[0].alias).toEqual('$varAlias');
  126. });
  127. it('should resolve the alias variable for the alias/target in the result', () => {
  128. expect(result.data[0].target).toEqual('resolvedVariable');
  129. });
  130. it('should json escape lucene query', () => {
  131. const body = angular.fromJson(parts[1]);
  132. expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test');
  133. });
  134. });
  135. describe('When issuing logs query with interval pattern', () => {
  136. let query, queryBuilderSpy: any;
  137. beforeEach(async () => {
  138. createDatasource({
  139. url: 'http://es.com',
  140. database: 'mock-index',
  141. jsonData: { interval: 'Daily', esVersion: 2, timeField: '@timestamp' } as ElasticsearchOptions,
  142. } as DataSourceInstanceSettings<ElasticsearchOptions>);
  143. ctx.backendSrv.datasourceRequest = jest.fn(options => {
  144. return Promise.resolve({
  145. data: {
  146. responses: [
  147. {
  148. aggregations: {
  149. '2': {
  150. buckets: [
  151. {
  152. doc_count: 10,
  153. key: 1000,
  154. },
  155. {
  156. doc_count: 15,
  157. key: 2000,
  158. },
  159. ],
  160. },
  161. },
  162. hits: {
  163. hits: [
  164. {
  165. '@timestamp': ['2019-06-24T09:51:19.765Z'],
  166. _id: 'fdsfs',
  167. _type: '_doc',
  168. _index: 'mock-index',
  169. _source: {
  170. '@timestamp': '2019-06-24T09:51:19.765Z',
  171. host: 'djisaodjsoad',
  172. message: 'hello, i am a message',
  173. },
  174. fields: {
  175. '@timestamp': ['2019-06-24T09:51:19.765Z'],
  176. },
  177. },
  178. {
  179. '@timestamp': ['2019-06-24T09:52:19.765Z'],
  180. _id: 'kdospaidopa',
  181. _type: '_doc',
  182. _index: 'mock-index',
  183. _source: {
  184. '@timestamp': '2019-06-24T09:52:19.765Z',
  185. host: 'dsalkdakdop',
  186. message: 'hello, i am also message',
  187. },
  188. fields: {
  189. '@timestamp': ['2019-06-24T09:52:19.765Z'],
  190. },
  191. },
  192. ],
  193. },
  194. },
  195. ],
  196. },
  197. });
  198. });
  199. query = {
  200. range: {
  201. from: toUtc([2015, 4, 30, 10]),
  202. to: toUtc([2019, 7, 1, 10]),
  203. },
  204. targets: [
  205. {
  206. alias: '$varAlias',
  207. refId: 'A',
  208. bucketAggs: [{ type: 'date_histogram', settings: { interval: 'auto' }, id: '2' }],
  209. metrics: [{ type: 'count', id: '1' }],
  210. query: 'escape\\:test',
  211. interval: '10s',
  212. isLogsQuery: true,
  213. timeField: '@timestamp',
  214. },
  215. ],
  216. };
  217. queryBuilderSpy = jest.spyOn(ctx.ds.queryBuilder, 'getLogsQuery');
  218. await ctx.ds.query(query);
  219. });
  220. it('should call getLogsQuery()', () => {
  221. expect(queryBuilderSpy).toHaveBeenCalled();
  222. });
  223. });
  224. describe('When issuing document query', () => {
  225. let requestOptions: any, parts: any, header: any;
  226. beforeEach(() => {
  227. createDatasource({
  228. url: 'http://es.com',
  229. database: 'test',
  230. jsonData: { esVersion: 2 } as ElasticsearchOptions,
  231. } as DataSourceInstanceSettings<ElasticsearchOptions>);
  232. ctx.backendSrv.datasourceRequest = jest.fn(options => {
  233. requestOptions = options;
  234. return Promise.resolve({ data: { responses: [] } });
  235. });
  236. ctx.ds.query({
  237. range: {
  238. from: dateTime([2015, 4, 30, 10]),
  239. to: dateTime([2015, 5, 1, 10]),
  240. },
  241. targets: [
  242. {
  243. bucketAggs: [],
  244. metrics: [{ type: 'raw_document' }],
  245. query: 'test',
  246. },
  247. ],
  248. });
  249. parts = requestOptions.data.split('\n');
  250. header = angular.fromJson(parts[0]);
  251. });
  252. it('should set search type to query_then_fetch', () => {
  253. expect(header.search_type).toEqual('query_then_fetch');
  254. });
  255. it('should set size', () => {
  256. const body = angular.fromJson(parts[1]);
  257. expect(body.size).toBe(500);
  258. });
  259. });
  260. describe('When getting fields', () => {
  261. beforeEach(() => {
  262. createDatasource({
  263. url: 'http://es.com',
  264. database: 'metricbeat',
  265. jsonData: { esVersion: 50 } as ElasticsearchOptions,
  266. } as DataSourceInstanceSettings<ElasticsearchOptions>);
  267. ctx.backendSrv.datasourceRequest = jest.fn(options => {
  268. return Promise.resolve({
  269. data: {
  270. metricbeat: {
  271. mappings: {
  272. metricsets: {
  273. _all: {},
  274. properties: {
  275. '@timestamp': { type: 'date' },
  276. beat: {
  277. properties: {
  278. name: {
  279. fields: { raw: { type: 'keyword' } },
  280. type: 'string',
  281. },
  282. hostname: { type: 'string' },
  283. },
  284. },
  285. system: {
  286. properties: {
  287. cpu: {
  288. properties: {
  289. system: { type: 'float' },
  290. user: { type: 'float' },
  291. },
  292. },
  293. process: {
  294. properties: {
  295. cpu: {
  296. properties: {
  297. total: { type: 'float' },
  298. },
  299. },
  300. name: { type: 'string' },
  301. },
  302. },
  303. },
  304. },
  305. },
  306. },
  307. },
  308. },
  309. },
  310. });
  311. });
  312. });
  313. it('should return nested fields', async () => {
  314. const fieldObjects = await ctx.ds.getFields({
  315. find: 'fields',
  316. query: '*',
  317. });
  318. const fields = _.map(fieldObjects, 'text');
  319. expect(fields).toEqual([
  320. '@timestamp',
  321. 'beat.name.raw',
  322. 'beat.name',
  323. 'beat.hostname',
  324. 'system.cpu.system',
  325. 'system.cpu.user',
  326. 'system.process.cpu.total',
  327. 'system.process.name',
  328. ]);
  329. });
  330. it('should return number fields', async () => {
  331. const fieldObjects = await ctx.ds.getFields({
  332. find: 'fields',
  333. query: '*',
  334. type: 'number',
  335. });
  336. const fields = _.map(fieldObjects, 'text');
  337. expect(fields).toEqual(['system.cpu.system', 'system.cpu.user', 'system.process.cpu.total']);
  338. });
  339. it('should return date fields', async () => {
  340. const fieldObjects = await ctx.ds.getFields({
  341. find: 'fields',
  342. query: '*',
  343. type: 'date',
  344. });
  345. const fields = _.map(fieldObjects, 'text');
  346. expect(fields).toEqual(['@timestamp']);
  347. });
  348. });
  349. describe('When getting fields from ES 7.0', () => {
  350. beforeEach(() => {
  351. createDatasource({
  352. url: 'http://es.com',
  353. database: 'genuine.es7._mapping.response',
  354. jsonData: { esVersion: 70 } as ElasticsearchOptions,
  355. } as DataSourceInstanceSettings<ElasticsearchOptions>);
  356. ctx.backendSrv.datasourceRequest = jest.fn(options => {
  357. return Promise.resolve({
  358. data: {
  359. 'genuine.es7._mapping.response': {
  360. mappings: {
  361. properties: {
  362. '@timestamp_millis': {
  363. type: 'date',
  364. format: 'epoch_millis',
  365. },
  366. classification_terms: {
  367. type: 'keyword',
  368. },
  369. domains: {
  370. type: 'keyword',
  371. },
  372. ip_address: {
  373. type: 'ip',
  374. },
  375. justification_blob: {
  376. properties: {
  377. criterion: {
  378. type: 'text',
  379. fields: {
  380. keyword: {
  381. type: 'keyword',
  382. ignore_above: 256,
  383. },
  384. },
  385. },
  386. overall_vote_score: {
  387. type: 'float',
  388. },
  389. shallow: {
  390. properties: {
  391. jsi: {
  392. properties: {
  393. sdb: {
  394. properties: {
  395. dsel2: {
  396. properties: {
  397. 'bootlegged-gille': {
  398. properties: {
  399. botness: {
  400. type: 'float',
  401. },
  402. general_algorithm_score: {
  403. type: 'float',
  404. },
  405. },
  406. },
  407. 'uncombed-boris': {
  408. properties: {
  409. botness: {
  410. type: 'float',
  411. },
  412. general_algorithm_score: {
  413. type: 'float',
  414. },
  415. },
  416. },
  417. },
  418. },
  419. },
  420. },
  421. },
  422. },
  423. },
  424. },
  425. },
  426. },
  427. overall_vote_score: {
  428. type: 'float',
  429. },
  430. ua_terms_long: {
  431. type: 'keyword',
  432. },
  433. ua_terms_short: {
  434. type: 'keyword',
  435. },
  436. },
  437. },
  438. },
  439. },
  440. });
  441. });
  442. });
  443. it('should return nested fields', async () => {
  444. const fieldObjects = await ctx.ds.getFields({
  445. find: 'fields',
  446. query: '*',
  447. });
  448. const fields = _.map(fieldObjects, 'text');
  449. expect(fields).toEqual([
  450. '@timestamp_millis',
  451. 'classification_terms',
  452. 'domains',
  453. 'ip_address',
  454. 'justification_blob.criterion.keyword',
  455. 'justification_blob.criterion',
  456. 'justification_blob.overall_vote_score',
  457. 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness',
  458. 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score',
  459. 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness',
  460. 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score',
  461. 'overall_vote_score',
  462. 'ua_terms_long',
  463. 'ua_terms_short',
  464. ]);
  465. });
  466. it('should return number fields', async () => {
  467. const fieldObjects = await ctx.ds.getFields({
  468. find: 'fields',
  469. query: '*',
  470. type: 'number',
  471. });
  472. const fields = _.map(fieldObjects, 'text');
  473. expect(fields).toEqual([
  474. 'justification_blob.overall_vote_score',
  475. 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.botness',
  476. 'justification_blob.shallow.jsi.sdb.dsel2.bootlegged-gille.general_algorithm_score',
  477. 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.botness',
  478. 'justification_blob.shallow.jsi.sdb.dsel2.uncombed-boris.general_algorithm_score',
  479. 'overall_vote_score',
  480. ]);
  481. });
  482. it('should return date fields', async () => {
  483. const fieldObjects = await ctx.ds.getFields({
  484. find: 'fields',
  485. query: '*',
  486. type: 'date',
  487. });
  488. const fields = _.map(fieldObjects, 'text');
  489. expect(fields).toEqual(['@timestamp_millis']);
  490. });
  491. });
  492. describe('When issuing aggregation query on es5.x', () => {
  493. let requestOptions: any, parts: any, header: any;
  494. beforeEach(() => {
  495. createDatasource({
  496. url: 'http://es.com',
  497. database: 'test',
  498. jsonData: { esVersion: 5 } as ElasticsearchOptions,
  499. } as DataSourceInstanceSettings<ElasticsearchOptions>);
  500. ctx.backendSrv.datasourceRequest = jest.fn(options => {
  501. requestOptions = options;
  502. return Promise.resolve({ data: { responses: [] } });
  503. });
  504. ctx.ds.query({
  505. range: {
  506. from: dateTime([2015, 4, 30, 10]),
  507. to: dateTime([2015, 5, 1, 10]),
  508. },
  509. targets: [
  510. {
  511. bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
  512. metrics: [{ type: 'count' }],
  513. query: 'test',
  514. },
  515. ],
  516. });
  517. parts = requestOptions.data.split('\n');
  518. header = angular.fromJson(parts[0]);
  519. });
  520. it('should not set search type to count', () => {
  521. expect(header.search_type).not.toEqual('count');
  522. });
  523. it('should set size to 0', () => {
  524. const body = angular.fromJson(parts[1]);
  525. expect(body.size).toBe(0);
  526. });
  527. });
  528. describe('When issuing metricFind query on es5.x', () => {
  529. let requestOptions: any, parts, header: any, body: any, results: any;
  530. beforeEach(() => {
  531. createDatasource({
  532. url: 'http://es.com',
  533. database: 'test',
  534. jsonData: { esVersion: 5 } as ElasticsearchOptions,
  535. } as DataSourceInstanceSettings<ElasticsearchOptions>);
  536. ctx.backendSrv.datasourceRequest = jest.fn(options => {
  537. requestOptions = options;
  538. return Promise.resolve({
  539. data: {
  540. responses: [
  541. {
  542. aggregations: {
  543. '1': {
  544. buckets: [
  545. { doc_count: 1, key: 'test' },
  546. {
  547. doc_count: 2,
  548. key: 'test2',
  549. key_as_string: 'test2_as_string',
  550. },
  551. ],
  552. },
  553. },
  554. },
  555. ],
  556. },
  557. });
  558. });
  559. ctx.ds.metricFindQuery('{"find": "terms", "field": "test"}').then((res: any) => {
  560. results = res;
  561. });
  562. parts = requestOptions.data.split('\n');
  563. header = angular.fromJson(parts[0]);
  564. body = angular.fromJson(parts[1]);
  565. });
  566. it('should get results', () => {
  567. expect(results.length).toEqual(2);
  568. });
  569. it('should use key or key_as_string', () => {
  570. expect(results[0].text).toEqual('test');
  571. expect(results[1].text).toEqual('test2_as_string');
  572. });
  573. it('should not set search type to count', () => {
  574. expect(header.search_type).not.toEqual('count');
  575. });
  576. it('should set size to 0', () => {
  577. expect(body.size).toBe(0);
  578. });
  579. it('should not set terms aggregation size to 0', () => {
  580. expect(body['aggs']['1']['terms'].size).not.toBe(0);
  581. });
  582. });
  583. });
  584. describe('getMaxConcurrenShardRequestOrDefault', () => {
  585. const testCases = [
  586. { version: 50, expectedMaxConcurrentShardRequests: 256 },
  587. { version: 50, maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 },
  588. { version: 56, expectedMaxConcurrentShardRequests: 256 },
  589. { version: 56, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 },
  590. { version: 56, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 },
  591. { version: 56, maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 },
  592. { version: 70, expectedMaxConcurrentShardRequests: 5 },
  593. { version: 70, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 },
  594. { version: 70, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 },
  595. { version: 70, maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 },
  596. ];
  597. testCases.forEach(tc => {
  598. it(`version = ${tc.version}, maxConcurrentShardRequests = ${tc.maxConcurrentShardRequests}`, () => {
  599. const options = { esVersion: tc.version, maxConcurrentShardRequests: tc.maxConcurrentShardRequests };
  600. expect(getMaxConcurrenShardRequestOrDefault(options as ElasticsearchOptions)).toBe(
  601. tc.expectedMaxConcurrentShardRequests
  602. );
  603. });
  604. });
  605. });