datasource.test.ts 41 KB

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