datasource.test.ts 47 KB

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