datasource.test.ts 75 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301
  1. import { cloneDeep } from 'lodash';
  2. import { of, throwError } from 'rxjs';
  3. import {
  4. CoreApp,
  5. DataQueryRequest,
  6. DataQueryResponseData,
  7. DataSourceInstanceSettings,
  8. dateTime,
  9. Field,
  10. getFieldDisplayName,
  11. LoadingState,
  12. toDataFrame,
  13. } from '@grafana/data';
  14. import { QueryOptions } from 'app/types';
  15. import { describe } from '../../../../test/lib/common';
  16. import { VariableHide } from '../../../features/variables/types';
  17. import {
  18. alignRange,
  19. extractRuleMappingFromGroups,
  20. PrometheusDatasource,
  21. prometheusRegularEscape,
  22. prometheusSpecialRegexEscape,
  23. } from './datasource';
  24. import { PromOptions, PromQuery } from './types';
  25. const fetchMock = jest.fn().mockReturnValue(of(createDefaultPromResponse()));
  26. jest.mock('./metric_find_query');
  27. jest.mock('@grafana/runtime', () => ({
  28. // @ts-ignore
  29. ...jest.requireActual('@grafana/runtime'),
  30. getBackendSrv: () => ({
  31. fetch: fetchMock,
  32. }),
  33. }));
  34. const templateSrvStub = {
  35. getAdhocFilters: jest.fn(() => [] as any[]),
  36. replace: jest.fn((a: string, ...rest: any) => a),
  37. };
  38. const timeSrvStub = {
  39. timeRange(): any {
  40. return {
  41. from: dateTime(1531468681),
  42. to: dateTime(1531489712),
  43. };
  44. },
  45. };
  46. beforeEach(() => {
  47. jest.clearAllMocks();
  48. });
  49. describe('PrometheusDatasource', () => {
  50. let ds: PrometheusDatasource;
  51. const instanceSettings = {
  52. url: 'proxied',
  53. id: 1,
  54. directUrl: 'direct',
  55. user: 'test',
  56. password: 'mupp',
  57. jsonData: {
  58. customQueryParameters: '',
  59. } as any,
  60. } as unknown as DataSourceInstanceSettings<PromOptions>;
  61. beforeEach(() => {
  62. ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
  63. });
  64. describe('Query', () => {
  65. it('returns empty array when no queries', async () => {
  66. await expect(ds.query(createDataRequest([]))).toEmitValuesWith((response) => {
  67. expect(response[0].data).toEqual([]);
  68. expect(response[0].state).toBe(LoadingState.Done);
  69. });
  70. });
  71. it('performs time series queries', async () => {
  72. await expect(ds.query(createDataRequest([{}]))).toEmitValuesWith((response) => {
  73. expect(response[0].data.length).not.toBe(0);
  74. expect(response[0].state).toBe(LoadingState.Done);
  75. });
  76. });
  77. it('with 2 queries and used from Explore, sends results as they arrive', async () => {
  78. await expect(ds.query(createDataRequest([{}, {}], { app: CoreApp.Explore }))).toEmitValuesWith((response) => {
  79. expect(response[0].data.length).not.toBe(0);
  80. expect(response[0].state).toBe(LoadingState.Loading);
  81. expect(response[1].state).toBe(LoadingState.Done);
  82. });
  83. });
  84. it('with 2 queries and used from Panel, waits for all to finish until sending Done status', async () => {
  85. await expect(ds.query(createDataRequest([{}, {}], { app: CoreApp.Dashboard }))).toEmitValuesWith((response) => {
  86. expect(response[0].data.length).not.toBe(0);
  87. expect(response[0].state).toBe(LoadingState.Done);
  88. });
  89. });
  90. });
  91. describe('Datasource metadata requests', () => {
  92. it('should perform a GET request with the default config', () => {
  93. ds.metadataRequest('/foo', { bar: 'baz baz', foo: 'foo' });
  94. expect(fetchMock.mock.calls.length).toBe(1);
  95. expect(fetchMock.mock.calls[0][0].method).toBe('GET');
  96. expect(fetchMock.mock.calls[0][0].url).toContain('bar=baz%20baz&foo=foo');
  97. });
  98. it('should still perform a GET request with the DS HTTP method set to POST and not POST-friendly endpoint', () => {
  99. const postSettings = cloneDeep(instanceSettings);
  100. postSettings.jsonData.httpMethod = 'POST';
  101. const promDs = new PrometheusDatasource(postSettings, templateSrvStub as any, timeSrvStub as any);
  102. promDs.metadataRequest('/foo');
  103. expect(fetchMock.mock.calls.length).toBe(1);
  104. expect(fetchMock.mock.calls[0][0].method).toBe('GET');
  105. });
  106. it('should try to perform a POST request with the DS HTTP method set to POST and POST-friendly endpoint', () => {
  107. const postSettings = cloneDeep(instanceSettings);
  108. postSettings.jsonData.httpMethod = 'POST';
  109. const promDs = new PrometheusDatasource(postSettings, templateSrvStub as any, timeSrvStub as any);
  110. promDs.metadataRequest('api/v1/series', { bar: 'baz baz', foo: 'foo' });
  111. expect(fetchMock.mock.calls.length).toBe(1);
  112. expect(fetchMock.mock.calls[0][0].method).toBe('POST');
  113. expect(fetchMock.mock.calls[0][0].url).not.toContain('bar=baz%20baz&foo=foo');
  114. expect(fetchMock.mock.calls[0][0].data).toEqual({ bar: 'baz baz', foo: 'foo' });
  115. });
  116. });
  117. describe('customQueryParams', () => {
  118. const target = { expr: 'test{job="testjob"}', format: 'time_series', refId: '' };
  119. function makeQuery(target: PromQuery) {
  120. return {
  121. range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
  122. targets: [target],
  123. interval: '60s',
  124. } as any;
  125. }
  126. describe('with GET http method', () => {
  127. const promDs = new PrometheusDatasource(
  128. { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123', httpMethod: 'GET' } as any },
  129. templateSrvStub as any,
  130. timeSrvStub as any
  131. );
  132. it('added to metadata request', () => {
  133. promDs.metadataRequest('/foo');
  134. expect(fetchMock.mock.calls.length).toBe(1);
  135. expect(fetchMock.mock.calls[0][0].url).toBe('/api/datasources/1/resources/foo?customQuery=123');
  136. });
  137. it('adds params to timeseries query', () => {
  138. promDs.query(makeQuery(target));
  139. expect(fetchMock.mock.calls.length).toBe(1);
  140. expect(fetchMock.mock.calls[0][0].url).toBe(
  141. 'proxied/api/v1/query_range?query=test%7Bjob%3D%22testjob%22%7D&start=60&end=180&step=60&customQuery=123'
  142. );
  143. });
  144. it('adds params to exemplars query', () => {
  145. promDs.query(makeQuery({ ...target, exemplar: true }));
  146. // We do also range query for single exemplars target
  147. expect(fetchMock.mock.calls.length).toBe(2);
  148. expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123');
  149. expect(fetchMock.mock.calls[1][0].url).toContain('&customQuery=123');
  150. });
  151. it('adds params to instant query', () => {
  152. promDs.query(makeQuery({ ...target, instant: true }));
  153. expect(fetchMock.mock.calls.length).toBe(1);
  154. expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123');
  155. });
  156. });
  157. describe('with POST http method', () => {
  158. const promDs = new PrometheusDatasource(
  159. { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123', httpMethod: 'POST' } as any },
  160. templateSrvStub as any,
  161. timeSrvStub as any
  162. );
  163. it('added to metadata request with non-POST endpoint', () => {
  164. promDs.metadataRequest('/foo');
  165. expect(fetchMock.mock.calls.length).toBe(1);
  166. expect(fetchMock.mock.calls[0][0].url).toBe('/api/datasources/1/resources/foo?customQuery=123');
  167. });
  168. it('added to metadata request with POST endpoint', () => {
  169. promDs.metadataRequest('/api/v1/labels');
  170. expect(fetchMock.mock.calls.length).toBe(1);
  171. expect(fetchMock.mock.calls[0][0].url).toBe('/api/datasources/1/resources/api/v1/labels');
  172. expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123');
  173. });
  174. it('adds params to timeseries query', () => {
  175. promDs.query(makeQuery(target));
  176. expect(fetchMock.mock.calls.length).toBe(1);
  177. expect(fetchMock.mock.calls[0][0].url).toBe('proxied/api/v1/query_range');
  178. expect(fetchMock.mock.calls[0][0].data).toEqual({
  179. customQuery: '123',
  180. query: 'test{job="testjob"}',
  181. step: 60,
  182. end: 180,
  183. start: 60,
  184. });
  185. });
  186. it('adds params to exemplars query', () => {
  187. promDs.query(makeQuery({ ...target, exemplar: true }));
  188. // We do also range query for single exemplars target
  189. expect(fetchMock.mock.calls.length).toBe(2);
  190. expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123');
  191. expect(fetchMock.mock.calls[1][0].data.customQuery).toBe('123');
  192. });
  193. it('adds params to instant query', () => {
  194. promDs.query(makeQuery({ ...target, instant: true }));
  195. expect(fetchMock.mock.calls.length).toBe(1);
  196. expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123');
  197. });
  198. });
  199. });
  200. describe('When using adhoc filters', () => {
  201. const DEFAULT_QUERY_EXPRESSION = 'metric{job="foo"} - metric';
  202. const target = { expr: DEFAULT_QUERY_EXPRESSION };
  203. const originalAdhocFiltersMock = templateSrvStub.getAdhocFilters();
  204. afterAll(() => {
  205. templateSrvStub.getAdhocFilters.mockReturnValue(originalAdhocFiltersMock);
  206. });
  207. it('should not modify expression with no filters', () => {
  208. const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0);
  209. expect(result).toMatchObject({ expr: DEFAULT_QUERY_EXPRESSION });
  210. });
  211. it('should add filters to expression', () => {
  212. templateSrvStub.getAdhocFilters.mockReturnValue([
  213. {
  214. key: 'k1',
  215. operator: '=',
  216. value: 'v1',
  217. },
  218. {
  219. key: 'k2',
  220. operator: '!=',
  221. value: 'v2',
  222. },
  223. ]);
  224. const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0);
  225. expect(result).toMatchObject({ expr: 'metric{job="foo", k1="v1", k2!="v2"} - metric{k1="v1", k2!="v2"}' });
  226. });
  227. it('should add escaping if needed to regex filter expressions', () => {
  228. templateSrvStub.getAdhocFilters.mockReturnValue([
  229. {
  230. key: 'k1',
  231. operator: '=~',
  232. value: 'v.*',
  233. },
  234. {
  235. key: 'k2',
  236. operator: '=~',
  237. value: `v'.*`,
  238. },
  239. ]);
  240. const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0);
  241. expect(result).toMatchObject({
  242. expr: `metric{job="foo", k1=~"v.*", k2=~"v\\\\'.*"} - metric{k1=~"v.*", k2=~"v\\\\'.*"}`,
  243. });
  244. });
  245. });
  246. describe('When converting prometheus histogram to heatmap format', () => {
  247. let query: any;
  248. beforeEach(() => {
  249. query = {
  250. range: { from: dateTime(1443454528000), to: dateTime(1443454528000) },
  251. targets: [{ expr: 'test{job="testjob"}', format: 'heatmap', legendFormat: '{{le}}' }],
  252. interval: '1s',
  253. };
  254. });
  255. it('should convert cumulative histogram to ordinary', async () => {
  256. const resultMock = [
  257. {
  258. metric: { __name__: 'metric', job: 'testjob', le: '10' },
  259. values: [
  260. [1443454528.0, '10'],
  261. [1443454528.0, '10'],
  262. ],
  263. },
  264. {
  265. metric: { __name__: 'metric', job: 'testjob', le: '20' },
  266. values: [
  267. [1443454528.0, '20'],
  268. [1443454528.0, '10'],
  269. ],
  270. },
  271. {
  272. metric: { __name__: 'metric', job: 'testjob', le: '30' },
  273. values: [
  274. [1443454528.0, '25'],
  275. [1443454528.0, '10'],
  276. ],
  277. },
  278. ];
  279. const responseMock = { data: { data: { result: resultMock } } };
  280. ds.performTimeSeriesQuery = jest.fn().mockReturnValue(of(responseMock));
  281. await expect(ds.query(query)).toEmitValuesWith((result) => {
  282. const results = result[0].data;
  283. expect(results[0].fields[1].values.toArray()).toEqual([10, 10]);
  284. expect(results[0].fields[2].values.toArray()).toEqual([10, 0]);
  285. expect(results[0].fields[3].values.toArray()).toEqual([5, 0]);
  286. });
  287. });
  288. it('should sort series by label value', async () => {
  289. const resultMock = [
  290. {
  291. metric: { __name__: 'metric', job: 'testjob', le: '2' },
  292. values: [
  293. [1443454528.0, '10'],
  294. [1443454528.0, '10'],
  295. ],
  296. },
  297. {
  298. metric: { __name__: 'metric', job: 'testjob', le: '4' },
  299. values: [
  300. [1443454528.0, '20'],
  301. [1443454528.0, '10'],
  302. ],
  303. },
  304. {
  305. metric: { __name__: 'metric', job: 'testjob', le: '+Inf' },
  306. values: [
  307. [1443454528.0, '25'],
  308. [1443454528.0, '10'],
  309. ],
  310. },
  311. {
  312. metric: { __name__: 'metric', job: 'testjob', le: '1' },
  313. values: [
  314. [1443454528.0, '25'],
  315. [1443454528.0, '10'],
  316. ],
  317. },
  318. ];
  319. const responseMock = { data: { data: { result: resultMock } } };
  320. const expected = ['1', '2', '4', '+Inf'];
  321. ds.performTimeSeriesQuery = jest.fn().mockReturnValue(of(responseMock));
  322. await expect(ds.query(query)).toEmitValuesWith((result) => {
  323. const seriesLabels = result[0].data[0].fields.slice(1).map((field: Field) => getFieldDisplayName(field));
  324. expect(seriesLabels).toEqual(expected);
  325. });
  326. });
  327. });
  328. describe('alignRange', () => {
  329. it('does not modify already aligned intervals with perfect step', () => {
  330. const range = alignRange(0, 3, 3, 0);
  331. expect(range.start).toEqual(0);
  332. expect(range.end).toEqual(3);
  333. });
  334. it('does modify end-aligned intervals to reflect number of steps possible', () => {
  335. const range = alignRange(1, 6, 3, 0);
  336. expect(range.start).toEqual(0);
  337. expect(range.end).toEqual(6);
  338. });
  339. it('does align intervals that are a multiple of steps', () => {
  340. const range = alignRange(1, 4, 3, 0);
  341. expect(range.start).toEqual(0);
  342. expect(range.end).toEqual(3);
  343. });
  344. it('does align intervals that are not a multiple of steps', () => {
  345. const range = alignRange(1, 5, 3, 0);
  346. expect(range.start).toEqual(0);
  347. expect(range.end).toEqual(3);
  348. });
  349. it('does align intervals with local midnight -UTC offset', () => {
  350. //week range, location 4+ hours UTC offset, 24h step time
  351. const range = alignRange(4 * 60 * 60, (7 * 24 + 4) * 60 * 60, 24 * 60 * 60, -4 * 60 * 60); //04:00 UTC, 7 day range
  352. expect(range.start).toEqual(4 * 60 * 60);
  353. expect(range.end).toEqual((7 * 24 + 4) * 60 * 60);
  354. });
  355. it('does align intervals with local midnight +UTC offset', () => {
  356. //week range, location 4- hours UTC offset, 24h step time
  357. 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
  358. expect(range.start).toEqual(20 * 60 * 60);
  359. expect(range.end).toEqual((8 * 24 - 4) * 60 * 60);
  360. });
  361. });
  362. describe('extractRuleMappingFromGroups()', () => {
  363. it('returns empty mapping for no rule groups', () => {
  364. expect(extractRuleMappingFromGroups([])).toEqual({});
  365. });
  366. it('returns a mapping for recording rules only', () => {
  367. const groups = [
  368. {
  369. rules: [
  370. {
  371. name: 'HighRequestLatency',
  372. query: 'job:request_latency_seconds:mean5m{job="myjob"} > 0.5',
  373. type: 'alerting',
  374. },
  375. {
  376. name: 'job:http_inprogress_requests:sum',
  377. query: 'sum(http_inprogress_requests) by (job)',
  378. type: 'recording',
  379. },
  380. ],
  381. file: '/rules.yaml',
  382. interval: 60,
  383. name: 'example',
  384. },
  385. ];
  386. const mapping = extractRuleMappingFromGroups(groups);
  387. expect(mapping).toEqual({ 'job:http_inprogress_requests:sum': 'sum(http_inprogress_requests) by (job)' });
  388. });
  389. });
  390. describe('Prometheus regular escaping', () => {
  391. it('should not escape non-string', () => {
  392. expect(prometheusRegularEscape(12)).toEqual(12);
  393. });
  394. it('should not escape simple string', () => {
  395. expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
  396. });
  397. it("should escape '", () => {
  398. expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
  399. });
  400. it('should escape \\', () => {
  401. expect(prometheusRegularEscape('looking\\glass')).toEqual('looking\\\\glass');
  402. });
  403. it('should escape multiple characters', () => {
  404. expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
  405. });
  406. it('should escape multiple different characters', () => {
  407. expect(prometheusRegularEscape("'loo\\king'glass'")).toEqual("\\\\'loo\\\\king\\\\'glass\\\\'");
  408. });
  409. });
  410. describe('Prometheus regexes escaping', () => {
  411. it('should not escape simple string', () => {
  412. expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
  413. });
  414. it('should escape $^*+?.()|\\', () => {
  415. expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass");
  416. expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass');
  417. expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass');
  418. expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass');
  419. expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass');
  420. expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass');
  421. expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass');
  422. expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass');
  423. expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass');
  424. expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass');
  425. expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass');
  426. expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass');
  427. expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass');
  428. expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass');
  429. expect(prometheusSpecialRegexEscape('looking|glass')).toEqual('looking\\\\|glass');
  430. });
  431. it('should escape multiple special characters', () => {
  432. expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
  433. });
  434. });
  435. describe('When interpolating variables', () => {
  436. let customVariable: any;
  437. beforeEach(() => {
  438. customVariable = {
  439. id: '',
  440. global: false,
  441. multi: false,
  442. includeAll: false,
  443. allValue: null,
  444. query: '',
  445. options: [],
  446. current: {},
  447. name: '',
  448. type: 'custom',
  449. label: null,
  450. hide: VariableHide.dontHide,
  451. skipUrlSync: false,
  452. index: -1,
  453. initLock: null,
  454. };
  455. });
  456. describe('and value is a string', () => {
  457. it('should only escape single quotes', () => {
  458. expect(ds.interpolateQueryExpr("abc'$^*{}[]+?.()|", customVariable)).toEqual("abc\\\\'$^*{}[]+?.()|");
  459. });
  460. });
  461. describe('and value is a number', () => {
  462. it('should return a number', () => {
  463. expect(ds.interpolateQueryExpr(1000 as any, customVariable)).toEqual(1000);
  464. });
  465. });
  466. describe('and variable allows multi-value', () => {
  467. beforeEach(() => {
  468. customVariable.multi = true;
  469. });
  470. it('should regex escape values if the value is a string', () => {
  471. expect(ds.interpolateQueryExpr('looking*glass', customVariable)).toEqual('looking\\\\*glass');
  472. });
  473. it('should return pipe separated values if the value is an array of strings', () => {
  474. expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], customVariable)).toEqual('(a\\\\|bc|de\\\\|f)');
  475. });
  476. it('should return 1 regex escaped value if there is just 1 value in an array of strings', () => {
  477. expect(ds.interpolateQueryExpr(['looking*glass'], customVariable)).toEqual('looking\\\\*glass');
  478. });
  479. });
  480. describe('and variable allows all', () => {
  481. beforeEach(() => {
  482. customVariable.includeAll = true;
  483. });
  484. it('should regex escape values if the array is a string', () => {
  485. expect(ds.interpolateQueryExpr('looking*glass', customVariable)).toEqual('looking\\\\*glass');
  486. });
  487. it('should return pipe separated values if the value is an array of strings', () => {
  488. expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], customVariable)).toEqual('(a\\\\|bc|de\\\\|f)');
  489. });
  490. it('should return 1 regex escaped value if there is just 1 value in an array of strings', () => {
  491. expect(ds.interpolateQueryExpr(['looking*glass'], customVariable)).toEqual('looking\\\\*glass');
  492. });
  493. });
  494. });
  495. describe('interpolateVariablesInQueries', () => {
  496. it('should call replace function 2 times', () => {
  497. const query = {
  498. expr: 'test{job="testjob"}',
  499. format: 'time_series',
  500. interval: '$Interval',
  501. refId: 'A',
  502. };
  503. const interval = '10m';
  504. templateSrvStub.replace.mockReturnValue(interval);
  505. const queries = ds.interpolateVariablesInQueries([query], { Interval: { text: interval, value: interval } });
  506. expect(templateSrvStub.replace).toBeCalledTimes(2);
  507. expect(queries[0].interval).toBe(interval);
  508. });
  509. });
  510. describe('applyTemplateVariables', () => {
  511. const originalAdhocFiltersMock = templateSrvStub.getAdhocFilters();
  512. const originalReplaceMock = jest.fn((a: string, ...rest: any) => a);
  513. afterAll(() => {
  514. templateSrvStub.getAdhocFilters.mockReturnValue(originalAdhocFiltersMock);
  515. templateSrvStub.replace = originalReplaceMock;
  516. });
  517. it('should call replace function for legendFormat', () => {
  518. const query = {
  519. expr: 'test{job="bar"}',
  520. legendFormat: '$legend',
  521. refId: 'A',
  522. };
  523. const legend = 'baz';
  524. templateSrvStub.replace.mockReturnValue(legend);
  525. const interpolatedQuery = ds.applyTemplateVariables(query, { legend: { text: legend, value: legend } });
  526. expect(interpolatedQuery.legendFormat).toBe(legend);
  527. });
  528. it('should call replace function for interval', () => {
  529. const query = {
  530. expr: 'test{job="bar"}',
  531. interval: '$step',
  532. refId: 'A',
  533. };
  534. const step = '5s';
  535. templateSrvStub.replace.mockReturnValue(step);
  536. const interpolatedQuery = ds.applyTemplateVariables(query, { step: { text: step, value: step } });
  537. expect(interpolatedQuery.interval).toBe(step);
  538. });
  539. it('should call replace function for expr', () => {
  540. const query = {
  541. expr: 'test{job="$job"}',
  542. refId: 'A',
  543. };
  544. const job = 'bar';
  545. templateSrvStub.replace.mockReturnValue(job);
  546. const interpolatedQuery = ds.applyTemplateVariables(query, { job: { text: job, value: job } });
  547. expect(interpolatedQuery.expr).toBe(job);
  548. });
  549. it('should add ad-hoc filters to expr', () => {
  550. templateSrvStub.replace = jest.fn((a: string) => a);
  551. templateSrvStub.getAdhocFilters.mockReturnValue([
  552. {
  553. key: 'k1',
  554. operator: '=',
  555. value: 'v1',
  556. },
  557. {
  558. key: 'k2',
  559. operator: '!=',
  560. value: 'v2',
  561. },
  562. ]);
  563. const query = {
  564. expr: 'test{job="bar"}',
  565. refId: 'A',
  566. };
  567. const result = ds.applyTemplateVariables(query, {});
  568. expect(result).toMatchObject({ expr: 'test{job="bar", k1="v1", k2!="v2"}' });
  569. });
  570. });
  571. describe('metricFindQuery', () => {
  572. beforeEach(() => {
  573. const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
  574. templateSrvStub.replace = jest.fn();
  575. ds.metricFindQuery(query);
  576. });
  577. afterAll(() => {
  578. templateSrvStub.replace = jest.fn((a: string) => a);
  579. });
  580. it('should call templateSrv.replace with scopedVars', () => {
  581. expect(templateSrvStub.replace.mock.calls[0][1]).toBeDefined();
  582. });
  583. it('should have the correct range and range_ms', () => {
  584. const range = templateSrvStub.replace.mock.calls[0][1].__range;
  585. const rangeMs = templateSrvStub.replace.mock.calls[0][1].__range_ms;
  586. const rangeS = templateSrvStub.replace.mock.calls[0][1].__range_s;
  587. expect(range).toEqual({ text: '21s', value: '21s' });
  588. expect(rangeMs).toEqual({ text: 21031, value: 21031 });
  589. expect(rangeS).toEqual({ text: 21, value: 21 });
  590. });
  591. it('should pass the default interval value', () => {
  592. const interval = templateSrvStub.replace.mock.calls[0][1].__interval;
  593. const intervalMs = templateSrvStub.replace.mock.calls[0][1].__interval_ms;
  594. expect(interval).toEqual({ text: '15s', value: '15s' });
  595. expect(intervalMs).toEqual({ text: 15000, value: 15000 });
  596. });
  597. });
  598. });
  599. const SECOND = 1000;
  600. const MINUTE = 60 * SECOND;
  601. const HOUR = 60 * MINUTE;
  602. const time = ({ hours = 0, seconds = 0, minutes = 0 }) => dateTime(hours * HOUR + minutes * MINUTE + seconds * SECOND);
  603. describe('PrometheusDatasource', () => {
  604. const instanceSettings = {
  605. url: 'proxied',
  606. directUrl: 'direct',
  607. user: 'test',
  608. password: 'mupp',
  609. jsonData: { httpMethod: 'GET' },
  610. } as unknown as DataSourceInstanceSettings<PromOptions>;
  611. let ds: PrometheusDatasource;
  612. beforeEach(() => {
  613. ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
  614. });
  615. describe('When querying prometheus with one target using query editor target spec', () => {
  616. describe('and query syntax is valid', () => {
  617. let results: any;
  618. const query = {
  619. range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
  620. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  621. interval: '60s',
  622. };
  623. // Interval alignment with step
  624. const urlExpected = `proxied/api/v1/query_range?query=${encodeURIComponent(
  625. 'test{job="testjob"}'
  626. )}&start=60&end=180&step=60`;
  627. beforeEach(async () => {
  628. const response = {
  629. data: {
  630. status: 'success',
  631. data: {
  632. resultType: 'matrix',
  633. result: [
  634. {
  635. metric: { __name__: 'test', job: 'testjob' },
  636. values: [[60, '3846']],
  637. },
  638. ],
  639. },
  640. },
  641. };
  642. fetchMock.mockImplementation(() => of(response));
  643. ds.query(query as any).subscribe((data: any) => {
  644. results = data;
  645. });
  646. });
  647. it('should generate the correct query', () => {
  648. const res = fetchMock.mock.calls[0][0];
  649. expect(res.method).toBe('GET');
  650. expect(res.url).toBe(urlExpected);
  651. });
  652. it('should return series list', async () => {
  653. const frame = toDataFrame(results.data[0]);
  654. expect(results.data.length).toBe(1);
  655. expect(getFieldDisplayName(frame.fields[1], frame)).toBe('test{job="testjob"}');
  656. });
  657. });
  658. describe('and query syntax is invalid', () => {
  659. let results: string;
  660. const query = {
  661. range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
  662. targets: [{ expr: 'tes;;t{job="testjob"}', format: 'time_series' }],
  663. interval: '60s',
  664. };
  665. const errMessage = 'parse error at char 25: could not parse remaining input';
  666. const response = {
  667. data: {
  668. status: 'error',
  669. errorType: 'bad_data',
  670. error: errMessage,
  671. },
  672. };
  673. it('should generate an error', () => {
  674. fetchMock.mockImplementation(() => throwError(response));
  675. ds.query(query as any).subscribe((e: any) => {
  676. results = e.message;
  677. expect(results).toBe(`"${errMessage}"`);
  678. });
  679. });
  680. });
  681. });
  682. describe('When querying prometheus with one target which returns multiple series', () => {
  683. let results: any;
  684. const start = 60;
  685. const end = 360;
  686. const step = 60;
  687. const query = {
  688. range: { from: time({ seconds: start }), to: time({ seconds: end }) },
  689. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  690. interval: '60s',
  691. };
  692. beforeEach(async () => {
  693. const response = {
  694. status: 'success',
  695. data: {
  696. data: {
  697. resultType: 'matrix',
  698. result: [
  699. {
  700. metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
  701. values: [
  702. [start + step * 1, '3846'],
  703. [start + step * 3, '3847'],
  704. [end - step * 1, '3848'],
  705. ],
  706. },
  707. {
  708. metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
  709. values: [[start + step * 2, '4846']],
  710. },
  711. ],
  712. },
  713. },
  714. };
  715. fetchMock.mockImplementation(() => of(response));
  716. ds.query(query as any).subscribe((data: any) => {
  717. results = data;
  718. });
  719. });
  720. it('should be same length', () => {
  721. expect(results.data.length).toBe(2);
  722. expect(results.data[0].length).toBe((end - start) / step + 1);
  723. expect(results.data[1].length).toBe((end - start) / step + 1);
  724. });
  725. it('should fill null until first datapoint in response', () => {
  726. expect(results.data[0].fields[0].values.get(0)).toBe(start * 1000);
  727. expect(results.data[0].fields[1].values.get(0)).toBe(null);
  728. expect(results.data[0].fields[0].values.get(1)).toBe((start + step * 1) * 1000);
  729. expect(results.data[0].fields[1].values.get(1)).toBe(3846);
  730. });
  731. it('should fill null after last datapoint in response', () => {
  732. const length = (end - start) / step + 1;
  733. expect(results.data[0].fields[0].values.get(length - 2)).toBe((end - step * 1) * 1000);
  734. expect(results.data[0].fields[1].values.get(length - 2)).toBe(3848);
  735. expect(results.data[0].fields[0].values.get(length - 1)).toBe(end * 1000);
  736. expect(results.data[0].fields[1].values.get(length - 1)).toBe(null);
  737. });
  738. it('should fill null at gap between series', () => {
  739. expect(results.data[0].fields[0].values.get(2)).toBe((start + step * 2) * 1000);
  740. expect(results.data[0].fields[1].values.get(2)).toBe(null);
  741. expect(results.data[1].fields[0].values.get(1)).toBe((start + step * 1) * 1000);
  742. expect(results.data[1].fields[1].values.get(1)).toBe(null);
  743. expect(results.data[1].fields[0].values.get(3)).toBe((start + step * 3) * 1000);
  744. expect(results.data[1].fields[1].values.get(3)).toBe(null);
  745. });
  746. });
  747. describe('When querying prometheus with one target and instant = true', () => {
  748. let results: any;
  749. const urlExpected = `proxied/api/v1/query?query=${encodeURIComponent('test{job="testjob"}')}&time=123`;
  750. const query = {
  751. range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
  752. targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
  753. interval: '60s',
  754. };
  755. beforeEach(async () => {
  756. const response = {
  757. status: 'success',
  758. data: {
  759. data: {
  760. resultType: 'vector',
  761. result: [
  762. {
  763. metric: { __name__: 'test', job: 'testjob' },
  764. value: [123, '3846'],
  765. },
  766. ],
  767. },
  768. },
  769. };
  770. fetchMock.mockImplementation(() => of(response));
  771. ds.query(query as any).subscribe((data: any) => {
  772. results = data;
  773. });
  774. });
  775. it('should generate the correct query', () => {
  776. const res = fetchMock.mock.calls[0][0];
  777. expect(res.method).toBe('GET');
  778. expect(res.url).toBe(urlExpected);
  779. });
  780. it('should return series list', () => {
  781. const frame = toDataFrame(results.data[0]);
  782. expect(results.data.length).toBe(1);
  783. expect(frame.name).toBe('test{job="testjob"}');
  784. expect(getFieldDisplayName(frame.fields[1], frame)).toBe('test{job="testjob"}');
  785. });
  786. });
  787. describe('annotationQuery', () => {
  788. let results: any;
  789. const options: any = {
  790. annotation: {
  791. expr: 'ALERTS{alertstate="firing"}',
  792. tagKeys: 'job',
  793. titleFormat: '{{alertname}}',
  794. textFormat: '{{instance}}',
  795. },
  796. range: {
  797. from: time({ seconds: 63 }),
  798. to: time({ seconds: 123 }),
  799. },
  800. };
  801. const response = createAnnotationResponse();
  802. describe('when time series query is cancelled', () => {
  803. it('should return empty results', async () => {
  804. fetchMock.mockImplementation(() => of({ cancelled: true }));
  805. await ds.annotationQuery(options).then((data: any) => {
  806. results = data;
  807. });
  808. expect(results).toEqual([]);
  809. });
  810. });
  811. describe('not use useValueForTime', () => {
  812. beforeEach(async () => {
  813. options.annotation.useValueForTime = false;
  814. fetchMock.mockImplementation(() => of(response));
  815. await ds.annotationQuery(options).then((data: any) => {
  816. results = data;
  817. });
  818. });
  819. it('should return annotation list', () => {
  820. expect(results.length).toBe(1);
  821. expect(results[0].tags).toContain('testjob');
  822. expect(results[0].title).toBe('InstanceDown');
  823. expect(results[0].text).toBe('testinstance');
  824. expect(results[0].time).toBe(123);
  825. });
  826. });
  827. describe('use useValueForTime', () => {
  828. beforeEach(async () => {
  829. options.annotation.useValueForTime = true;
  830. fetchMock.mockImplementation(() => of(response));
  831. await ds.annotationQuery(options).then((data: any) => {
  832. results = data;
  833. });
  834. });
  835. it('should return annotation list', () => {
  836. expect(results[0].time).toEqual(456);
  837. });
  838. });
  839. describe('step parameter', () => {
  840. beforeEach(() => {
  841. fetchMock.mockImplementation(() => of(response));
  842. });
  843. it('should use default step for short range if no interval is given', () => {
  844. const query = {
  845. ...options,
  846. range: {
  847. from: time({ seconds: 63 }),
  848. to: time({ seconds: 123 }),
  849. },
  850. };
  851. ds.annotationQuery(query);
  852. const req = fetchMock.mock.calls[0][0];
  853. expect(req.data.queries[0].interval).toBe('60s');
  854. });
  855. it('should use default step for short range when annotation step is empty string', () => {
  856. const query = {
  857. ...options,
  858. annotation: {
  859. ...options.annotation,
  860. step: '',
  861. },
  862. range: {
  863. from: time({ seconds: 63 }),
  864. to: time({ seconds: 123 }),
  865. },
  866. };
  867. ds.annotationQuery(query);
  868. const req = fetchMock.mock.calls[0][0];
  869. expect(req.data.queries[0].interval).toBe('60s');
  870. });
  871. it('should use custom step for short range', () => {
  872. const annotation = {
  873. ...options.annotation,
  874. step: '10s',
  875. };
  876. const query = {
  877. ...options,
  878. annotation,
  879. range: {
  880. from: time({ seconds: 63 }),
  881. to: time({ seconds: 123 }),
  882. },
  883. };
  884. ds.annotationQuery(query);
  885. const req = fetchMock.mock.calls[0][0];
  886. expect(req.data.queries[0].interval).toBe('10s');
  887. });
  888. });
  889. describe('region annotations for sectors', () => {
  890. const options: any = {
  891. annotation: {
  892. expr: 'ALERTS{alertstate="firing"}',
  893. tagKeys: 'job',
  894. titleFormat: '{{alertname}}',
  895. textFormat: '{{instance}}',
  896. },
  897. range: {
  898. from: time({ seconds: 63 }),
  899. to: time({ seconds: 900 }),
  900. },
  901. };
  902. async function runAnnotationQuery(data: number[][]) {
  903. let response = createAnnotationResponse();
  904. response.data.results['X'].frames[0].data.values = data;
  905. options.annotation.useValueForTime = false;
  906. fetchMock.mockImplementation(() => of(response));
  907. return ds.annotationQuery(options);
  908. }
  909. it('should handle gaps and inactive values', async () => {
  910. const results = await runAnnotationQuery([
  911. [2 * 60000, 3 * 60000, 5 * 60000, 6 * 60000, 7 * 60000, 8 * 60000, 9 * 60000],
  912. [1, 1, 1, 1, 1, 0, 1],
  913. ]);
  914. expect(results.map((result) => [result.time, result.timeEnd])).toEqual([
  915. [120000, 180000],
  916. [300000, 420000],
  917. [540000, 540000],
  918. ]);
  919. });
  920. it('should handle single region', async () => {
  921. const results = await runAnnotationQuery([
  922. [2 * 60000, 3 * 60000],
  923. [1, 1],
  924. ]);
  925. expect(results.map((result) => [result.time, result.timeEnd])).toEqual([[120000, 180000]]);
  926. });
  927. it('should handle 0 active regions', async () => {
  928. const results = await runAnnotationQuery([
  929. [2 * 60000, 3 * 60000, 5 * 60000],
  930. [0, 0, 0],
  931. ]);
  932. expect(results.length).toBe(0);
  933. });
  934. it('should handle single active value', async () => {
  935. const results = await runAnnotationQuery([[2 * 60000], [1]]);
  936. expect(results.map((result) => [result.time, result.timeEnd])).toEqual([[120000, 120000]]);
  937. });
  938. });
  939. describe('with template variables', () => {
  940. const originalReplaceMock = jest.fn((a: string, ...rest: any) => a);
  941. afterAll(() => {
  942. templateSrvStub.replace = originalReplaceMock;
  943. });
  944. it('should interpolate variables in query expr', () => {
  945. const query = {
  946. ...options,
  947. annotation: {
  948. ...options.annotation,
  949. expr: '$variable',
  950. },
  951. range: {
  952. from: time({ seconds: 1 }),
  953. to: time({ seconds: 2 }),
  954. },
  955. };
  956. const interpolated = 'interpolated_expr';
  957. templateSrvStub.replace.mockReturnValue(interpolated);
  958. ds.annotationQuery(query);
  959. const req = fetchMock.mock.calls[0][0];
  960. expect(req.data.queries[0].expr).toBe(interpolated);
  961. });
  962. });
  963. });
  964. describe('When resultFormat is table and instant = true', () => {
  965. let results: any;
  966. const query = {
  967. range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
  968. targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
  969. interval: '60s',
  970. };
  971. beforeEach(async () => {
  972. const response = {
  973. status: 'success',
  974. data: {
  975. data: {
  976. resultType: 'vector',
  977. result: [
  978. {
  979. metric: { __name__: 'test', job: 'testjob' },
  980. value: [123, '3846'],
  981. },
  982. ],
  983. },
  984. },
  985. };
  986. fetchMock.mockImplementation(() => of(response));
  987. ds.query(query as any).subscribe((data: any) => {
  988. results = data;
  989. });
  990. });
  991. it('should return result', () => {
  992. expect(results).not.toBe(null);
  993. });
  994. });
  995. describe('The "step" query parameter', () => {
  996. const response = {
  997. status: 'success',
  998. data: {
  999. data: {
  1000. resultType: 'matrix',
  1001. result: [] as DataQueryResponseData[],
  1002. },
  1003. },
  1004. };
  1005. it('should be min interval when greater than auto interval', async () => {
  1006. const query = {
  1007. // 6 minute range
  1008. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1009. targets: [
  1010. {
  1011. expr: 'test',
  1012. interval: '10s',
  1013. },
  1014. ],
  1015. interval: '5s',
  1016. };
  1017. const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
  1018. fetchMock.mockImplementation(() => of(response));
  1019. ds.query(query as any);
  1020. const res = fetchMock.mock.calls[0][0];
  1021. expect(res.method).toBe('GET');
  1022. expect(res.url).toBe(urlExpected);
  1023. });
  1024. it('step should be fractional for sub second intervals', async () => {
  1025. const query = {
  1026. // 6 minute range
  1027. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1028. targets: [{ expr: 'test' }],
  1029. interval: '100ms',
  1030. };
  1031. const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=0.1';
  1032. fetchMock.mockImplementation(() => of(response));
  1033. ds.query(query as any);
  1034. const res = fetchMock.mock.calls[0][0];
  1035. expect(res.method).toBe('GET');
  1036. expect(res.url).toBe(urlExpected);
  1037. });
  1038. it('should be auto interval when greater than min interval', async () => {
  1039. const query = {
  1040. // 6 minute range
  1041. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1042. targets: [
  1043. {
  1044. expr: 'test',
  1045. interval: '5s',
  1046. },
  1047. ],
  1048. interval: '10s',
  1049. };
  1050. const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
  1051. fetchMock.mockImplementation(() => of(response));
  1052. ds.query(query as any);
  1053. const res = fetchMock.mock.calls[0][0];
  1054. expect(res.method).toBe('GET');
  1055. expect(res.url).toBe(urlExpected);
  1056. });
  1057. it('should result in querying fewer than 11000 data points', async () => {
  1058. const query = {
  1059. // 6 hour range
  1060. range: { from: time({ hours: 1 }), to: time({ hours: 7 }) },
  1061. targets: [{ expr: 'test' }],
  1062. interval: '1s',
  1063. };
  1064. const end = 7 * 60 * 60;
  1065. const start = 60 * 60;
  1066. const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
  1067. fetchMock.mockImplementation(() => of(response));
  1068. ds.query(query as any);
  1069. const res = fetchMock.mock.calls[0][0];
  1070. expect(res.method).toBe('GET');
  1071. expect(res.url).toBe(urlExpected);
  1072. });
  1073. it('should not apply min interval when interval * intervalFactor greater', async () => {
  1074. const query = {
  1075. // 6 minute range
  1076. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1077. targets: [
  1078. {
  1079. expr: 'test',
  1080. interval: '10s',
  1081. intervalFactor: 10,
  1082. },
  1083. ],
  1084. interval: '5s',
  1085. };
  1086. // times get rounded up to interval
  1087. const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50';
  1088. fetchMock.mockImplementation(() => of(response));
  1089. ds.query(query as any);
  1090. const res = fetchMock.mock.calls[0][0];
  1091. expect(res.method).toBe('GET');
  1092. expect(res.url).toBe(urlExpected);
  1093. });
  1094. it('should apply min interval when interval * intervalFactor smaller', async () => {
  1095. const query = {
  1096. // 6 minute range
  1097. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1098. targets: [
  1099. {
  1100. expr: 'test',
  1101. interval: '15s',
  1102. intervalFactor: 2,
  1103. },
  1104. ],
  1105. interval: '5s',
  1106. };
  1107. const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
  1108. fetchMock.mockImplementation(() => of(response));
  1109. ds.query(query as any);
  1110. const res = fetchMock.mock.calls[0][0];
  1111. expect(res.method).toBe('GET');
  1112. expect(res.url).toBe(urlExpected);
  1113. });
  1114. it('should apply intervalFactor to auto interval when greater', async () => {
  1115. const query = {
  1116. // 6 minute range
  1117. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1118. targets: [
  1119. {
  1120. expr: 'test',
  1121. interval: '5s',
  1122. intervalFactor: 10,
  1123. },
  1124. ],
  1125. interval: '10s',
  1126. };
  1127. // times get aligned to interval
  1128. const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100';
  1129. fetchMock.mockImplementation(() => of(response));
  1130. ds.query(query as any);
  1131. const res = fetchMock.mock.calls[0][0];
  1132. expect(res.method).toBe('GET');
  1133. expect(res.url).toBe(urlExpected);
  1134. });
  1135. it('should not not be affected by the 11000 data points limit when large enough', async () => {
  1136. const query = {
  1137. // 1 week range
  1138. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  1139. targets: [
  1140. {
  1141. expr: 'test',
  1142. intervalFactor: 10,
  1143. },
  1144. ],
  1145. interval: '10s',
  1146. };
  1147. const end = 7 * 24 * 60 * 60;
  1148. const start = 0;
  1149. const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
  1150. fetchMock.mockImplementation(() => of(response));
  1151. ds.query(query as any);
  1152. const res = fetchMock.mock.calls[0][0];
  1153. expect(res.method).toBe('GET');
  1154. expect(res.url).toBe(urlExpected);
  1155. });
  1156. it('should be determined by the 11000 data points limit when too small', async () => {
  1157. const query = {
  1158. // 1 week range
  1159. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  1160. targets: [
  1161. {
  1162. expr: 'test',
  1163. intervalFactor: 10,
  1164. },
  1165. ],
  1166. interval: '5s',
  1167. };
  1168. let end = 7 * 24 * 60 * 60;
  1169. end -= end % 55;
  1170. const start = 0;
  1171. const step = 55;
  1172. const adjusted = alignRange(start, end, step, timeSrvStub.timeRange().to.utcOffset() * 60);
  1173. const urlExpected =
  1174. 'proxied/api/v1/query_range?query=test' + '&start=' + adjusted.start + '&end=' + adjusted.end + '&step=' + step;
  1175. fetchMock.mockImplementation(() => of(response));
  1176. ds.query(query as any);
  1177. const res = fetchMock.mock.calls[0][0];
  1178. expect(res.method).toBe('GET');
  1179. expect(res.url).toBe(urlExpected);
  1180. });
  1181. });
  1182. describe('The __interval and __interval_ms template variables', () => {
  1183. const response = {
  1184. status: 'success',
  1185. data: {
  1186. data: {
  1187. resultType: 'matrix',
  1188. result: [] as DataQueryResponseData[],
  1189. },
  1190. },
  1191. };
  1192. it('should be unchanged when auto interval is greater than min interval', async () => {
  1193. const query = {
  1194. // 6 minute range
  1195. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1196. targets: [
  1197. {
  1198. expr: 'rate(test[$__interval])',
  1199. interval: '5s',
  1200. },
  1201. ],
  1202. interval: '10s',
  1203. scopedVars: {
  1204. __interval: { text: '10s', value: '10s' },
  1205. __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
  1206. },
  1207. };
  1208. const urlExpected =
  1209. 'proxied/api/v1/query_range?query=' +
  1210. encodeURIComponent('rate(test[$__interval])') +
  1211. '&start=60&end=420&step=10';
  1212. templateSrvStub.replace = jest.fn((str) => str) as any;
  1213. fetchMock.mockImplementation(() => of(response));
  1214. ds.query(query as any);
  1215. const res = fetchMock.mock.calls[0][0];
  1216. expect(res.method).toBe('GET');
  1217. expect(res.url).toBe(urlExpected);
  1218. expect(templateSrvStub.replace.mock.calls[0][1]).toEqual({
  1219. __interval: {
  1220. text: '10s',
  1221. value: '10s',
  1222. },
  1223. __interval_ms: {
  1224. text: 10000,
  1225. value: 10000,
  1226. },
  1227. });
  1228. });
  1229. it('should be min interval when it is greater than auto interval', async () => {
  1230. const query = {
  1231. // 6 minute range
  1232. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1233. targets: [
  1234. {
  1235. expr: 'rate(test[$__interval])',
  1236. interval: '10s',
  1237. },
  1238. ],
  1239. interval: '5s',
  1240. scopedVars: {
  1241. __interval: { text: '5s', value: '5s' },
  1242. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  1243. },
  1244. };
  1245. const urlExpected =
  1246. 'proxied/api/v1/query_range?query=' +
  1247. encodeURIComponent('rate(test[$__interval])') +
  1248. '&start=60&end=420&step=10';
  1249. fetchMock.mockImplementation(() => of(response));
  1250. templateSrvStub.replace = jest.fn((str) => str) as any;
  1251. ds.query(query as any);
  1252. const res = fetchMock.mock.calls[0][0];
  1253. expect(res.method).toBe('GET');
  1254. expect(res.url).toBe(urlExpected);
  1255. expect(templateSrvStub.replace.mock.calls[0][1]).toEqual({
  1256. __interval: {
  1257. text: '5s',
  1258. value: '5s',
  1259. },
  1260. __interval_ms: {
  1261. text: 5000,
  1262. value: 5000,
  1263. },
  1264. });
  1265. });
  1266. it('should account for intervalFactor', async () => {
  1267. const query = {
  1268. // 6 minute range
  1269. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1270. targets: [
  1271. {
  1272. expr: 'rate(test[$__interval])',
  1273. interval: '5s',
  1274. intervalFactor: 10,
  1275. },
  1276. ],
  1277. interval: '10s',
  1278. scopedVars: {
  1279. __interval: { text: '10s', value: '10s' },
  1280. __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
  1281. },
  1282. };
  1283. const urlExpected =
  1284. 'proxied/api/v1/query_range?query=' +
  1285. encodeURIComponent('rate(test[$__interval])') +
  1286. '&start=0&end=400&step=100';
  1287. fetchMock.mockImplementation(() => of(response));
  1288. templateSrvStub.replace = jest.fn((str) => str) as any;
  1289. ds.query(query as any);
  1290. const res = fetchMock.mock.calls[0][0];
  1291. expect(res.method).toBe('GET');
  1292. expect(res.url).toBe(urlExpected);
  1293. expect(templateSrvStub.replace.mock.calls[0][1]).toEqual({
  1294. __interval: {
  1295. text: '10s',
  1296. value: '10s',
  1297. },
  1298. __interval_ms: {
  1299. text: 10000,
  1300. value: 10000,
  1301. },
  1302. });
  1303. expect(query.scopedVars.__interval.text).toBe('10s');
  1304. expect(query.scopedVars.__interval.value).toBe('10s');
  1305. expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
  1306. expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000);
  1307. });
  1308. it('should be interval * intervalFactor when greater than min interval', async () => {
  1309. const query = {
  1310. // 6 minute range
  1311. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1312. targets: [
  1313. {
  1314. expr: 'rate(test[$__interval])',
  1315. interval: '10s',
  1316. intervalFactor: 10,
  1317. },
  1318. ],
  1319. interval: '5s',
  1320. scopedVars: {
  1321. __interval: { text: '5s', value: '5s' },
  1322. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  1323. },
  1324. };
  1325. const urlExpected =
  1326. 'proxied/api/v1/query_range?query=' +
  1327. encodeURIComponent('rate(test[$__interval])') +
  1328. '&start=50&end=400&step=50';
  1329. templateSrvStub.replace = jest.fn((str) => str) as any;
  1330. fetchMock.mockImplementation(() => of(response));
  1331. ds.query(query as any);
  1332. const res = fetchMock.mock.calls[0][0];
  1333. expect(res.method).toBe('GET');
  1334. expect(res.url).toBe(urlExpected);
  1335. expect(templateSrvStub.replace.mock.calls[0][1]).toEqual({
  1336. __interval: {
  1337. text: '5s',
  1338. value: '5s',
  1339. },
  1340. __interval_ms: {
  1341. text: 5000,
  1342. value: 5000,
  1343. },
  1344. });
  1345. });
  1346. it('should be min interval when greater than interval * intervalFactor', async () => {
  1347. const query = {
  1348. // 6 minute range
  1349. range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
  1350. targets: [
  1351. {
  1352. expr: 'rate(test[$__interval])',
  1353. interval: '15s',
  1354. intervalFactor: 2,
  1355. },
  1356. ],
  1357. interval: '5s',
  1358. scopedVars: {
  1359. __interval: { text: '5s', value: '5s' },
  1360. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  1361. },
  1362. };
  1363. const urlExpected =
  1364. 'proxied/api/v1/query_range?query=' +
  1365. encodeURIComponent('rate(test[$__interval])') +
  1366. '&start=60&end=420&step=15';
  1367. fetchMock.mockImplementation(() => of(response));
  1368. ds.query(query as any);
  1369. const res = fetchMock.mock.calls[0][0];
  1370. expect(res.method).toBe('GET');
  1371. expect(res.url).toBe(urlExpected);
  1372. expect(templateSrvStub.replace.mock.calls[0][1]).toEqual({
  1373. __interval: {
  1374. text: '5s',
  1375. value: '5s',
  1376. },
  1377. __interval_ms: {
  1378. text: 5000,
  1379. value: 5000,
  1380. },
  1381. });
  1382. });
  1383. it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => {
  1384. const query = {
  1385. // 1 week range
  1386. range: { from: time({}), to: time({ hours: 7 * 24 }) },
  1387. targets: [
  1388. {
  1389. expr: 'rate(test[$__interval])',
  1390. intervalFactor: 10,
  1391. },
  1392. ],
  1393. interval: '5s',
  1394. scopedVars: {
  1395. __interval: { text: '5s', value: '5s' },
  1396. __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
  1397. },
  1398. };
  1399. let end = 7 * 24 * 60 * 60;
  1400. end -= end % 55;
  1401. const start = 0;
  1402. const step = 55;
  1403. const adjusted = alignRange(start, end, step, timeSrvStub.timeRange().to.utcOffset() * 60);
  1404. const urlExpected =
  1405. 'proxied/api/v1/query_range?query=' +
  1406. encodeURIComponent('rate(test[$__interval])') +
  1407. '&start=' +
  1408. adjusted.start +
  1409. '&end=' +
  1410. adjusted.end +
  1411. '&step=' +
  1412. step;
  1413. fetchMock.mockImplementation(() => of(response));
  1414. templateSrvStub.replace = jest.fn((str) => str) as any;
  1415. ds.query(query as any);
  1416. const res = fetchMock.mock.calls[0][0];
  1417. expect(res.method).toBe('GET');
  1418. expect(res.url).toBe(urlExpected);
  1419. expect(templateSrvStub.replace.mock.calls[0][1]).toEqual({
  1420. __interval: {
  1421. text: '5s',
  1422. value: '5s',
  1423. },
  1424. __interval_ms: {
  1425. text: 5000,
  1426. value: 5000,
  1427. },
  1428. });
  1429. });
  1430. });
  1431. describe('The __range, __range_s and __range_ms variables', () => {
  1432. const response = {
  1433. status: 'success',
  1434. data: {
  1435. data: {
  1436. resultType: 'matrix',
  1437. result: [] as DataQueryResponseData[],
  1438. },
  1439. },
  1440. };
  1441. it('should use overridden ranges, not dashboard ranges', async () => {
  1442. const expectedRangeSecond = 3600;
  1443. const expectedRangeString = '3600s';
  1444. const query = {
  1445. range: {
  1446. from: time({}),
  1447. to: time({ hours: 1 }),
  1448. },
  1449. targets: [
  1450. {
  1451. expr: 'test[${__range_s}s]',
  1452. },
  1453. ],
  1454. interval: '60s',
  1455. };
  1456. const urlExpected = `proxied/api/v1/query_range?query=${encodeURIComponent(
  1457. query.targets[0].expr
  1458. )}&start=0&end=3600&step=60`;
  1459. templateSrvStub.replace = jest.fn((str) => str) as any;
  1460. fetchMock.mockImplementation(() => of(response));
  1461. ds.query(query as any);
  1462. const res = fetchMock.mock.calls[0][0];
  1463. expect(res.url).toBe(urlExpected);
  1464. expect(templateSrvStub.replace.mock.calls[1][1]).toEqual({
  1465. __range_s: {
  1466. text: expectedRangeSecond,
  1467. value: expectedRangeSecond,
  1468. },
  1469. __range: {
  1470. text: expectedRangeString,
  1471. value: expectedRangeString,
  1472. },
  1473. __range_ms: {
  1474. text: expectedRangeSecond * 1000,
  1475. value: expectedRangeSecond * 1000,
  1476. },
  1477. __rate_interval: {
  1478. text: '75s',
  1479. value: '75s',
  1480. },
  1481. });
  1482. });
  1483. });
  1484. describe('The __rate_interval variable', () => {
  1485. const target = { expr: 'rate(process_cpu_seconds_total[$__rate_interval])', refId: 'A' };
  1486. beforeEach(() => {
  1487. templateSrvStub.replace.mockClear();
  1488. });
  1489. it('should be 4 times the scrape interval if interval + scrape interval is lower', () => {
  1490. ds.createQuery(target, { interval: '15s' } as any, 0, 300);
  1491. expect(templateSrvStub.replace.mock.calls[1][1]['__rate_interval'].value).toBe('60s');
  1492. });
  1493. it('should be interval + scrape interval if 4 times the scrape interval is lower', () => {
  1494. ds.createQuery(target, { interval: '5m' } as any, 0, 10080);
  1495. expect(templateSrvStub.replace.mock.calls[1][1]['__rate_interval'].value).toBe('315s');
  1496. });
  1497. it('should fall back to a scrape interval of 15s if min step is set to 0, resulting in 4*15s = 60s', () => {
  1498. ds.createQuery({ ...target, interval: '' }, { interval: '15s' } as any, 0, 300);
  1499. expect(templateSrvStub.replace.mock.calls[1][1]['__rate_interval'].value).toBe('60s');
  1500. });
  1501. it('should be 4 times the scrape interval if min step set to 1m and interval is 15s', () => {
  1502. // For a 5m graph, $__interval is 15s
  1503. ds.createQuery({ ...target, interval: '1m' }, { interval: '15s' } as any, 0, 300);
  1504. expect(templateSrvStub.replace.mock.calls[2][1]['__rate_interval'].value).toBe('240s');
  1505. });
  1506. it('should be interval + scrape interval if min step set to 1m and interval is 5m', () => {
  1507. // For a 7d graph, $__interval is 5m
  1508. ds.createQuery({ ...target, interval: '1m' }, { interval: '5m' } as any, 0, 10080);
  1509. expect(templateSrvStub.replace.mock.calls[2][1]['__rate_interval'].value).toBe('360s');
  1510. });
  1511. it('should be interval + scrape interval if resolution is set to 1/2 and interval is 10m', () => {
  1512. // For a 7d graph, $__interval is 10m
  1513. ds.createQuery({ ...target, intervalFactor: 2 }, { interval: '10m' } as any, 0, 10080);
  1514. expect(templateSrvStub.replace.mock.calls[1][1]['__rate_interval'].value).toBe('1215s');
  1515. });
  1516. it('should be 4 times the scrape interval if resolution is set to 1/2 and interval is 15s', () => {
  1517. // For a 5m graph, $__interval is 15s
  1518. ds.createQuery({ ...target, intervalFactor: 2 }, { interval: '15s' } as any, 0, 300);
  1519. expect(templateSrvStub.replace.mock.calls[1][1]['__rate_interval'].value).toBe('60s');
  1520. });
  1521. it('should interpolate min step if set', () => {
  1522. templateSrvStub.replace = jest.fn((_: string) => '15s');
  1523. ds.createQuery({ ...target, interval: '$int' }, { interval: '15s' } as any, 0, 300);
  1524. expect(templateSrvStub.replace.mock.calls).toHaveLength(3);
  1525. templateSrvStub.replace = jest.fn((a: string) => a);
  1526. });
  1527. });
  1528. it('should give back 1 exemplar target when multiple queries with exemplar enabled and same metric', () => {
  1529. const targetA: PromQuery = {
  1530. refId: 'A',
  1531. expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
  1532. exemplar: true,
  1533. };
  1534. const targetB: PromQuery = {
  1535. refId: 'B',
  1536. expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
  1537. exemplar: true,
  1538. };
  1539. ds.languageProvider = {
  1540. histogramMetrics: ['tns_request_duration_seconds_bucket'],
  1541. } as any;
  1542. const request = {
  1543. targets: [targetA, targetB],
  1544. interval: '1s',
  1545. panelId: '',
  1546. } as any as DataQueryRequest<PromQuery>;
  1547. const Aexemplars = ds.shouldRunExemplarQuery(targetA, request);
  1548. const BExpemplars = ds.shouldRunExemplarQuery(targetB, request);
  1549. expect(Aexemplars).toBe(true);
  1550. expect(BExpemplars).toBe(false);
  1551. });
  1552. });
  1553. describe('PrometheusDatasource for POST', () => {
  1554. const instanceSettings = {
  1555. url: 'proxied',
  1556. directUrl: 'direct',
  1557. user: 'test',
  1558. password: 'mupp',
  1559. jsonData: { httpMethod: 'POST' },
  1560. } as unknown as DataSourceInstanceSettings<PromOptions>;
  1561. let ds: PrometheusDatasource;
  1562. beforeEach(() => {
  1563. ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
  1564. });
  1565. describe('When querying prometheus with one target using query editor target spec', () => {
  1566. let results: any;
  1567. const urlExpected = 'proxied/api/v1/query_range';
  1568. const dataExpected = {
  1569. query: 'test{job="testjob"}',
  1570. start: 1 * 60,
  1571. end: 2 * 60,
  1572. step: 60,
  1573. };
  1574. const query = {
  1575. range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) },
  1576. targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
  1577. interval: '60s',
  1578. };
  1579. beforeEach(async () => {
  1580. const response = {
  1581. status: 'success',
  1582. data: {
  1583. data: {
  1584. resultType: 'matrix',
  1585. result: [
  1586. {
  1587. metric: { __name__: 'test', job: 'testjob' },
  1588. values: [[2 * 60, '3846']],
  1589. },
  1590. ],
  1591. },
  1592. },
  1593. };
  1594. fetchMock.mockImplementation(() => of(response));
  1595. ds.query(query as any).subscribe((data: any) => {
  1596. results = data;
  1597. });
  1598. });
  1599. it('should generate the correct query', () => {
  1600. const res = fetchMock.mock.calls[0][0];
  1601. expect(res.method).toBe('POST');
  1602. expect(res.url).toBe(urlExpected);
  1603. expect(res.data).toEqual(dataExpected);
  1604. });
  1605. it('should return series list', () => {
  1606. const frame = toDataFrame(results.data[0]);
  1607. expect(results.data.length).toBe(1);
  1608. expect(getFieldDisplayName(frame.fields[1], frame)).toBe('test{job="testjob"}');
  1609. });
  1610. });
  1611. describe('When querying prometheus via check headers X-Dashboard-Id and X-Panel-Id', () => {
  1612. const options = { dashboardId: 1, panelId: 2 };
  1613. const httpOptions = {
  1614. headers: {} as { [key: string]: number | undefined },
  1615. };
  1616. it('with proxy access tracing headers should be added', () => {
  1617. ds._addTracingHeaders(httpOptions as any, options as any);
  1618. expect(httpOptions.headers['X-Dashboard-Id']).toBe(1);
  1619. expect(httpOptions.headers['X-Panel-Id']).toBe(2);
  1620. });
  1621. it('with direct access tracing headers should not be added', () => {
  1622. const mockDs = new PrometheusDatasource(
  1623. { ...instanceSettings, url: 'http://127.0.0.1:8000' },
  1624. templateSrvStub as any,
  1625. timeSrvStub as any
  1626. );
  1627. mockDs._addTracingHeaders(httpOptions as any, options as any);
  1628. expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined);
  1629. expect(httpOptions.headers['X-Panel-Id']).toBe(undefined);
  1630. });
  1631. });
  1632. });
  1633. function getPrepareTargetsContext({
  1634. targets,
  1635. app,
  1636. queryOptions,
  1637. languageProvider,
  1638. }: {
  1639. targets: PromQuery[];
  1640. app?: CoreApp;
  1641. queryOptions?: Partial<QueryOptions>;
  1642. languageProvider?: any;
  1643. }) {
  1644. const instanceSettings = {
  1645. url: 'proxied',
  1646. directUrl: 'direct',
  1647. user: 'test',
  1648. password: 'mupp',
  1649. jsonData: { httpMethod: 'POST' },
  1650. } as unknown as DataSourceInstanceSettings<PromOptions>;
  1651. const start = 0;
  1652. const end = 1;
  1653. const panelId = '2';
  1654. const options = {
  1655. targets,
  1656. interval: '1s',
  1657. panelId,
  1658. app,
  1659. ...queryOptions,
  1660. } as any as DataQueryRequest<PromQuery>;
  1661. const ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
  1662. if (languageProvider) {
  1663. ds.languageProvider = languageProvider;
  1664. }
  1665. const { queries, activeTargets } = ds.prepareTargets(options, start, end);
  1666. return {
  1667. queries,
  1668. activeTargets,
  1669. start,
  1670. end,
  1671. panelId,
  1672. };
  1673. }
  1674. describe('prepareTargets', () => {
  1675. describe('when run from a Panel', () => {
  1676. it('then it should just add targets', () => {
  1677. const target: PromQuery = {
  1678. refId: 'A',
  1679. expr: 'up',
  1680. requestId: '2A',
  1681. };
  1682. const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({ targets: [target] });
  1683. expect(queries.length).toBe(1);
  1684. expect(activeTargets.length).toBe(1);
  1685. expect(queries[0]).toEqual({
  1686. end,
  1687. expr: 'up',
  1688. headers: {
  1689. 'X-Dashboard-Id': undefined,
  1690. 'X-Panel-Id': panelId,
  1691. },
  1692. hinting: undefined,
  1693. instant: undefined,
  1694. refId: target.refId,
  1695. requestId: panelId + target.refId,
  1696. start,
  1697. step: 1,
  1698. });
  1699. expect(activeTargets[0]).toEqual(target);
  1700. });
  1701. it('should give back 3 targets when multiple queries with exemplar enabled and same metric', () => {
  1702. const targetA: PromQuery = {
  1703. refId: 'A',
  1704. expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
  1705. exemplar: true,
  1706. };
  1707. const targetB: PromQuery = {
  1708. refId: 'B',
  1709. expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
  1710. exemplar: true,
  1711. };
  1712. const { queries, activeTargets } = getPrepareTargetsContext({
  1713. targets: [targetA, targetB],
  1714. languageProvider: {
  1715. histogramMetrics: ['tns_request_duration_seconds_bucket'],
  1716. },
  1717. });
  1718. expect(queries).toHaveLength(3);
  1719. expect(activeTargets).toHaveLength(3);
  1720. });
  1721. it('should give back 4 targets when multiple queries with exemplar enabled', () => {
  1722. const targetA: PromQuery = {
  1723. refId: 'A',
  1724. expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
  1725. exemplar: true,
  1726. };
  1727. const targetB: PromQuery = {
  1728. refId: 'B',
  1729. expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_bucket[5m])) by (le))',
  1730. exemplar: true,
  1731. };
  1732. const { queries, activeTargets } = getPrepareTargetsContext({
  1733. targets: [targetA, targetB],
  1734. languageProvider: {
  1735. histogramMetrics: ['tns_request_duration_seconds_bucket'],
  1736. },
  1737. });
  1738. expect(queries).toHaveLength(4);
  1739. expect(activeTargets).toHaveLength(4);
  1740. });
  1741. it('should give back 2 targets when exemplar enabled', () => {
  1742. const target: PromQuery = {
  1743. refId: 'A',
  1744. expr: 'up',
  1745. exemplar: true,
  1746. };
  1747. const { queries, activeTargets } = getPrepareTargetsContext({ targets: [target] });
  1748. expect(queries).toHaveLength(2);
  1749. expect(activeTargets).toHaveLength(2);
  1750. expect(activeTargets[0].exemplar).toBe(true);
  1751. expect(activeTargets[1].exemplar).toBe(false);
  1752. });
  1753. it('should give back 1 target when exemplar and instant are enabled', () => {
  1754. const target: PromQuery = {
  1755. refId: 'A',
  1756. expr: 'up',
  1757. exemplar: true,
  1758. instant: true,
  1759. };
  1760. const { queries, activeTargets } = getPrepareTargetsContext({ targets: [target] });
  1761. expect(queries).toHaveLength(1);
  1762. expect(activeTargets).toHaveLength(1);
  1763. expect(activeTargets[0].instant).toBe(true);
  1764. });
  1765. });
  1766. describe('when run from Explore', () => {
  1767. describe('when query type Both is selected', () => {
  1768. it('should give back 6 targets when multiple queries with exemplar enabled', () => {
  1769. const targetA: PromQuery = {
  1770. refId: 'A',
  1771. expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
  1772. instant: true,
  1773. range: true,
  1774. exemplar: true,
  1775. };
  1776. const targetB: PromQuery = {
  1777. refId: 'B',
  1778. expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_bucket[5m])) by (le))',
  1779. exemplar: true,
  1780. instant: true,
  1781. range: true,
  1782. };
  1783. const { queries, activeTargets } = getPrepareTargetsContext({
  1784. targets: [targetA, targetB],
  1785. app: CoreApp.Explore,
  1786. languageProvider: {
  1787. histogramMetrics: ['tns_request_duration_seconds_bucket'],
  1788. },
  1789. });
  1790. expect(queries).toHaveLength(6);
  1791. expect(activeTargets).toHaveLength(6);
  1792. });
  1793. it('should give back 5 targets when multiple queries with exemplar enabled and same metric', () => {
  1794. const targetA: PromQuery = {
  1795. refId: 'A',
  1796. expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
  1797. instant: true,
  1798. range: true,
  1799. exemplar: true,
  1800. };
  1801. const targetB: PromQuery = {
  1802. refId: 'B',
  1803. expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
  1804. exemplar: true,
  1805. instant: true,
  1806. range: true,
  1807. };
  1808. const { queries, activeTargets } = getPrepareTargetsContext({
  1809. targets: [targetA, targetB],
  1810. app: CoreApp.Explore,
  1811. languageProvider: {
  1812. histogramMetrics: ['tns_request_duration_seconds_bucket'],
  1813. },
  1814. });
  1815. expect(queries).toHaveLength(5);
  1816. expect(activeTargets).toHaveLength(5);
  1817. });
  1818. it('then it should return both instant and time series related objects', () => {
  1819. const target: PromQuery = {
  1820. refId: 'A',
  1821. expr: 'up',
  1822. range: true,
  1823. instant: true,
  1824. requestId: '2A',
  1825. };
  1826. const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({
  1827. targets: [target],
  1828. app: CoreApp.Explore,
  1829. });
  1830. expect(queries.length).toBe(2);
  1831. expect(activeTargets.length).toBe(2);
  1832. expect(queries[0]).toEqual({
  1833. end,
  1834. expr: 'up',
  1835. headers: {
  1836. 'X-Dashboard-Id': undefined,
  1837. 'X-Panel-Id': panelId,
  1838. },
  1839. hinting: undefined,
  1840. instant: true,
  1841. refId: target.refId,
  1842. requestId: panelId + target.refId + '_instant',
  1843. start,
  1844. step: 1,
  1845. });
  1846. expect(activeTargets[0]).toEqual({
  1847. ...target,
  1848. format: 'table',
  1849. instant: true,
  1850. requestId: panelId + target.refId + '_instant',
  1851. valueWithRefId: true,
  1852. });
  1853. expect(queries[1]).toEqual({
  1854. end,
  1855. expr: 'up',
  1856. headers: {
  1857. 'X-Dashboard-Id': undefined,
  1858. 'X-Panel-Id': panelId,
  1859. },
  1860. hinting: undefined,
  1861. instant: false,
  1862. refId: target.refId,
  1863. requestId: panelId + target.refId,
  1864. start,
  1865. step: 1,
  1866. });
  1867. expect(activeTargets[1]).toEqual({
  1868. ...target,
  1869. format: 'time_series',
  1870. instant: false,
  1871. requestId: panelId + target.refId,
  1872. });
  1873. });
  1874. });
  1875. describe('when query type Instant is selected', () => {
  1876. it('then it should target and modify its format to table', () => {
  1877. const target: PromQuery = {
  1878. refId: 'A',
  1879. expr: 'up',
  1880. instant: true,
  1881. range: false,
  1882. requestId: '2A',
  1883. };
  1884. const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({
  1885. targets: [target],
  1886. app: CoreApp.Explore,
  1887. });
  1888. expect(queries.length).toBe(1);
  1889. expect(activeTargets.length).toBe(1);
  1890. expect(queries[0]).toEqual({
  1891. end,
  1892. expr: 'up',
  1893. headers: {
  1894. 'X-Dashboard-Id': undefined,
  1895. 'X-Panel-Id': panelId,
  1896. },
  1897. hinting: undefined,
  1898. instant: true,
  1899. refId: target.refId,
  1900. requestId: panelId + target.refId,
  1901. start,
  1902. step: 1,
  1903. });
  1904. expect(activeTargets[0]).toEqual({ ...target, format: 'table' });
  1905. });
  1906. });
  1907. });
  1908. describe('when query type Range is selected', () => {
  1909. it('then it should just add targets', () => {
  1910. const target: PromQuery = {
  1911. refId: 'A',
  1912. expr: 'up',
  1913. range: true,
  1914. instant: false,
  1915. requestId: '2A',
  1916. };
  1917. const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({
  1918. targets: [target],
  1919. app: CoreApp.Explore,
  1920. });
  1921. expect(queries.length).toBe(1);
  1922. expect(activeTargets.length).toBe(1);
  1923. expect(queries[0]).toEqual({
  1924. end,
  1925. expr: 'up',
  1926. headers: {
  1927. 'X-Dashboard-Id': undefined,
  1928. 'X-Panel-Id': panelId,
  1929. },
  1930. hinting: undefined,
  1931. instant: false,
  1932. refId: target.refId,
  1933. requestId: panelId + target.refId,
  1934. start,
  1935. step: 1,
  1936. });
  1937. expect(activeTargets[0]).toEqual(target);
  1938. });
  1939. });
  1940. });
  1941. describe('modifyQuery', () => {
  1942. describe('when called with ADD_FILTER', () => {
  1943. describe('and query has no labels', () => {
  1944. it('then the correct label should be added', () => {
  1945. const query: PromQuery = { refId: 'A', expr: 'go_goroutines' };
  1946. const action = { key: 'cluster', value: 'us-cluster', type: 'ADD_FILTER' };
  1947. const instanceSettings = { jsonData: {} } as unknown as DataSourceInstanceSettings<PromOptions>;
  1948. const ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
  1949. const result = ds.modifyQuery(query, action);
  1950. expect(result.refId).toEqual('A');
  1951. expect(result.expr).toEqual('go_goroutines{cluster="us-cluster"}');
  1952. });
  1953. });
  1954. describe('and query has labels', () => {
  1955. it('then the correct label should be added', () => {
  1956. const query: PromQuery = { refId: 'A', expr: 'go_goroutines{cluster="us-cluster"}' };
  1957. const action = { key: 'pod', value: 'pod-123', type: 'ADD_FILTER' };
  1958. const instanceSettings = { jsonData: {} } as unknown as DataSourceInstanceSettings<PromOptions>;
  1959. const ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
  1960. const result = ds.modifyQuery(query, action);
  1961. expect(result.refId).toEqual('A');
  1962. expect(result.expr).toEqual('go_goroutines{cluster="us-cluster", pod="pod-123"}');
  1963. });
  1964. });
  1965. });
  1966. describe('when called with ADD_FILTER_OUT', () => {
  1967. describe('and query has no labels', () => {
  1968. it('then the correct label should be added', () => {
  1969. const query: PromQuery = { refId: 'A', expr: 'go_goroutines' };
  1970. const action = { key: 'cluster', value: 'us-cluster', type: 'ADD_FILTER_OUT' };
  1971. const instanceSettings = { jsonData: {} } as unknown as DataSourceInstanceSettings<PromOptions>;
  1972. const ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
  1973. const result = ds.modifyQuery(query, action);
  1974. expect(result.refId).toEqual('A');
  1975. expect(result.expr).toEqual('go_goroutines{cluster!="us-cluster"}');
  1976. });
  1977. });
  1978. describe('and query has labels', () => {
  1979. it('then the correct label should be added', () => {
  1980. const query: PromQuery = { refId: 'A', expr: 'go_goroutines{cluster="us-cluster"}' };
  1981. const action = { key: 'pod', value: 'pod-123', type: 'ADD_FILTER_OUT' };
  1982. const instanceSettings = { jsonData: {} } as unknown as DataSourceInstanceSettings<PromOptions>;
  1983. const ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
  1984. const result = ds.modifyQuery(query, action);
  1985. expect(result.refId).toEqual('A');
  1986. expect(result.expr).toEqual('go_goroutines{cluster="us-cluster", pod!="pod-123"}');
  1987. });
  1988. });
  1989. });
  1990. });
  1991. function createDataRequest(targets: any[], overrides?: Partial<DataQueryRequest>): DataQueryRequest<PromQuery> {
  1992. const defaults = {
  1993. app: CoreApp.Dashboard,
  1994. targets: targets.map((t) => {
  1995. return {
  1996. instant: false,
  1997. start: dateTime().subtract(5, 'minutes'),
  1998. end: dateTime(),
  1999. expr: 'test',
  2000. ...t,
  2001. };
  2002. }),
  2003. range: {
  2004. from: dateTime(),
  2005. to: dateTime(),
  2006. },
  2007. interval: '15s',
  2008. showingGraph: true,
  2009. };
  2010. return Object.assign(defaults, overrides || {}) as DataQueryRequest<PromQuery>;
  2011. }
  2012. function createDefaultPromResponse() {
  2013. return {
  2014. data: {
  2015. data: {
  2016. result: [
  2017. {
  2018. metric: {
  2019. __name__: 'test_metric',
  2020. },
  2021. values: [[1568369640, 1]],
  2022. },
  2023. ],
  2024. resultType: 'matrix',
  2025. },
  2026. },
  2027. };
  2028. }
  2029. function createAnnotationResponse() {
  2030. const response = {
  2031. data: {
  2032. results: {
  2033. X: {
  2034. frames: [
  2035. {
  2036. schema: {
  2037. name: 'bar',
  2038. refId: 'X',
  2039. fields: [
  2040. {
  2041. name: 'Time',
  2042. type: 'time',
  2043. typeInfo: {
  2044. frame: 'time.Time',
  2045. },
  2046. },
  2047. {
  2048. name: 'Value',
  2049. type: 'number',
  2050. typeInfo: {
  2051. frame: 'float64',
  2052. },
  2053. labels: {
  2054. __name__: 'ALERTS',
  2055. alertname: 'InstanceDown',
  2056. alertstate: 'firing',
  2057. instance: 'testinstance',
  2058. job: 'testjob',
  2059. },
  2060. },
  2061. ],
  2062. },
  2063. data: {
  2064. values: [[123], [456]],
  2065. },
  2066. },
  2067. ],
  2068. },
  2069. },
  2070. },
  2071. };
  2072. return { ...response };
  2073. }