datasource.test.ts 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434
  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/data';
  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, 0);
  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, 0);
  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, 0);
  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, 0);
  211. expect(range.start).toEqual(0);
  212. expect(range.end).toEqual(3);
  213. });
  214. it('does align intervals with local midnight -UTC offset', () => {
  215. //week range, location 4+ hours UTC offset, 24h step time
  216. const range = alignRange(4 * 60 * 60, (7 * 24 + 4) * 60 * 60, 24 * 60 * 60, -4 * 60 * 60); //04:00 UTC, 7 day range
  217. expect(range.start).toEqual(4 * 60 * 60);
  218. expect(range.end).toEqual((7 * 24 + 4) * 60 * 60);
  219. });
  220. it('does align intervals with local midnight +UTC offset', () => {
  221. //week range, location 4- hours UTC offset, 24h step time
  222. const range = alignRange(20 * 60 * 60, (8 * 24 - 4) * 60 * 60, 24 * 60 * 60, 4 * 60 * 60); //20:00 UTC on day1, 7 days later is 20:00 on day8
  223. expect(range.start).toEqual(20 * 60 * 60);
  224. expect(range.end).toEqual((8 * 24 - 4) * 60 * 60);
  225. });
  226. });
  227. describe('extractRuleMappingFromGroups()', () => {
  228. it('returns empty mapping for no rule groups', () => {
  229. expect(extractRuleMappingFromGroups([])).toEqual({});
  230. });
  231. it('returns a mapping for recording rules only', () => {
  232. const groups = [
  233. {
  234. rules: [
  235. {
  236. name: 'HighRequestLatency',
  237. query: 'job:request_latency_seconds:mean5m{job="myjob"} > 0.5',
  238. type: 'alerting',
  239. },
  240. {
  241. name: 'job:http_inprogress_requests:sum',
  242. query: 'sum(http_inprogress_requests) by (job)',
  243. type: 'recording',
  244. },
  245. ],
  246. file: '/rules.yaml',
  247. interval: 60,
  248. name: 'example',
  249. },
  250. ];
  251. const mapping = extractRuleMappingFromGroups(groups);
  252. expect(mapping).toEqual({ 'job:http_inprogress_requests:sum': 'sum(http_inprogress_requests) by (job)' });
  253. });
  254. });
  255. describe('Prometheus regular escaping', () => {
  256. it('should not escape non-string', () => {
  257. expect(prometheusRegularEscape(12)).toEqual(12);
  258. });
  259. it('should not escape simple string', () => {
  260. expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
  261. });
  262. it("should escape '", () => {
  263. expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
  264. });
  265. it('should escape multiple characters', () => {
  266. expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
  267. });
  268. });
  269. describe('Prometheus regexes escaping', () => {
  270. it('should not escape simple string', () => {
  271. expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
  272. });
  273. it('should escape $^*+?.()|\\', () => {
  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. 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. });
  290. it('should escape multiple special characters', () => {
  291. expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
  292. });
  293. });
  294. describe('When interpolating variables', () => {
  295. beforeEach(() => {
  296. ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
  297. ctx.variable = new CustomVariable({}, {} as any);
  298. });
  299. describe('and value is a string', () => {
  300. it('should only escape single quotes', () => {
  301. expect(ctx.ds.interpolateQueryExpr("abc'$^*{}[]+?.()|", ctx.variable)).toEqual("abc\\\\'$^*{}[]+?.()|");
  302. });
  303. });
  304. describe('and value is a number', () => {
  305. it('should return a number', () => {
  306. expect(ctx.ds.interpolateQueryExpr(1000, ctx.variable)).toEqual(1000);
  307. });
  308. });
  309. describe('and variable allows multi-value', () => {
  310. beforeEach(() => {
  311. ctx.variable.multi = true;
  312. });
  313. it('should regex escape values if the value is a string', () => {
  314. expect(ctx.ds.interpolateQueryExpr('looking*glass', ctx.variable)).toEqual('looking\\\\*glass');
  315. });
  316. it('should return pipe separated values if the value is an array of strings', () => {
  317. expect(ctx.ds.interpolateQueryExpr(['a|bc', 'de|f'], ctx.variable)).toEqual('a\\\\|bc|de\\\\|f');
  318. });
  319. });
  320. describe('and variable allows all', () => {
  321. beforeEach(() => {
  322. ctx.variable.includeAll = true;
  323. });
  324. it('should regex escape values if the array is a string', () => {
  325. expect(ctx.ds.interpolateQueryExpr('looking*glass', ctx.variable)).toEqual('looking\\\\*glass');
  326. });
  327. it('should return pipe separated values if the value is an array of strings', () => {
  328. expect(ctx.ds.interpolateQueryExpr(['a|bc', 'de|f'], ctx.variable)).toEqual('a\\\\|bc|de\\\\|f');
  329. });
  330. });
  331. });
  332. describe('metricFindQuery', () => {
  333. beforeEach(() => {
  334. const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
  335. ctx.templateSrvMock.replace = jest.fn();
  336. ctx.timeSrvMock.timeRange = () => {
  337. return {
  338. from: dateTime(1531468681),
  339. to: dateTime(1531489712),
  340. };
  341. };
  342. ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
  343. ctx.ds.metricFindQuery(query);
  344. });
  345. it('should call templateSrv.replace with scopedVars', () => {
  346. expect(ctx.templateSrvMock.replace.mock.calls[0][1]).toBeDefined();
  347. });
  348. it('should have the correct range and range_ms', () => {
  349. const range = ctx.templateSrvMock.replace.mock.calls[0][1].__range;
  350. const rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms;
  351. const rangeS = ctx.templateSrvMock.replace.mock.calls[0][1].__range_s;
  352. expect(range).toEqual({ text: '21s', value: '21s' });
  353. expect(rangeMs).toEqual({ text: 21031, value: 21031 });
  354. expect(rangeS).toEqual({ text: 21, value: 21 });
  355. });
  356. it('should pass the default interval value', () => {
  357. const interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval;
  358. const intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms;
  359. expect(interval).toEqual({ text: '15s', value: '15s' });
  360. expect(intervalMs).toEqual({ text: 15000, value: 15000 });
  361. });
  362. });
  363. });
  364. const SECOND = 1000;
  365. const MINUTE = 60 * SECOND;
  366. const HOUR = 60 * MINUTE;
  367. const time = ({ hours = 0, seconds = 0, minutes = 0 }) => dateTime(hours * HOUR + minutes * MINUTE + seconds * SECOND);
  368. const ctx = {} as any;
  369. const instanceSettings = ({
  370. url: 'proxied',
  371. directUrl: 'direct',
  372. user: 'test',
  373. password: 'mupp',
  374. jsonData: { httpMethod: 'GET' },
  375. } as unknown) as DataSourceInstanceSettings<PromOptions>;
  376. const backendSrv = {
  377. datasourceRequest: jest.fn(),
  378. } as any;
  379. const templateSrv = ({
  380. getAdhocFilters: (): any[] => [],
  381. replace: jest.fn(str => str),
  382. } as unknown) as TemplateSrv;
  383. const timeSrv = ({
  384. timeRange: () => {
  385. return {
  386. from: dateTime(1531468681),
  387. to: dateTime(1531468681 + 2000),
  388. };
  389. },
  390. } as unknown) as TimeSrv;
  391. describe('PrometheusDatasource', () => {
  392. describe('When querying prometheus with one target using query editor target spec', () => {
  393. describe('and query syntax is valid', () => {
  394. let results: any;
  395. const query = {
  396. range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
  397. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  398. interval: '60s',
  399. };
  400. // Interval alignment with step
  401. const urlExpected =
  402. 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=180&step=60';
  403. beforeEach(async () => {
  404. const response = {
  405. data: {
  406. status: 'success',
  407. data: {
  408. resultType: 'matrix',
  409. result: [
  410. {
  411. metric: { __name__: 'test', job: 'testjob' },
  412. values: [[60, '3846']],
  413. },
  414. ],
  415. },
  416. },
  417. };
  418. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  419. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  420. await ctx.ds.query(query).then((data: any) => {
  421. results = data;
  422. });
  423. });
  424. it('should generate the correct query', () => {
  425. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  426. expect(res.method).toBe('GET');
  427. expect(res.url).toBe(urlExpected);
  428. });
  429. it('should return series list', async () => {
  430. expect(results.data.length).toBe(1);
  431. expect(results.data[0].target).toBe('test{job="testjob"}');
  432. });
  433. });
  434. describe('and query syntax is invalid', () => {
  435. let results: string;
  436. const query = {
  437. range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
  438. targets: [{ expr: 'tes;;t{job="testjob"}', format: 'time_series' }],
  439. interval: '60s',
  440. };
  441. const errMessage = 'parse error at char 25: could not parse remaining input';
  442. const response = {
  443. data: {
  444. status: 'error',
  445. errorType: 'bad_data',
  446. error: errMessage,
  447. },
  448. };
  449. beforeEach(async () => {
  450. backendSrv.datasourceRequest = jest.fn(() => Promise.reject(response));
  451. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  452. await ctx.ds.query(query).catch((e: any) => {
  453. results = e.message;
  454. });
  455. });
  456. it('should generate an error', () => {
  457. expect(results).toBe(`"${errMessage}"`);
  458. });
  459. });
  460. });
  461. describe('When querying prometheus with one target which returns multiple series', () => {
  462. let results: any;
  463. const start = 60;
  464. const end = 360;
  465. const step = 60;
  466. const query = {
  467. range: { from: time({ seconds: start }), to: time({ seconds: end }) },
  468. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  469. interval: '60s',
  470. };
  471. beforeEach(async () => {
  472. const response = {
  473. status: 'success',
  474. data: {
  475. data: {
  476. resultType: 'matrix',
  477. result: [
  478. {
  479. metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
  480. values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']],
  481. },
  482. {
  483. metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
  484. values: [[start + step * 2, '4846']],
  485. },
  486. ],
  487. },
  488. },
  489. };
  490. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  491. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  492. await ctx.ds.query(query).then((data: any) => {
  493. results = data;
  494. });
  495. });
  496. it('should be same length', () => {
  497. expect(results.data.length).toBe(2);
  498. expect(results.data[0].datapoints.length).toBe((end - start) / step + 1);
  499. expect(results.data[1].datapoints.length).toBe((end - start) / step + 1);
  500. });
  501. it('should fill null until first datapoint in response', () => {
  502. expect(results.data[0].datapoints[0][1]).toBe(start * 1000);
  503. expect(results.data[0].datapoints[0][0]).toBe(null);
  504. expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000);
  505. expect(results.data[0].datapoints[1][0]).toBe(3846);
  506. });
  507. it('should fill null after last datapoint in response', () => {
  508. const length = (end - start) / step + 1;
  509. expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000);
  510. expect(results.data[0].datapoints[length - 2][0]).toBe(3848);
  511. expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000);
  512. expect(results.data[0].datapoints[length - 1][0]).toBe(null);
  513. });
  514. it('should fill null at gap between series', () => {
  515. expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000);
  516. expect(results.data[0].datapoints[2][0]).toBe(null);
  517. expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000);
  518. expect(results.data[1].datapoints[1][0]).toBe(null);
  519. expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000);
  520. expect(results.data[1].datapoints[3][0]).toBe(null);
  521. });
  522. });
  523. describe('When querying prometheus with one target and instant = true', () => {
  524. let results: any;
  525. const urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
  526. const query = {
  527. range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
  528. targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
  529. interval: '60s',
  530. };
  531. beforeEach(async () => {
  532. const response = {
  533. status: 'success',
  534. data: {
  535. data: {
  536. resultType: 'vector',
  537. result: [
  538. {
  539. metric: { __name__: 'test', job: 'testjob' },
  540. value: [123, '3846'],
  541. },
  542. ],
  543. },
  544. },
  545. };
  546. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  547. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  548. await ctx.ds.query(query).then((data: any) => {
  549. results = data;
  550. });
  551. });
  552. it('should generate the correct query', () => {
  553. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  554. expect(res.method).toBe('GET');
  555. expect(res.url).toBe(urlExpected);
  556. });
  557. it('should return series list', () => {
  558. expect(results.data.length).toBe(1);
  559. expect(results.data[0].target).toBe('test{job="testjob"}');
  560. });
  561. });
  562. describe('When performing annotationQuery', () => {
  563. let results: any;
  564. const options: any = {
  565. annotation: {
  566. expr: 'ALERTS{alertstate="firing"}',
  567. tagKeys: 'job',
  568. titleFormat: '{{alertname}}',
  569. textFormat: '{{instance}}',
  570. },
  571. range: {
  572. from: time({ seconds: 63 }),
  573. to: time({ seconds: 123 }),
  574. },
  575. };
  576. const response = {
  577. status: 'success',
  578. data: {
  579. data: {
  580. resultType: 'matrix',
  581. result: [
  582. {
  583. metric: {
  584. __name__: 'ALERTS',
  585. alertname: 'InstanceDown',
  586. alertstate: 'firing',
  587. instance: 'testinstance',
  588. job: 'testjob',
  589. },
  590. values: [[123, '1']],
  591. },
  592. ],
  593. },
  594. },
  595. };
  596. describe('when time series query is cancelled', () => {
  597. it('should return empty results', async () => {
  598. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve({ cancelled: true }));
  599. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  600. await ctx.ds.annotationQuery(options).then((data: any) => {
  601. results = data;
  602. });
  603. expect(results).toEqual([]);
  604. });
  605. });
  606. describe('not use useValueForTime', () => {
  607. beforeEach(async () => {
  608. options.annotation.useValueForTime = false;
  609. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  610. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  611. await ctx.ds.annotationQuery(options).then((data: any) => {
  612. results = data;
  613. });
  614. });
  615. it('should return annotation list', () => {
  616. expect(results.length).toBe(1);
  617. expect(results[0].tags).toContain('testjob');
  618. expect(results[0].title).toBe('InstanceDown');
  619. expect(results[0].text).toBe('testinstance');
  620. expect(results[0].time).toBe(123 * 1000);
  621. });
  622. });
  623. describe('use useValueForTime', () => {
  624. beforeEach(async () => {
  625. options.annotation.useValueForTime = true;
  626. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  627. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  628. await ctx.ds.annotationQuery(options).then((data: any) => {
  629. results = data;
  630. });
  631. });
  632. it('should return annotation list', () => {
  633. expect(results[0].time).toEqual(1);
  634. });
  635. });
  636. describe('step parameter', () => {
  637. beforeEach(() => {
  638. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  639. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  640. });
  641. it('should use default step for short range if no interval is given', () => {
  642. const query = {
  643. ...options,
  644. range: {
  645. from: time({ seconds: 63 }),
  646. to: time({ seconds: 123 }),
  647. },
  648. };
  649. ctx.ds.annotationQuery(query);
  650. const req = backendSrv.datasourceRequest.mock.calls[0][0];
  651. expect(req.url).toContain('step=60');
  652. });
  653. it('should use custom step for short range', () => {
  654. const annotation = {
  655. ...options.annotation,
  656. step: '10s',
  657. };
  658. const query = {
  659. ...options,
  660. annotation,
  661. range: {
  662. from: time({ seconds: 63 }),
  663. to: time({ seconds: 123 }),
  664. },
  665. };
  666. ctx.ds.annotationQuery(query);
  667. const req = backendSrv.datasourceRequest.mock.calls[0][0];
  668. expect(req.url).toContain('step=10');
  669. });
  670. it('should use custom step for short range', () => {
  671. const annotation = {
  672. ...options.annotation,
  673. step: '10s',
  674. };
  675. const query = {
  676. ...options,
  677. annotation,
  678. range: {
  679. from: time({ seconds: 63 }),
  680. to: time({ seconds: 123 }),
  681. },
  682. };
  683. ctx.ds.annotationQuery(query);
  684. const req = backendSrv.datasourceRequest.mock.calls[0][0];
  685. expect(req.url).toContain('step=10');
  686. });
  687. it('should use dynamic step on long ranges if no option was given', () => {
  688. const query = {
  689. ...options,
  690. range: {
  691. from: time({ seconds: 63 }),
  692. to: time({ hours: 24 * 30, seconds: 63 }),
  693. },
  694. };
  695. ctx.ds.annotationQuery(query);
  696. const req = backendSrv.datasourceRequest.mock.calls[0][0];
  697. // Range in seconds: (to - from) / 1000
  698. // Max_datapoints: 11000
  699. // Step: range / max_datapoints
  700. const step = 236;
  701. expect(req.url).toContain(`step=${step}`);
  702. });
  703. });
  704. });
  705. describe('When resultFormat is table and instant = true', () => {
  706. let results: any;
  707. const query = {
  708. range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
  709. targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
  710. interval: '60s',
  711. };
  712. beforeEach(async () => {
  713. const response = {
  714. status: 'success',
  715. data: {
  716. data: {
  717. resultType: 'vector',
  718. result: [
  719. {
  720. metric: { __name__: 'test', job: 'testjob' },
  721. value: [123, '3846'],
  722. },
  723. ],
  724. },
  725. },
  726. };
  727. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  728. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  729. await ctx.ds.query(query).then((data: any) => {
  730. results = data;
  731. });
  732. });
  733. it('should return result', () => {
  734. expect(results).not.toBe(null);
  735. });
  736. });
  737. describe('The "step" query parameter', () => {
  738. const response = {
  739. status: 'success',
  740. data: {
  741. data: {
  742. resultType: 'matrix',
  743. result: [] as DataQueryResponseData[],
  744. },
  745. },
  746. };
  747. it('should be min interval when greater than auto interval', async () => {
  748. const query = {
  749. // 6 minute range
  750. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  751. targets: [
  752. {
  753. expr: 'test',
  754. interval: '10s',
  755. },
  756. ],
  757. interval: '5s',
  758. };
  759. const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
  760. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  761. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  762. await ctx.ds.query(query);
  763. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  764. expect(res.method).toBe('GET');
  765. expect(res.url).toBe(urlExpected);
  766. });
  767. it('step should never go below 1', async () => {
  768. const query = {
  769. // 6 minute range
  770. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  771. targets: [{ expr: 'test' }],
  772. interval: '100ms',
  773. };
  774. const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
  775. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  776. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  777. await ctx.ds.query(query);
  778. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  779. expect(res.method).toBe('GET');
  780. expect(res.url).toBe(urlExpected);
  781. });
  782. it('should be auto interval when greater than min interval', async () => {
  783. const query = {
  784. // 6 minute range
  785. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  786. targets: [
  787. {
  788. expr: 'test',
  789. interval: '5s',
  790. },
  791. ],
  792. interval: '10s',
  793. };
  794. const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
  795. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  796. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  797. await ctx.ds.query(query);
  798. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  799. expect(res.method).toBe('GET');
  800. expect(res.url).toBe(urlExpected);
  801. });
  802. it('should result in querying fewer than 11000 data points', async () => {
  803. const query = {
  804. // 6 hour range
  805. range: { from: time({ hours: 1 }), to: time({ hours: 7 }) },
  806. targets: [{ expr: 'test' }],
  807. interval: '1s',
  808. };
  809. const end = 7 * 60 * 60;
  810. const start = 60 * 60;
  811. const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
  812. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  813. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  814. await ctx.ds.query(query);
  815. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  816. expect(res.method).toBe('GET');
  817. expect(res.url).toBe(urlExpected);
  818. });
  819. it('should not apply min interval when interval * intervalFactor greater', async () => {
  820. const query = {
  821. // 6 minute range
  822. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  823. targets: [
  824. {
  825. expr: 'test',
  826. interval: '10s',
  827. intervalFactor: 10,
  828. },
  829. ],
  830. interval: '5s',
  831. };
  832. // times get rounded up to interval
  833. const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50';
  834. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  835. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  836. await ctx.ds.query(query);
  837. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  838. expect(res.method).toBe('GET');
  839. expect(res.url).toBe(urlExpected);
  840. });
  841. it('should apply min interval when interval * intervalFactor smaller', async () => {
  842. const query = {
  843. // 6 minute range
  844. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  845. targets: [
  846. {
  847. expr: 'test',
  848. interval: '15s',
  849. intervalFactor: 2,
  850. },
  851. ],
  852. interval: '5s',
  853. };
  854. const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
  855. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  856. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  857. await ctx.ds.query(query);
  858. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  859. expect(res.method).toBe('GET');
  860. expect(res.url).toBe(urlExpected);
  861. });
  862. it('should apply intervalFactor to auto interval when greater', async () => {
  863. const query = {
  864. // 6 minute range
  865. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  866. targets: [
  867. {
  868. expr: 'test',
  869. interval: '5s',
  870. intervalFactor: 10,
  871. },
  872. ],
  873. interval: '10s',
  874. };
  875. // times get aligned to interval
  876. const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100';
  877. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  878. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  879. await ctx.ds.query(query);
  880. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  881. expect(res.method).toBe('GET');
  882. expect(res.url).toBe(urlExpected);
  883. });
  884. it('should not not be affected by the 11000 data points limit when large enough', async () => {
  885. const query = {
  886. // 1 week range
  887. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  888. targets: [
  889. {
  890. expr: 'test',
  891. intervalFactor: 10,
  892. },
  893. ],
  894. interval: '10s',
  895. };
  896. const end = 7 * 24 * 60 * 60;
  897. const start = 0;
  898. const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
  899. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  900. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  901. await ctx.ds.query(query);
  902. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  903. expect(res.method).toBe('GET');
  904. expect(res.url).toBe(urlExpected);
  905. });
  906. it('should be determined by the 11000 data points limit when too small', async () => {
  907. const query = {
  908. // 1 week range
  909. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  910. targets: [
  911. {
  912. expr: 'test',
  913. intervalFactor: 10,
  914. },
  915. ],
  916. interval: '5s',
  917. };
  918. const end = 7 * 24 * 60 * 60;
  919. const start = 0;
  920. const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
  921. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  922. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  923. await ctx.ds.query(query);
  924. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  925. expect(res.method).toBe('GET');
  926. expect(res.url).toBe(urlExpected);
  927. });
  928. });
  929. describe('The __interval and __interval_ms template variables', () => {
  930. const response = {
  931. status: 'success',
  932. data: {
  933. data: {
  934. resultType: 'matrix',
  935. result: [] as DataQueryResponseData[],
  936. },
  937. },
  938. };
  939. it('should be unchanged when auto interval is greater than min interval', async () => {
  940. const query = {
  941. // 6 minute range
  942. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  943. targets: [
  944. {
  945. expr: 'rate(test[$__interval])',
  946. interval: '5s',
  947. },
  948. ],
  949. interval: '10s',
  950. scopedVars: {
  951. __interval: { text: '10s', value: '10s' },
  952. __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
  953. },
  954. };
  955. const urlExpected =
  956. 'proxied/api/v1/query_range?query=' +
  957. encodeURIComponent('rate(test[$__interval])') +
  958. '&start=60&end=420&step=10';
  959. templateSrv.replace = jest.fn(str => str);
  960. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  961. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  962. await ctx.ds.query(query);
  963. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  964. expect(res.method).toBe('GET');
  965. expect(res.url).toBe(urlExpected);
  966. // @ts-ignore
  967. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  968. __interval: {
  969. text: '10s',
  970. value: '10s',
  971. },
  972. __interval_ms: {
  973. text: 10000,
  974. value: 10000,
  975. },
  976. });
  977. });
  978. it('should be min interval when it is greater than auto interval', async () => {
  979. const query = {
  980. // 6 minute range
  981. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  982. targets: [
  983. {
  984. expr: 'rate(test[$__interval])',
  985. interval: '10s',
  986. },
  987. ],
  988. interval: '5s',
  989. scopedVars: {
  990. __interval: { text: '5s', value: '5s' },
  991. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  992. },
  993. };
  994. const urlExpected =
  995. 'proxied/api/v1/query_range?query=' +
  996. encodeURIComponent('rate(test[$__interval])') +
  997. '&start=60&end=420&step=10';
  998. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  999. templateSrv.replace = jest.fn(str => str);
  1000. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1001. await ctx.ds.query(query);
  1002. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  1003. expect(res.method).toBe('GET');
  1004. expect(res.url).toBe(urlExpected);
  1005. // @ts-ignore
  1006. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  1007. __interval: {
  1008. text: '5s',
  1009. value: '5s',
  1010. },
  1011. __interval_ms: {
  1012. text: 5000,
  1013. value: 5000,
  1014. },
  1015. });
  1016. });
  1017. it('should account for intervalFactor', async () => {
  1018. const query = {
  1019. // 6 minute range
  1020. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1021. targets: [
  1022. {
  1023. expr: 'rate(test[$__interval])',
  1024. interval: '5s',
  1025. intervalFactor: 10,
  1026. },
  1027. ],
  1028. interval: '10s',
  1029. scopedVars: {
  1030. __interval: { text: '10s', value: '10s' },
  1031. __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
  1032. },
  1033. };
  1034. const urlExpected =
  1035. 'proxied/api/v1/query_range?query=' +
  1036. encodeURIComponent('rate(test[$__interval])') +
  1037. '&start=0&end=400&step=100';
  1038. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  1039. templateSrv.replace = jest.fn(str => str);
  1040. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1041. await ctx.ds.query(query);
  1042. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  1043. expect(res.method).toBe('GET');
  1044. expect(res.url).toBe(urlExpected);
  1045. // @ts-ignore
  1046. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  1047. __interval: {
  1048. text: '10s',
  1049. value: '10s',
  1050. },
  1051. __interval_ms: {
  1052. text: 10000,
  1053. value: 10000,
  1054. },
  1055. });
  1056. expect(query.scopedVars.__interval.text).toBe('10s');
  1057. expect(query.scopedVars.__interval.value).toBe('10s');
  1058. expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
  1059. expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000);
  1060. });
  1061. it('should be interval * intervalFactor when greater than min interval', async () => {
  1062. const query = {
  1063. // 6 minute range
  1064. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1065. targets: [
  1066. {
  1067. expr: 'rate(test[$__interval])',
  1068. interval: '10s',
  1069. intervalFactor: 10,
  1070. },
  1071. ],
  1072. interval: '5s',
  1073. scopedVars: {
  1074. __interval: { text: '5s', value: '5s' },
  1075. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  1076. },
  1077. };
  1078. const urlExpected =
  1079. 'proxied/api/v1/query_range?query=' +
  1080. encodeURIComponent('rate(test[$__interval])') +
  1081. '&start=50&end=400&step=50';
  1082. templateSrv.replace = jest.fn(str => str);
  1083. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  1084. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1085. await ctx.ds.query(query);
  1086. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  1087. expect(res.method).toBe('GET');
  1088. expect(res.url).toBe(urlExpected);
  1089. // @ts-ignore
  1090. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  1091. __interval: {
  1092. text: '5s',
  1093. value: '5s',
  1094. },
  1095. __interval_ms: {
  1096. text: 5000,
  1097. value: 5000,
  1098. },
  1099. });
  1100. });
  1101. it('should be min interval when greater than interval * intervalFactor', async () => {
  1102. const query = {
  1103. // 6 minute range
  1104. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1105. targets: [
  1106. {
  1107. expr: 'rate(test[$__interval])',
  1108. interval: '15s',
  1109. intervalFactor: 2,
  1110. },
  1111. ],
  1112. interval: '5s',
  1113. scopedVars: {
  1114. __interval: { text: '5s', value: '5s' },
  1115. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  1116. },
  1117. };
  1118. const urlExpected =
  1119. 'proxied/api/v1/query_range?query=' +
  1120. encodeURIComponent('rate(test[$__interval])') +
  1121. '&start=60&end=420&step=15';
  1122. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  1123. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1124. await ctx.ds.query(query);
  1125. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  1126. expect(res.method).toBe('GET');
  1127. expect(res.url).toBe(urlExpected);
  1128. // @ts-ignore
  1129. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  1130. __interval: {
  1131. text: '5s',
  1132. value: '5s',
  1133. },
  1134. __interval_ms: {
  1135. text: 5000,
  1136. value: 5000,
  1137. },
  1138. });
  1139. });
  1140. it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => {
  1141. const query = {
  1142. // 1 week range
  1143. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  1144. targets: [
  1145. {
  1146. expr: 'rate(test[$__interval])',
  1147. intervalFactor: 10,
  1148. },
  1149. ],
  1150. interval: '5s',
  1151. scopedVars: {
  1152. __interval: { text: '5s', value: '5s' },
  1153. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  1154. },
  1155. };
  1156. const end = 7 * 24 * 60 * 60;
  1157. const start = 0;
  1158. const urlExpected =
  1159. 'proxied/api/v1/query_range?query=' +
  1160. encodeURIComponent('rate(test[$__interval])') +
  1161. '&start=' +
  1162. start +
  1163. '&end=' +
  1164. end +
  1165. '&step=60';
  1166. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  1167. templateSrv.replace = jest.fn(str => str);
  1168. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1169. await ctx.ds.query(query);
  1170. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  1171. expect(res.method).toBe('GET');
  1172. expect(res.url).toBe(urlExpected);
  1173. // @ts-ignore
  1174. expect(templateSrv.replace.mock.calls[0][1]).toEqual({
  1175. __interval: {
  1176. text: '5s',
  1177. value: '5s',
  1178. },
  1179. __interval_ms: {
  1180. text: 5000,
  1181. value: 5000,
  1182. },
  1183. });
  1184. });
  1185. });
  1186. describe('The __range, __range_s and __range_ms variables', () => {
  1187. const response = {
  1188. status: 'success',
  1189. data: {
  1190. data: {
  1191. resultType: 'matrix',
  1192. result: [] as DataQueryResponseData[],
  1193. },
  1194. },
  1195. };
  1196. it('should use overridden ranges, not dashboard ranges', async () => {
  1197. const expectedRangeSecond = 3600;
  1198. const expectedRangeString = '1h';
  1199. const query = {
  1200. range: {
  1201. from: time({}),
  1202. to: time({ hours: 1 }),
  1203. },
  1204. targets: [
  1205. {
  1206. expr: 'test[${__range_s}s]',
  1207. },
  1208. ],
  1209. interval: '60s',
  1210. };
  1211. const urlExpected = `proxied/api/v1/query_range?query=${encodeURIComponent(
  1212. query.targets[0].expr
  1213. )}&start=0&end=3600&step=60`;
  1214. templateSrv.replace = jest.fn(str => str);
  1215. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  1216. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1217. await ctx.ds.query(query);
  1218. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  1219. expect(res.url).toBe(urlExpected);
  1220. // @ts-ignore
  1221. expect(templateSrv.replace.mock.calls[1][1]).toEqual({
  1222. __range_s: {
  1223. text: expectedRangeSecond,
  1224. value: expectedRangeSecond,
  1225. },
  1226. __range: {
  1227. text: expectedRangeString,
  1228. value: expectedRangeString,
  1229. },
  1230. __range_ms: {
  1231. text: expectedRangeSecond * 1000,
  1232. value: expectedRangeSecond * 1000,
  1233. },
  1234. });
  1235. });
  1236. });
  1237. });
  1238. describe('PrometheusDatasource for POST', () => {
  1239. // const ctx = new helpers.ServiceTestContext();
  1240. const instanceSettings = ({
  1241. url: 'proxied',
  1242. directUrl: 'direct',
  1243. user: 'test',
  1244. password: 'mupp',
  1245. jsonData: { httpMethod: 'POST' },
  1246. } as unknown) as DataSourceInstanceSettings<PromOptions>;
  1247. describe('When querying prometheus with one target using query editor target spec', () => {
  1248. let results: any;
  1249. const urlExpected = 'proxied/api/v1/query_range';
  1250. const dataExpected = {
  1251. query: 'test{job="testjob"}',
  1252. start: 1 * 60,
  1253. end: 2 * 60,
  1254. step: 60,
  1255. };
  1256. const query = {
  1257. range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) },
  1258. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  1259. interval: '60s',
  1260. };
  1261. beforeEach(async () => {
  1262. const response = {
  1263. status: 'success',
  1264. data: {
  1265. data: {
  1266. resultType: 'matrix',
  1267. result: [
  1268. {
  1269. metric: { __name__: 'test', job: 'testjob' },
  1270. values: [[2 * 60, '3846']],
  1271. },
  1272. ],
  1273. },
  1274. },
  1275. };
  1276. backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
  1277. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1278. await ctx.ds.query(query).then((data: any) => {
  1279. results = data;
  1280. });
  1281. });
  1282. it('should generate the correct query', () => {
  1283. const res = backendSrv.datasourceRequest.mock.calls[0][0];
  1284. expect(res.method).toBe('POST');
  1285. expect(res.url).toBe(urlExpected);
  1286. expect(res.data).toEqual(dataExpected);
  1287. });
  1288. it('should return series list', () => {
  1289. expect(results.data.length).toBe(1);
  1290. expect(results.data[0].target).toBe('test{job="testjob"}');
  1291. });
  1292. });
  1293. describe('When querying prometheus via check headers X-Dashboard-Id and X-Panel-Id', () => {
  1294. const options = { dashboardId: 1, panelId: 2 };
  1295. const httpOptions = {
  1296. headers: {} as { [key: string]: number | undefined },
  1297. };
  1298. it('with proxy access tracing headers should be added', () => {
  1299. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1300. ctx.ds._addTracingHeaders(httpOptions, options);
  1301. expect(httpOptions.headers['X-Dashboard-Id']).toBe(1);
  1302. expect(httpOptions.headers['X-Panel-Id']).toBe(2);
  1303. });
  1304. it('with direct access tracing headers should not be added', () => {
  1305. instanceSettings.url = 'http://127.0.0.1:8000';
  1306. ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
  1307. ctx.ds._addTracingHeaders(httpOptions, options);
  1308. expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined);
  1309. expect(httpOptions.headers['X-Panel-Id']).toBe(undefined);
  1310. });
  1311. });
  1312. });