datasource.test.ts 39 KB

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