datasource.jest.ts 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  1. import _ from 'lodash';
  2. import moment from 'moment';
  3. import q from 'q';
  4. import {
  5. alignRange,
  6. determineQueryHints,
  7. extractRuleMappingFromGroups,
  8. PrometheusDatasource,
  9. prometheusSpecialRegexEscape,
  10. prometheusRegularEscape,
  11. addLabelToQuery,
  12. } from '../datasource';
  13. jest.mock('../metric_find_query');
  14. describe('PrometheusDatasource', () => {
  15. let ctx: any = {};
  16. let instanceSettings = {
  17. url: 'proxied',
  18. directUrl: 'direct',
  19. user: 'test',
  20. password: 'mupp',
  21. jsonData: {},
  22. };
  23. ctx.backendSrvMock = {};
  24. ctx.templateSrvMock = {
  25. replace: a => a,
  26. };
  27. ctx.timeSrvMock = {
  28. timeRange: () => {
  29. return {
  30. from: moment(1531468681),
  31. to: moment(1531489712),
  32. };
  33. },
  34. };
  35. beforeEach(() => {
  36. ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
  37. });
  38. describe('Datasource metadata requests', () => {
  39. it('should perform a GET request with the default config', () => {
  40. ctx.backendSrvMock.datasourceRequest = jest.fn();
  41. ctx.ds.metadataRequest('/foo');
  42. expect(ctx.backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
  43. expect(ctx.backendSrvMock.datasourceRequest.mock.calls[0][0].method).toBe('GET');
  44. });
  45. it('should still perform a GET request with the DS HTTP method set to POST', () => {
  46. ctx.backendSrvMock.datasourceRequest = jest.fn();
  47. const postSettings = _.cloneDeep(instanceSettings);
  48. postSettings.jsonData.httpMethod = 'POST';
  49. const ds = new PrometheusDatasource(postSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
  50. ds.metadataRequest('/foo');
  51. expect(ctx.backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
  52. expect(ctx.backendSrvMock.datasourceRequest.mock.calls[0][0].method).toBe('GET');
  53. });
  54. });
  55. describe('When performing performSuggestQuery', () => {
  56. it('should cache response', async () => {
  57. ctx.backendSrvMock.datasourceRequest.mockReturnValue(
  58. Promise.resolve({
  59. status: 'success',
  60. data: { data: ['value1', 'value2', 'value3'] },
  61. })
  62. );
  63. let results = await ctx.ds.performSuggestQuery('value', true);
  64. expect(results).toHaveLength(3);
  65. ctx.backendSrvMock.datasourceRequest.mockReset();
  66. results = await ctx.ds.performSuggestQuery('value', true);
  67. expect(results).toHaveLength(3);
  68. });
  69. });
  70. describe('When converting prometheus histogram to heatmap format', () => {
  71. beforeEach(() => {
  72. ctx.query = {
  73. range: { from: moment(1443454528000), to: moment(1443454528000) },
  74. targets: [{ expr: 'test{job="testjob"}', format: 'heatmap', legendFormat: '{{le}}' }],
  75. interval: '1s',
  76. };
  77. });
  78. it('should convert cumullative histogram to ordinary', () => {
  79. const resultMock = [
  80. {
  81. metric: { __name__: 'metric', job: 'testjob', le: '10' },
  82. values: [[1443454528.0, '10'], [1443454528.0, '10']],
  83. },
  84. {
  85. metric: { __name__: 'metric', job: 'testjob', le: '20' },
  86. values: [[1443454528.0, '20'], [1443454528.0, '10']],
  87. },
  88. {
  89. metric: { __name__: 'metric', job: 'testjob', le: '30' },
  90. values: [[1443454528.0, '25'], [1443454528.0, '10']],
  91. },
  92. ];
  93. const responseMock = { data: { data: { result: resultMock } } };
  94. const expected = [
  95. {
  96. target: '10',
  97. datapoints: [[10, 1443454528000], [10, 1443454528000]],
  98. },
  99. {
  100. target: '20',
  101. datapoints: [[10, 1443454528000], [0, 1443454528000]],
  102. },
  103. {
  104. target: '30',
  105. datapoints: [[5, 1443454528000], [0, 1443454528000]],
  106. },
  107. ];
  108. ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock);
  109. return ctx.ds.query(ctx.query).then(result => {
  110. let results = result.data;
  111. return expect(results).toMatchObject(expected);
  112. });
  113. });
  114. it('should sort series by label value', () => {
  115. const resultMock = [
  116. {
  117. metric: { __name__: 'metric', job: 'testjob', le: '2' },
  118. values: [[1443454528.0, '10'], [1443454528.0, '10']],
  119. },
  120. {
  121. metric: { __name__: 'metric', job: 'testjob', le: '4' },
  122. values: [[1443454528.0, '20'], [1443454528.0, '10']],
  123. },
  124. {
  125. metric: { __name__: 'metric', job: 'testjob', le: '+Inf' },
  126. values: [[1443454528.0, '25'], [1443454528.0, '10']],
  127. },
  128. {
  129. metric: { __name__: 'metric', job: 'testjob', le: '1' },
  130. values: [[1443454528.0, '25'], [1443454528.0, '10']],
  131. },
  132. ];
  133. const responseMock = { data: { data: { result: resultMock } } };
  134. const expected = ['1', '2', '4', '+Inf'];
  135. ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock);
  136. return ctx.ds.query(ctx.query).then(result => {
  137. let seriesLabels = _.map(result.data, 'target');
  138. return expect(seriesLabels).toEqual(expected);
  139. });
  140. });
  141. });
  142. describe('alignRange', () => {
  143. it('does not modify already aligned intervals with perfect step', () => {
  144. const range = alignRange(0, 3, 3);
  145. expect(range.start).toEqual(0);
  146. expect(range.end).toEqual(3);
  147. });
  148. it('does modify end-aligned intervals to reflect number of steps possible', () => {
  149. const range = alignRange(1, 6, 3);
  150. expect(range.start).toEqual(0);
  151. expect(range.end).toEqual(6);
  152. });
  153. it('does align intervals that are a multiple of steps', () => {
  154. const range = alignRange(1, 4, 3);
  155. expect(range.start).toEqual(0);
  156. expect(range.end).toEqual(6);
  157. });
  158. it('does align intervals that are not a multiple of steps', () => {
  159. const range = alignRange(1, 5, 3);
  160. expect(range.start).toEqual(0);
  161. expect(range.end).toEqual(6);
  162. });
  163. });
  164. describe('determineQueryHints()', () => {
  165. it('returns no hints for no series', () => {
  166. expect(determineQueryHints([])).toEqual([]);
  167. });
  168. it('returns no hints for empty series', () => {
  169. expect(determineQueryHints([{ datapoints: [], query: '' }])).toEqual([null]);
  170. });
  171. it('returns no hint for a monotonously decreasing series', () => {
  172. const series = [{ datapoints: [[23, 1000], [22, 1001]], query: 'metric', responseIndex: 0 }];
  173. const hints = determineQueryHints(series);
  174. expect(hints).toEqual([null]);
  175. });
  176. it('returns a rate hint for a monotonously increasing series', () => {
  177. const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'metric', responseIndex: 0 }];
  178. const hints = determineQueryHints(series);
  179. expect(hints.length).toBe(1);
  180. expect(hints[0]).toMatchObject({
  181. label: 'Time series is monotonously increasing.',
  182. index: 0,
  183. fix: {
  184. action: {
  185. type: 'ADD_RATE',
  186. query: 'metric',
  187. },
  188. },
  189. });
  190. });
  191. it('returns a histogram hint for a bucket series', () => {
  192. const series = [{ datapoints: [[23, 1000]], query: 'metric_bucket', responseIndex: 0 }];
  193. const hints = determineQueryHints(series);
  194. expect(hints.length).toBe(1);
  195. expect(hints[0]).toMatchObject({
  196. label: 'Time series has buckets, you probably wanted a histogram.',
  197. index: 0,
  198. fix: {
  199. action: {
  200. type: 'ADD_HISTOGRAM_QUANTILE',
  201. query: 'metric_bucket',
  202. },
  203. },
  204. });
  205. });
  206. });
  207. describe('extractRuleMappingFromGroups()', () => {
  208. it('returns empty mapping for no rule groups', () => {
  209. expect(extractRuleMappingFromGroups([])).toEqual({});
  210. });
  211. it('returns a mapping for recording rules only', () => {
  212. const groups = [
  213. {
  214. rules: [
  215. {
  216. name: 'HighRequestLatency',
  217. query: 'job:request_latency_seconds:mean5m{job="myjob"} > 0.5',
  218. type: 'alerting',
  219. },
  220. {
  221. name: 'job:http_inprogress_requests:sum',
  222. query: 'sum(http_inprogress_requests) by (job)',
  223. type: 'recording',
  224. },
  225. ],
  226. file: '/rules.yaml',
  227. interval: 60,
  228. name: 'example',
  229. },
  230. ];
  231. const mapping = extractRuleMappingFromGroups(groups);
  232. expect(mapping).toEqual({ 'job:http_inprogress_requests:sum': 'sum(http_inprogress_requests) by (job)' });
  233. });
  234. });
  235. describe('Prometheus regular escaping', () => {
  236. it('should not escape non-string', () => {
  237. expect(prometheusRegularEscape(12)).toEqual(12);
  238. });
  239. it('should not escape simple string', () => {
  240. expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
  241. });
  242. it("should escape '", () => {
  243. expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
  244. });
  245. it('should escape multiple characters', () => {
  246. expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
  247. });
  248. });
  249. describe('Prometheus regexes escaping', () => {
  250. it('should not escape simple string', () => {
  251. expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
  252. });
  253. it('should escape $^*+?.()\\', () => {
  254. expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass");
  255. expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass');
  256. expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass');
  257. expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass');
  258. expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass');
  259. expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass');
  260. expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass');
  261. expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass');
  262. expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass');
  263. expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass');
  264. expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass');
  265. expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass');
  266. expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass');
  267. expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass');
  268. });
  269. it('should escape multiple special characters', () => {
  270. expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
  271. });
  272. });
  273. describe('metricFindQuery', () => {
  274. beforeEach(() => {
  275. let query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
  276. ctx.templateSrvMock.replace = jest.fn();
  277. ctx.timeSrvMock.timeRange = () => {
  278. return {
  279. from: moment(1531468681),
  280. to: moment(1531489712),
  281. };
  282. };
  283. ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
  284. ctx.ds.metricFindQuery(query);
  285. });
  286. it('should call templateSrv.replace with scopedVars', () => {
  287. expect(ctx.templateSrvMock.replace.mock.calls[0][1]).toBeDefined();
  288. });
  289. it('should have the correct range and range_ms', () => {
  290. let range = ctx.templateSrvMock.replace.mock.calls[0][1].__range;
  291. let rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms;
  292. expect(range).toEqual({ text: '21s', value: '21s' });
  293. expect(rangeMs).toEqual({ text: 21031, value: 21031 });
  294. });
  295. it('should pass the default interval value', () => {
  296. let interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval;
  297. let intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms;
  298. expect(interval).toEqual({ text: '15s', value: '15s' });
  299. expect(intervalMs).toEqual({ text: 15000, value: 15000 });
  300. });
  301. });
  302. describe('addLabelToQuery()', () => {
  303. expect(() => {
  304. addLabelToQuery('foo', '', '');
  305. }).toThrow();
  306. expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}');
  307. expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}');
  308. expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"}');
  309. expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"} + metric{bar="baz"}');
  310. expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})');
  311. expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe(
  312. 'foo{bar="baz",x="yy"} * metric{a="bb",bar="baz",y="zz"} * metric2{bar="baz"}'
  313. );
  314. expect(addLabelToQuery('sum by (xx) (foo)', 'bar', 'baz')).toBe('sum by (xx) (foo{bar="baz"})');
  315. expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe(
  316. 'foo{bar="baz",instance="my-host.com:9100"}'
  317. );
  318. });
  319. });
  320. const SECOND = 1000;
  321. const MINUTE = 60 * SECOND;
  322. const HOUR = 60 * MINUTE;
  323. const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND);
  324. let ctx = <any>{};
  325. let instanceSettings = {
  326. url: 'proxied',
  327. directUrl: 'direct',
  328. user: 'test',
  329. password: 'mupp',
  330. jsonData: { httpMethod: 'GET' },
  331. };
  332. let backendSrv = <any>{
  333. datasourceRequest: jest.fn(),
  334. };
  335. let templateSrv = {
  336. replace: jest.fn(str => str),
  337. };
  338. let timeSrv = {
  339. timeRange: () => {
  340. return { to: { diff: () => 2000 }, from: '' };
  341. },
  342. };
  343. describe('PrometheusDatasource', () => {
  344. describe('When querying prometheus with one target using query editor target spec', async () => {
  345. var results;
  346. var query = {
  347. range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
  348. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  349. interval: '60s',
  350. };
  351. // Interval alignment with step
  352. var urlExpected =
  353. 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60';
  354. beforeEach(async () => {
  355. let response = {
  356. data: {
  357. status: 'success',
  358. data: {
  359. resultType: 'matrix',
  360. result: [
  361. {
  362. metric: { __name__: 'test', job: 'testjob' },
  363. values: [[60, '3846']],
  364. },
  365. ],
  366. },
  367. },
  368. };
  369. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  370. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  371. await ctx.ds.query(query).then(function(data) {
  372. results = data;
  373. });
  374. });
  375. it('should generate the correct query', () => {
  376. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  377. expect(res.method).toBe('GET');
  378. expect(res.url).toBe(urlExpected);
  379. });
  380. it('should return series list', async () => {
  381. expect(results.data.length).toBe(1);
  382. expect(results.data[0].target).toBe('test{job="testjob"}');
  383. });
  384. });
  385. describe('When querying prometheus with one target which return multiple series', () => {
  386. var results;
  387. var start = 60;
  388. var end = 360;
  389. var step = 60;
  390. var query = {
  391. range: { from: time({ seconds: start }), to: time({ seconds: end }) },
  392. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  393. interval: '60s',
  394. };
  395. beforeEach(async () => {
  396. let response = {
  397. status: 'success',
  398. data: {
  399. data: {
  400. resultType: 'matrix',
  401. result: [
  402. {
  403. metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
  404. values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']],
  405. },
  406. {
  407. metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
  408. values: [[start + step * 2, '4846']],
  409. },
  410. ],
  411. },
  412. },
  413. };
  414. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  415. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  416. await ctx.ds.query(query).then(function(data) {
  417. results = data;
  418. });
  419. });
  420. it('should be same length', () => {
  421. expect(results.data.length).toBe(2);
  422. expect(results.data[0].datapoints.length).toBe((end - start) / step + 1);
  423. expect(results.data[1].datapoints.length).toBe((end - start) / step + 1);
  424. });
  425. it('should fill null until first datapoint in response', () => {
  426. expect(results.data[0].datapoints[0][1]).toBe(start * 1000);
  427. expect(results.data[0].datapoints[0][0]).toBe(null);
  428. expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000);
  429. expect(results.data[0].datapoints[1][0]).toBe(3846);
  430. });
  431. it('should fill null after last datapoint in response', () => {
  432. var length = (end - start) / step + 1;
  433. expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000);
  434. expect(results.data[0].datapoints[length - 2][0]).toBe(3848);
  435. expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000);
  436. expect(results.data[0].datapoints[length - 1][0]).toBe(null);
  437. });
  438. it('should fill null at gap between series', () => {
  439. expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000);
  440. expect(results.data[0].datapoints[2][0]).toBe(null);
  441. expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000);
  442. expect(results.data[1].datapoints[1][0]).toBe(null);
  443. expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000);
  444. expect(results.data[1].datapoints[3][0]).toBe(null);
  445. });
  446. });
  447. describe('When querying prometheus with one target and instant = true', () => {
  448. var results;
  449. var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
  450. var query = {
  451. range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
  452. targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
  453. interval: '60s',
  454. };
  455. beforeEach(async () => {
  456. let response = {
  457. status: 'success',
  458. data: {
  459. data: {
  460. resultType: 'vector',
  461. result: [
  462. {
  463. metric: { __name__: 'test', job: 'testjob' },
  464. value: [123, '3846'],
  465. },
  466. ],
  467. },
  468. },
  469. };
  470. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  471. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  472. await ctx.ds.query(query).then(function(data) {
  473. results = data;
  474. });
  475. });
  476. it('should generate the correct query', () => {
  477. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  478. expect(res.method).toBe('GET');
  479. expect(res.url).toBe(urlExpected);
  480. });
  481. it('should return series list', () => {
  482. expect(results.data.length).toBe(1);
  483. expect(results.data[0].target).toBe('test{job="testjob"}');
  484. });
  485. });
  486. describe('When performing annotationQuery', () => {
  487. var results;
  488. var options = {
  489. annotation: {
  490. expr: 'ALERTS{alertstate="firing"}',
  491. tagKeys: 'job',
  492. titleFormat: '{{alertname}}',
  493. textFormat: '{{instance}}',
  494. },
  495. range: {
  496. from: time({ seconds: 63 }),
  497. to: time({ seconds: 123 }),
  498. },
  499. };
  500. beforeEach(async () => {
  501. let response = {
  502. status: 'success',
  503. data: {
  504. data: {
  505. resultType: 'matrix',
  506. result: [
  507. {
  508. metric: {
  509. __name__: 'ALERTS',
  510. alertname: 'InstanceDown',
  511. alertstate: 'firing',
  512. instance: 'testinstance',
  513. job: 'testjob',
  514. },
  515. values: [[123, '1']],
  516. },
  517. ],
  518. },
  519. },
  520. };
  521. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  522. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  523. await ctx.ds.annotationQuery(options).then(function(data) {
  524. results = data;
  525. });
  526. });
  527. it('should return annotation list', () => {
  528. expect(results.length).toBe(1);
  529. expect(results[0].tags).toContain('testjob');
  530. expect(results[0].title).toBe('InstanceDown');
  531. expect(results[0].text).toBe('testinstance');
  532. expect(results[0].time).toBe(123 * 1000);
  533. });
  534. });
  535. describe('When resultFormat is table and instant = true', () => {
  536. var results;
  537. var query = {
  538. range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
  539. targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
  540. interval: '60s',
  541. };
  542. beforeEach(async () => {
  543. let response = {
  544. status: 'success',
  545. data: {
  546. data: {
  547. resultType: 'vector',
  548. result: [
  549. {
  550. metric: { __name__: 'test', job: 'testjob' },
  551. value: [123, '3846'],
  552. },
  553. ],
  554. },
  555. },
  556. };
  557. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  558. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  559. await ctx.ds.query(query).then(function(data) {
  560. results = data;
  561. });
  562. });
  563. it('should return result', () => {
  564. expect(results).not.toBe(null);
  565. });
  566. });
  567. describe('The "step" query parameter', () => {
  568. var response = {
  569. status: 'success',
  570. data: {
  571. data: {
  572. resultType: 'matrix',
  573. result: [],
  574. },
  575. },
  576. };
  577. it('should be min interval when greater than auto interval', async () => {
  578. let query = {
  579. // 6 minute range
  580. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  581. targets: [
  582. {
  583. expr: 'test',
  584. interval: '10s',
  585. },
  586. ],
  587. interval: '5s',
  588. };
  589. let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
  590. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  591. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  592. await ctx.ds.query(query);
  593. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  594. expect(res.method).toBe('GET');
  595. expect(res.url).toBe(urlExpected);
  596. });
  597. it('step should never go below 1', async () => {
  598. var query = {
  599. // 6 minute range
  600. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  601. targets: [{ expr: 'test' }],
  602. interval: '100ms',
  603. };
  604. var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
  605. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  606. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  607. await ctx.ds.query(query);
  608. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  609. expect(res.method).toBe('GET');
  610. expect(res.url).toBe(urlExpected);
  611. });
  612. it('should be auto interval when greater than min interval', async () => {
  613. var query = {
  614. // 6 minute range
  615. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  616. targets: [
  617. {
  618. expr: 'test',
  619. interval: '5s',
  620. },
  621. ],
  622. interval: '10s',
  623. };
  624. var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
  625. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  626. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  627. await ctx.ds.query(query);
  628. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  629. expect(res.method).toBe('GET');
  630. expect(res.url).toBe(urlExpected);
  631. });
  632. it('should result in querying fewer than 11000 data points', async () => {
  633. var query = {
  634. // 6 hour range
  635. range: { from: time({ hours: 1 }), to: time({ hours: 7 }) },
  636. targets: [{ expr: 'test' }],
  637. interval: '1s',
  638. };
  639. var end = 7 * 60 * 60;
  640. var start = 60 * 60;
  641. var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
  642. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  643. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  644. await ctx.ds.query(query);
  645. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  646. expect(res.method).toBe('GET');
  647. expect(res.url).toBe(urlExpected);
  648. });
  649. it('should not apply min interval when interval * intervalFactor greater', async () => {
  650. var query = {
  651. // 6 minute range
  652. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  653. targets: [
  654. {
  655. expr: 'test',
  656. interval: '10s',
  657. intervalFactor: 10,
  658. },
  659. ],
  660. interval: '5s',
  661. };
  662. // times get rounded up to interval
  663. var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50';
  664. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  665. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  666. await ctx.ds.query(query);
  667. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  668. expect(res.method).toBe('GET');
  669. expect(res.url).toBe(urlExpected);
  670. });
  671. it('should apply min interval when interval * intervalFactor smaller', async () => {
  672. var query = {
  673. // 6 minute range
  674. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  675. targets: [
  676. {
  677. expr: 'test',
  678. interval: '15s',
  679. intervalFactor: 2,
  680. },
  681. ],
  682. interval: '5s',
  683. };
  684. var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
  685. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  686. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  687. await ctx.ds.query(query);
  688. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  689. expect(res.method).toBe('GET');
  690. expect(res.url).toBe(urlExpected);
  691. });
  692. it('should apply intervalFactor to auto interval when greater', async () => {
  693. var query = {
  694. // 6 minute range
  695. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  696. targets: [
  697. {
  698. expr: 'test',
  699. interval: '5s',
  700. intervalFactor: 10,
  701. },
  702. ],
  703. interval: '10s',
  704. };
  705. // times get aligned to interval
  706. var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100';
  707. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  708. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  709. await ctx.ds.query(query);
  710. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  711. expect(res.method).toBe('GET');
  712. expect(res.url).toBe(urlExpected);
  713. });
  714. it('should not not be affected by the 11000 data points limit when large enough', async () => {
  715. var query = {
  716. // 1 week range
  717. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  718. targets: [
  719. {
  720. expr: 'test',
  721. intervalFactor: 10,
  722. },
  723. ],
  724. interval: '10s',
  725. };
  726. var end = 7 * 24 * 60 * 60;
  727. var start = 0;
  728. var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
  729. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  730. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  731. await ctx.ds.query(query);
  732. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  733. expect(res.method).toBe('GET');
  734. expect(res.url).toBe(urlExpected);
  735. });
  736. it('should be determined by the 11000 data points limit when too small', async () => {
  737. var query = {
  738. // 1 week range
  739. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  740. targets: [
  741. {
  742. expr: 'test',
  743. intervalFactor: 10,
  744. },
  745. ],
  746. interval: '5s',
  747. };
  748. var end = 7 * 24 * 60 * 60;
  749. var start = 0;
  750. var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
  751. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  752. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  753. await ctx.ds.query(query);
  754. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  755. expect(res.method).toBe('GET');
  756. expect(res.url).toBe(urlExpected);
  757. });
  758. });
  759. describe('The __interval and __interval_ms template variables', () => {
  760. var response = {
  761. status: 'success',
  762. data: {
  763. data: {
  764. resultType: 'matrix',
  765. result: [],
  766. },
  767. },
  768. };
  769. it('should be unchanged when auto interval is greater than min interval', async () => {
  770. var query = {
  771. // 6 minute range
  772. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  773. targets: [
  774. {
  775. expr: 'rate(test[$__interval])',
  776. interval: '5s',
  777. },
  778. ],
  779. interval: '10s',
  780. scopedVars: {
  781. __interval: { text: '10s', value: '10s' },
  782. __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
  783. },
  784. };
  785. var urlExpected =
  786. 'proxied/api/v1/query_range?query=' +
  787. encodeURIComponent('rate(test[$__interval])') +
  788. '&start=60&end=420&step=10';
  789. templateSrv.replace = jest.fn(str => str);
  790. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  791. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  792. await ctx.ds.query(query);
  793. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  794. expect(res.method).toBe('GET');
  795. expect(res.url).toBe(urlExpected);
  796. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  797. __interval: {
  798. text: '10s',
  799. value: '10s',
  800. },
  801. __interval_ms: {
  802. text: 10000,
  803. value: 10000,
  804. },
  805. });
  806. });
  807. it('should be min interval when it is greater than auto interval', async () => {
  808. var query = {
  809. // 6 minute range
  810. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  811. targets: [
  812. {
  813. expr: 'rate(test[$__interval])',
  814. interval: '10s',
  815. },
  816. ],
  817. interval: '5s',
  818. scopedVars: {
  819. __interval: { text: '5s', value: '5s' },
  820. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  821. },
  822. };
  823. var urlExpected =
  824. 'proxied/api/v1/query_range?query=' +
  825. encodeURIComponent('rate(test[$__interval])') +
  826. '&start=60&end=420&step=10';
  827. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  828. templateSrv.replace = jest.fn(str => str);
  829. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  830. await ctx.ds.query(query);
  831. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  832. expect(res.method).toBe('GET');
  833. expect(res.url).toBe(urlExpected);
  834. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  835. __interval: {
  836. text: '5s',
  837. value: '5s',
  838. },
  839. __interval_ms: {
  840. text: 5000,
  841. value: 5000,
  842. },
  843. });
  844. });
  845. it('should account for intervalFactor', async () => {
  846. var query = {
  847. // 6 minute range
  848. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  849. targets: [
  850. {
  851. expr: 'rate(test[$__interval])',
  852. interval: '5s',
  853. intervalFactor: 10,
  854. },
  855. ],
  856. interval: '10s',
  857. scopedVars: {
  858. __interval: { text: '10s', value: '10s' },
  859. __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
  860. },
  861. };
  862. var urlExpected =
  863. 'proxied/api/v1/query_range?query=' +
  864. encodeURIComponent('rate(test[$__interval])') +
  865. '&start=0&end=500&step=100';
  866. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  867. templateSrv.replace = jest.fn(str => str);
  868. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  869. await ctx.ds.query(query);
  870. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  871. expect(res.method).toBe('GET');
  872. expect(res.url).toBe(urlExpected);
  873. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  874. __interval: {
  875. text: '10s',
  876. value: '10s',
  877. },
  878. __interval_ms: {
  879. text: 10000,
  880. value: 10000,
  881. },
  882. });
  883. expect(query.scopedVars.__interval.text).toBe('10s');
  884. expect(query.scopedVars.__interval.value).toBe('10s');
  885. expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
  886. expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000);
  887. });
  888. it('should be interval * intervalFactor when greater than min interval', async () => {
  889. var query = {
  890. // 6 minute range
  891. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  892. targets: [
  893. {
  894. expr: 'rate(test[$__interval])',
  895. interval: '10s',
  896. intervalFactor: 10,
  897. },
  898. ],
  899. interval: '5s',
  900. scopedVars: {
  901. __interval: { text: '5s', value: '5s' },
  902. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  903. },
  904. };
  905. var urlExpected =
  906. 'proxied/api/v1/query_range?query=' +
  907. encodeURIComponent('rate(test[$__interval])') +
  908. '&start=50&end=450&step=50';
  909. templateSrv.replace = jest.fn(str => str);
  910. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  911. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  912. await ctx.ds.query(query);
  913. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  914. expect(res.method).toBe('GET');
  915. expect(res.url).toBe(urlExpected);
  916. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  917. __interval: {
  918. text: '5s',
  919. value: '5s',
  920. },
  921. __interval_ms: {
  922. text: 5000,
  923. value: 5000,
  924. },
  925. });
  926. });
  927. it('should be min interval when greater than interval * intervalFactor', async () => {
  928. var query = {
  929. // 6 minute range
  930. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  931. targets: [
  932. {
  933. expr: 'rate(test[$__interval])',
  934. interval: '15s',
  935. intervalFactor: 2,
  936. },
  937. ],
  938. interval: '5s',
  939. scopedVars: {
  940. __interval: { text: '5s', value: '5s' },
  941. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  942. },
  943. };
  944. var urlExpected =
  945. 'proxied/api/v1/query_range?query=' +
  946. encodeURIComponent('rate(test[$__interval])') +
  947. '&start=60&end=420&step=15';
  948. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  949. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  950. await ctx.ds.query(query);
  951. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  952. expect(res.method).toBe('GET');
  953. expect(res.url).toBe(urlExpected);
  954. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  955. __interval: {
  956. text: '5s',
  957. value: '5s',
  958. },
  959. __interval_ms: {
  960. text: 5000,
  961. value: 5000,
  962. },
  963. });
  964. });
  965. it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => {
  966. var query = {
  967. // 1 week range
  968. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  969. targets: [
  970. {
  971. expr: 'rate(test[$__interval])',
  972. intervalFactor: 10,
  973. },
  974. ],
  975. interval: '5s',
  976. scopedVars: {
  977. __interval: { text: '5s', value: '5s' },
  978. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  979. },
  980. };
  981. var end = 7 * 24 * 60 * 60;
  982. var start = 0;
  983. var urlExpected =
  984. 'proxied/api/v1/query_range?query=' +
  985. encodeURIComponent('rate(test[$__interval])') +
  986. '&start=' +
  987. start +
  988. '&end=' +
  989. end +
  990. '&step=60';
  991. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  992. templateSrv.replace = jest.fn(str => str);
  993. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  994. await ctx.ds.query(query);
  995. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  996. expect(res.method).toBe('GET');
  997. expect(res.url).toBe(urlExpected);
  998. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  999. __interval: {
  1000. text: '5s',
  1001. value: '5s',
  1002. },
  1003. __interval_ms: {
  1004. text: 5000,
  1005. value: 5000,
  1006. },
  1007. });
  1008. });
  1009. });
  1010. });
  1011. describe('PrometheusDatasource for POST', () => {
  1012. // var ctx = new helpers.ServiceTestContext();
  1013. let instanceSettings = {
  1014. url: 'proxied',
  1015. directUrl: 'direct',
  1016. user: 'test',
  1017. password: 'mupp',
  1018. jsonData: { httpMethod: 'POST' },
  1019. };
  1020. describe('When querying prometheus with one target using query editor target spec', () => {
  1021. var results;
  1022. var urlExpected = 'proxied/api/v1/query_range';
  1023. var dataExpected = {
  1024. query: 'test{job="testjob"}',
  1025. start: 1 * 60,
  1026. end: 3 * 60,
  1027. step: 60,
  1028. };
  1029. var query = {
  1030. range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) },
  1031. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  1032. interval: '60s',
  1033. };
  1034. beforeEach(async () => {
  1035. let response = {
  1036. status: 'success',
  1037. data: {
  1038. data: {
  1039. resultType: 'matrix',
  1040. result: [
  1041. {
  1042. metric: { __name__: 'test', job: 'testjob' },
  1043. values: [[2 * 60, '3846']],
  1044. },
  1045. ],
  1046. },
  1047. },
  1048. };
  1049. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  1050. ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
  1051. await ctx.ds.query(query).then(function(data) {
  1052. results = data;
  1053. });
  1054. });
  1055. it('should generate the correct query', () => {
  1056. let res = backendSrv.datasourceRequest.mock.calls[0][0];
  1057. expect(res.method).toBe('POST');
  1058. expect(res.url).toBe(urlExpected);
  1059. expect(res.data).toEqual(dataExpected);
  1060. });
  1061. it('should return series list', () => {
  1062. expect(results.data.length).toBe(1);
  1063. expect(results.data[0].target).toBe('test{job="testjob"}');
  1064. });
  1065. });
  1066. });