datasource.test.ts 40 KB

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