datasource.test.ts 41 KB

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