datasource.test.ts 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117
  1. import { lastValueFrom, of, throwError } from 'rxjs';
  2. import { take } from 'rxjs/operators';
  3. import { createFetchResponse } from 'test/helpers/createFetchResponse';
  4. import { getQueryOptions } from 'test/helpers/getQueryOptions';
  5. import {
  6. AbstractLabelOperator,
  7. AnnotationQueryRequest,
  8. CoreApp,
  9. DataFrame,
  10. dateTime,
  11. FieldCache,
  12. FieldType,
  13. LogRowModel,
  14. MutableDataFrame,
  15. toUtc,
  16. } from '@grafana/data';
  17. import { BackendSrvRequest, FetchResponse } from '@grafana/runtime';
  18. import { backendSrv } from 'app/core/services/backend_srv';
  19. import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  20. import { TemplateSrv } from 'app/features/templating/template_srv';
  21. import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer';
  22. import { CustomVariableModel } from '../../../features/variables/types';
  23. import { isMetricsQuery, LokiDatasource, RangeQueryOptions } from './datasource';
  24. import { makeMockLokiDatasource } from './mocks';
  25. import { LokiQuery, LokiQueryType, LokiResponse, LokiResultType } from './types';
  26. jest.mock('@grafana/runtime', () => ({
  27. // @ts-ignore
  28. ...jest.requireActual('@grafana/runtime'),
  29. getBackendSrv: () => backendSrv,
  30. }));
  31. const rawRange = {
  32. from: toUtc('2018-04-25 10:00'),
  33. to: toUtc('2018-04-25 11:00'),
  34. };
  35. const timeSrvStub = {
  36. timeRange: () => ({
  37. from: rawRange.from,
  38. to: rawRange.to,
  39. raw: rawRange,
  40. }),
  41. } as unknown as TimeSrv;
  42. const templateSrvStub = {
  43. getAdhocFilters: jest.fn(() => [] as any[]),
  44. replace: jest.fn((a: string, ...rest: any) => a),
  45. };
  46. const testLogsResponse: FetchResponse<LokiResponse> = {
  47. data: {
  48. data: {
  49. resultType: LokiResultType.Stream,
  50. result: [
  51. {
  52. stream: {},
  53. values: [['1573646419522934000', 'hello']],
  54. },
  55. ],
  56. },
  57. status: 'success',
  58. },
  59. ok: true,
  60. headers: {} as unknown as Headers,
  61. redirected: false,
  62. status: 200,
  63. statusText: 'Success',
  64. type: 'default',
  65. url: '',
  66. config: {} as unknown as BackendSrvRequest,
  67. };
  68. const testMetricsResponse: FetchResponse<LokiResponse> = {
  69. data: {
  70. data: {
  71. resultType: LokiResultType.Matrix,
  72. result: [
  73. {
  74. metric: {},
  75. values: [[1605715380, '1.1']],
  76. },
  77. ],
  78. },
  79. status: 'success',
  80. },
  81. ok: true,
  82. headers: {} as unknown as Headers,
  83. redirected: false,
  84. status: 200,
  85. statusText: 'OK',
  86. type: 'basic',
  87. url: '',
  88. config: {} as unknown as BackendSrvRequest,
  89. };
  90. interface AdHocFilter {
  91. condition: string;
  92. key: string;
  93. operator: string;
  94. value: string;
  95. }
  96. describe('LokiDatasource', () => {
  97. const fetchMock = jest.spyOn(backendSrv, 'fetch');
  98. beforeEach(() => {
  99. jest.clearAllMocks();
  100. fetchMock.mockImplementation(() => of(createFetchResponse({})));
  101. });
  102. describe('when creating range query', () => {
  103. let ds: LokiDatasource;
  104. let adjustIntervalSpy: jest.SpyInstance;
  105. beforeEach(() => {
  106. ds = createLokiDSForTests();
  107. adjustIntervalSpy = jest.spyOn(ds, 'adjustInterval');
  108. });
  109. it('should use default intervalMs if one is not provided', () => {
  110. const target = { expr: '{job="grafana"}', refId: 'B' };
  111. const raw = { from: 'now', to: 'now-1h' };
  112. const range = { from: dateTime(), to: dateTime(), raw: raw };
  113. const options = {
  114. range,
  115. };
  116. const req = ds.createRangeQuery(target, options as any, 1000);
  117. expect(req.start).toBeDefined();
  118. expect(req.end).toBeDefined();
  119. expect(adjustIntervalSpy).toHaveBeenCalledWith(1000, 1, expect.anything());
  120. });
  121. it('should use provided intervalMs', () => {
  122. const target = { expr: '{job="grafana"}', refId: 'B' };
  123. const raw = { from: 'now', to: 'now-1h' };
  124. const range = { from: dateTime(), to: dateTime(), raw: raw };
  125. const options = {
  126. range,
  127. intervalMs: 2000,
  128. };
  129. const req = ds.createRangeQuery(target, options as any, 1000);
  130. expect(req.start).toBeDefined();
  131. expect(req.end).toBeDefined();
  132. expect(adjustIntervalSpy).toHaveBeenCalledWith(2000, 1, expect.anything());
  133. });
  134. it('should set the minimal step to 1ms', () => {
  135. const target = { expr: '{job="grafana"}', refId: 'B' };
  136. const raw = { from: 'now', to: 'now-1h' };
  137. const range = { from: dateTime('2020-10-14T00:00:00'), to: dateTime('2020-10-14T00:00:01'), raw: raw };
  138. const options = {
  139. range,
  140. intervalMs: 0.0005,
  141. };
  142. const req = ds.createRangeQuery(target, options as any, 1000);
  143. expect(req.start).toBeDefined();
  144. expect(req.end).toBeDefined();
  145. expect(adjustIntervalSpy).toHaveBeenCalledWith(0.0005, expect.anything(), 1000);
  146. // Step is in seconds (1 ms === 0.001 s)
  147. expect(req.step).toEqual(0.001);
  148. });
  149. describe('log volume hint', () => {
  150. let options: RangeQueryOptions;
  151. beforeEach(() => {
  152. const raw = { from: 'now', to: 'now-1h' };
  153. const range = { from: dateTime(), to: dateTime(), raw: raw };
  154. options = {
  155. range,
  156. } as unknown as RangeQueryOptions;
  157. });
  158. it('should add volume hint param for log volume queries', () => {
  159. const target = { expr: '{job="grafana"}', refId: 'B', volumeQuery: true };
  160. ds.runRangeQuery(target, options);
  161. expect(backendSrv.fetch).toBeCalledWith(
  162. expect.objectContaining({
  163. headers: {
  164. 'X-Query-Tags': 'Source=logvolhist',
  165. },
  166. })
  167. );
  168. });
  169. it('should not add volume hint param for regular queries', () => {
  170. const target = { expr: '{job="grafana"}', refId: 'B', volumeQuery: false };
  171. ds.runRangeQuery(target, options);
  172. expect(backendSrv.fetch).not.toBeCalledWith(
  173. expect.objectContaining({
  174. headers: {
  175. 'X-Query-Tags': 'Source=logvolhist',
  176. },
  177. })
  178. );
  179. });
  180. });
  181. });
  182. describe('when doing logs queries with limits', () => {
  183. const runLimitTest = async ({
  184. maxDataPoints = 123,
  185. queryMaxLines,
  186. dsMaxLines = 456,
  187. expectedLimit,
  188. expr = '{label="val"}',
  189. }: any) => {
  190. let settings: any = {
  191. url: 'myloggingurl',
  192. jsonData: {
  193. maxLines: dsMaxLines,
  194. },
  195. };
  196. const templateSrvMock = {
  197. getAdhocFilters: (): any[] => [],
  198. replace: (a: string) => a,
  199. } as unknown as TemplateSrv;
  200. const ds = new LokiDatasource(settings, templateSrvMock, timeSrvStub as any);
  201. const options = getQueryOptions<LokiQuery>({ targets: [{ expr, refId: 'B', maxLines: queryMaxLines }] });
  202. options.maxDataPoints = maxDataPoints;
  203. fetchMock.mockImplementation(() => of(testLogsResponse));
  204. await expect(ds.query(options).pipe(take(1))).toEmitValuesWith(() => {
  205. expect(fetchMock.mock.calls.length).toBe(1);
  206. expect(fetchMock.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`);
  207. });
  208. };
  209. it('should use datasource max lines when no limit given and it is log query', async () => {
  210. await runLimitTest({ expectedLimit: 456 });
  211. });
  212. it('should use custom max lines from query if set and it is logs query', async () => {
  213. await runLimitTest({ queryMaxLines: 20, expectedLimit: 20 });
  214. });
  215. it('should use custom max lines from query if set and it is logs query even if it is higher than data source limit', async () => {
  216. await runLimitTest({ queryMaxLines: 500, expectedLimit: 500 });
  217. });
  218. it('should use maxDataPoints if it is metrics query', async () => {
  219. await runLimitTest({ expr: 'rate({label="val"}[10m])', expectedLimit: 123 });
  220. });
  221. it('should use maxDataPoints if it is metrics query and using search', async () => {
  222. await runLimitTest({ expr: 'rate({label="val"}[10m])', expectedLimit: 123 });
  223. });
  224. });
  225. describe('when querying', () => {
  226. function setup(expr: string, app: CoreApp, instant?: boolean, range?: boolean) {
  227. const ds = createLokiDSForTests();
  228. const options = getQueryOptions<LokiQuery>({
  229. targets: [{ expr, refId: 'B', instant, range }],
  230. app,
  231. });
  232. ds.runInstantQuery = jest.fn(() => of({ data: [] }));
  233. ds.runRangeQuery = jest.fn(() => of({ data: [] }));
  234. return { ds, options };
  235. }
  236. const metricsQuery = 'rate({job="grafana"}[10m])';
  237. const logsQuery = '{job="grafana"} |= "foo"';
  238. it('should run logs instant if only instant is selected', async () => {
  239. const { ds, options } = setup(logsQuery, CoreApp.Explore, true, false);
  240. await lastValueFrom(ds.query(options));
  241. expect(ds.runInstantQuery).toBeCalled();
  242. expect(ds.runRangeQuery).not.toBeCalled();
  243. });
  244. it('should run metrics instant if only instant is selected', async () => {
  245. const { ds, options } = setup(metricsQuery, CoreApp.Explore, true, false);
  246. lastValueFrom(await ds.query(options));
  247. expect(ds.runInstantQuery).toBeCalled();
  248. expect(ds.runRangeQuery).not.toBeCalled();
  249. });
  250. it('should run only logs range query if only range is selected', async () => {
  251. const { ds, options } = setup(logsQuery, CoreApp.Explore, false, true);
  252. lastValueFrom(await ds.query(options));
  253. expect(ds.runInstantQuery).not.toBeCalled();
  254. expect(ds.runRangeQuery).toBeCalled();
  255. });
  256. it('should run only metrics range query if only range is selected', async () => {
  257. const { ds, options } = setup(metricsQuery, CoreApp.Explore, false, true);
  258. lastValueFrom(await ds.query(options));
  259. expect(ds.runInstantQuery).not.toBeCalled();
  260. expect(ds.runRangeQuery).toBeCalled();
  261. });
  262. it('should run only logs range query if no query type is selected in Explore', async () => {
  263. const { ds, options } = setup(logsQuery, CoreApp.Explore);
  264. lastValueFrom(await ds.query(options));
  265. expect(ds.runInstantQuery).not.toBeCalled();
  266. expect(ds.runRangeQuery).toBeCalled();
  267. });
  268. it('should run only metrics range query if no query type is selected in Explore', async () => {
  269. const { ds, options } = setup(metricsQuery, CoreApp.Explore);
  270. lastValueFrom(await ds.query(options));
  271. expect(ds.runInstantQuery).not.toBeCalled();
  272. expect(ds.runRangeQuery).toBeCalled();
  273. });
  274. it('should run only logs range query in Dashboard', async () => {
  275. const { ds, options } = setup(logsQuery, CoreApp.Dashboard);
  276. lastValueFrom(await ds.query(options));
  277. expect(ds.runInstantQuery).not.toBeCalled();
  278. expect(ds.runRangeQuery).toBeCalled();
  279. });
  280. it('should run only metrics range query in Dashboard', async () => {
  281. const { ds, options } = setup(metricsQuery, CoreApp.Dashboard);
  282. lastValueFrom(await ds.query(options));
  283. expect(ds.runInstantQuery).not.toBeCalled();
  284. expect(ds.runRangeQuery).toBeCalled();
  285. });
  286. it('should return dataframe data for metrics range queries', async () => {
  287. const ds = createLokiDSForTests();
  288. const options = getQueryOptions<LokiQuery>({
  289. targets: [{ expr: metricsQuery, refId: 'B', range: true }],
  290. app: CoreApp.Explore,
  291. });
  292. fetchMock.mockImplementation(() => of(testMetricsResponse));
  293. await expect(ds.query(options)).toEmitValuesWith((received) => {
  294. const result = received[0];
  295. const frame = result.data[0] as DataFrame;
  296. expect(frame.meta?.preferredVisualisationType).toBe('graph');
  297. expect(frame.refId).toBe('B');
  298. frame.fields.forEach((field) => {
  299. const value = field.values.get(0);
  300. if (field.type === FieldType.time) {
  301. expect(value).toBe(1605715380000);
  302. } else {
  303. expect(value).toBe(1.1);
  304. }
  305. });
  306. });
  307. });
  308. it('should return series data for logs range query', async () => {
  309. const ds = createLokiDSForTests();
  310. const options = getQueryOptions<LokiQuery>({
  311. targets: [{ expr: logsQuery, refId: 'B' }],
  312. });
  313. fetchMock.mockImplementation(() => of(testLogsResponse));
  314. await expect(ds.query(options)).toEmitValuesWith((received) => {
  315. const result = received[0];
  316. const dataFrame = result.data[0] as DataFrame;
  317. const fieldCache = new FieldCache(dataFrame);
  318. expect(fieldCache.getFieldByName('Line')?.values.get(0)).toBe('hello');
  319. expect(dataFrame.meta?.limit).toBe(20);
  320. expect(dataFrame.meta?.searchWords).toEqual(['foo']);
  321. });
  322. });
  323. it('should return custom error message when Loki returns escaping error', async () => {
  324. const ds = createLokiDSForTests();
  325. const options = getQueryOptions<LokiQuery>({
  326. targets: [{ expr: '{job="gra\\fana"}', refId: 'B' }],
  327. });
  328. fetchMock.mockImplementation(() =>
  329. throwError({
  330. data: {
  331. message: 'parse error at line 1, col 6: invalid char escape',
  332. },
  333. status: 400,
  334. statusText: 'Bad Request',
  335. })
  336. );
  337. await expect(ds.query(options)).toEmitValuesWith((received) => {
  338. const err: any = received[0];
  339. expect(err.data.message).toBe(
  340. 'Error: parse error at line 1, col 6: invalid char escape. Make sure that all special characters are escaped with \\. For more information on escaping of special characters visit LogQL documentation at https://grafana.com/docs/loki/latest/logql/.'
  341. );
  342. });
  343. });
  344. describe('When using adhoc filters', () => {
  345. const DEFAULT_EXPR = 'rate({bar="baz", job="foo"} |= "bar" [5m])';
  346. const options = {
  347. targets: [{ expr: DEFAULT_EXPR }],
  348. };
  349. const originalAdhocFiltersMock = templateSrvStub.getAdhocFilters();
  350. const ds = new LokiDatasource({} as any, templateSrvStub as any, timeSrvStub as any);
  351. ds.runRangeQuery = jest.fn(() => of({ data: [] }));
  352. afterAll(() => {
  353. templateSrvStub.getAdhocFilters.mockReturnValue(originalAdhocFiltersMock);
  354. });
  355. it('should not modify expression with no filters', async () => {
  356. await lastValueFrom(ds.query(options as any));
  357. expect(ds.runRangeQuery).toBeCalledWith({ expr: DEFAULT_EXPR }, expect.anything());
  358. });
  359. it('should add filters to expression', async () => {
  360. templateSrvStub.getAdhocFilters.mockReturnValue([
  361. {
  362. key: 'k1',
  363. operator: '=',
  364. value: 'v1',
  365. },
  366. {
  367. key: 'k2',
  368. operator: '!=',
  369. value: 'v2',
  370. },
  371. ]);
  372. await lastValueFrom(ds.query(options as any));
  373. expect(ds.runRangeQuery).toBeCalledWith(
  374. { expr: 'rate({bar="baz",job="foo",k1="v1",k2!="v2"} |= "bar" [5m])' },
  375. expect.anything()
  376. );
  377. });
  378. it('should add escaping if needed to regex filter expressions', async () => {
  379. templateSrvStub.getAdhocFilters.mockReturnValue([
  380. {
  381. key: 'k1',
  382. operator: '=~',
  383. value: 'v.*',
  384. },
  385. {
  386. key: 'k2',
  387. operator: '=~',
  388. value: `v'.*`,
  389. },
  390. ]);
  391. await lastValueFrom(ds.query(options as any));
  392. expect(ds.runRangeQuery).toBeCalledWith(
  393. { expr: 'rate({bar="baz",job="foo",k1=~"v\\\\.\\\\*",k2=~"v\'\\\\.\\\\*"} |= "bar" [5m])' },
  394. expect.anything()
  395. );
  396. });
  397. });
  398. describe('__range, __range_s and __range_ms variables', () => {
  399. const options = {
  400. targets: [{ expr: 'rate(process_cpu_seconds_total[$__range])', refId: 'A', stepInterval: '2s' }],
  401. range: {
  402. from: rawRange.from,
  403. to: rawRange.to,
  404. raw: rawRange,
  405. },
  406. };
  407. const ds = new LokiDatasource({} as any, templateSrvStub as any, timeSrvStub as any);
  408. beforeEach(() => {
  409. templateSrvStub.replace.mockClear();
  410. });
  411. it('should be correctly interpolated', () => {
  412. ds.query(options as any);
  413. const range = templateSrvStub.replace.mock.calls[0][1].__range;
  414. const rangeMs = templateSrvStub.replace.mock.calls[0][1].__range_ms;
  415. const rangeS = templateSrvStub.replace.mock.calls[0][1].__range_s;
  416. expect(range).toEqual({ text: '3600s', value: '3600s' });
  417. expect(rangeMs).toEqual({ text: 3600000, value: 3600000 });
  418. expect(rangeS).toEqual({ text: 3600, value: 3600 });
  419. });
  420. });
  421. });
  422. describe('when interpolating variables', () => {
  423. let ds: LokiDatasource;
  424. let variable: CustomVariableModel;
  425. beforeEach(() => {
  426. ds = createLokiDSForTests();
  427. variable = { ...initialCustomVariableModelState };
  428. });
  429. it('should only escape single quotes', () => {
  430. expect(ds.interpolateQueryExpr("abc'$^*{}[]+?.()|", variable)).toEqual("abc\\\\'$^*{}[]+?.()|");
  431. });
  432. it('should return a number', () => {
  433. expect(ds.interpolateQueryExpr(1000, variable)).toEqual(1000);
  434. });
  435. describe('and variable allows multi-value', () => {
  436. beforeEach(() => {
  437. variable.multi = true;
  438. });
  439. it('should regex escape values if the value is a string', () => {
  440. expect(ds.interpolateQueryExpr('looking*glass', variable)).toEqual('looking\\\\*glass');
  441. });
  442. it('should return pipe separated values if the value is an array of strings', () => {
  443. expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], variable)).toEqual('a\\\\|bc|de\\\\|f');
  444. });
  445. });
  446. describe('and variable allows all', () => {
  447. beforeEach(() => {
  448. variable.includeAll = true;
  449. });
  450. it('should regex escape values if the array is a string', () => {
  451. expect(ds.interpolateQueryExpr('looking*glass', variable)).toEqual('looking\\\\*glass');
  452. });
  453. it('should return pipe separated values if the value is an array of strings', () => {
  454. expect(ds.interpolateQueryExpr(['a|bc', 'de|f'], variable)).toEqual('a\\\\|bc|de\\\\|f');
  455. });
  456. });
  457. });
  458. describe('when performing testDataSource', () => {
  459. it('should return successfully when call succeeds with labels', async () => {
  460. const ds = createLokiDSForTests({} as TemplateSrv);
  461. ds.metadataRequest = () => Promise.resolve(['avalue']);
  462. const result = await ds.testDatasource();
  463. expect(result).toStrictEqual({
  464. status: 'success',
  465. message: 'Data source connected and labels found.',
  466. });
  467. });
  468. it('should return error when call succeeds without labels', async () => {
  469. const ds = createLokiDSForTests({} as TemplateSrv);
  470. ds.metadataRequest = () => Promise.resolve([]);
  471. const result = await ds.testDatasource();
  472. expect(result).toStrictEqual({
  473. status: 'error',
  474. message: 'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
  475. });
  476. });
  477. it('should return error status with no details when call fails with no details', async () => {
  478. const ds = createLokiDSForTests({} as TemplateSrv);
  479. ds.metadataRequest = () => Promise.reject({});
  480. const result = await ds.testDatasource();
  481. expect(result).toStrictEqual({
  482. status: 'error',
  483. message: 'Unable to fetch labels from Loki, please check the server logs for more details',
  484. });
  485. });
  486. it('should return error status with details when call fails with details', async () => {
  487. const ds = createLokiDSForTests({} as TemplateSrv);
  488. ds.metadataRequest = () =>
  489. Promise.reject({
  490. data: {
  491. message: 'error42',
  492. },
  493. });
  494. const result = await ds.testDatasource();
  495. expect(result).toStrictEqual({
  496. status: 'error',
  497. message: 'Unable to fetch labels from Loki (error42), please check the server logs for more details',
  498. });
  499. });
  500. });
  501. describe('when calling annotationQuery', () => {
  502. const getTestContext = (response: any, options: any = []) => {
  503. const query = makeAnnotationQueryRequest(options);
  504. fetchMock.mockImplementation(() => of(response));
  505. const ds = createLokiDSForTests();
  506. const promise = ds.annotationQuery(query);
  507. return { promise };
  508. };
  509. it('should transform the loki data to annotation response', async () => {
  510. const response: FetchResponse = {
  511. data: {
  512. data: {
  513. resultType: LokiResultType.Stream,
  514. result: [
  515. {
  516. stream: {
  517. label: 'value',
  518. label2: 'value ',
  519. },
  520. values: [['1549016857498000000', 'hello']],
  521. },
  522. {
  523. stream: {
  524. label: '', // empty value gets filtered
  525. label2: 'value2',
  526. label3: ' ', // whitespace value gets trimmed then filtered
  527. },
  528. values: [['1549024057498000000', 'hello 2']],
  529. },
  530. ],
  531. },
  532. status: 'success',
  533. },
  534. } as unknown as FetchResponse;
  535. const { promise } = getTestContext(response, { stepInterval: '15s' });
  536. const res = await promise;
  537. expect(res.length).toBe(2);
  538. expect(res[0].text).toBe('hello');
  539. expect(res[0].tags).toEqual(['value']);
  540. expect(res[1].text).toBe('hello 2');
  541. expect(res[1].tags).toEqual(['value2']);
  542. });
  543. describe('Formatting', () => {
  544. const response: FetchResponse = {
  545. data: {
  546. data: {
  547. resultType: LokiResultType.Stream,
  548. result: [
  549. {
  550. stream: {
  551. label: 'value',
  552. label2: 'value2',
  553. label3: 'value3',
  554. },
  555. values: [['1549016857498000000', 'hello']],
  556. },
  557. ],
  558. },
  559. status: 'success',
  560. },
  561. } as unknown as FetchResponse;
  562. describe('When tagKeys is set', () => {
  563. it('should only include selected labels', async () => {
  564. const { promise } = getTestContext(response, { tagKeys: 'label2,label3', stepInterval: '15s' });
  565. const res = await promise;
  566. expect(res.length).toBe(1);
  567. expect(res[0].text).toBe('hello');
  568. expect(res[0].tags).toEqual(['value2', 'value3']);
  569. });
  570. });
  571. describe('When textFormat is set', () => {
  572. it('should fromat the text accordingly', async () => {
  573. const { promise } = getTestContext(response, { textFormat: 'hello {{label2}}', stepInterval: '15s' });
  574. const res = await promise;
  575. expect(res.length).toBe(1);
  576. expect(res[0].text).toBe('hello value2');
  577. });
  578. });
  579. describe('When titleFormat is set', () => {
  580. it('should fromat the title accordingly', async () => {
  581. const { promise } = getTestContext(response, { titleFormat: 'Title {{label2}}', stepInterval: '15s' });
  582. const res = await promise;
  583. expect(res.length).toBe(1);
  584. expect(res[0].title).toBe('Title value2');
  585. expect(res[0].text).toBe('hello');
  586. });
  587. });
  588. });
  589. });
  590. describe('metricFindQuery', () => {
  591. const getTestContext = (mock: LokiDatasource) => {
  592. const ds = createLokiDSForTests();
  593. ds.metadataRequest = mock.metadataRequest;
  594. return { ds };
  595. };
  596. const mock = makeMockLokiDatasource(
  597. { label1: ['value1', 'value2'], label2: ['value3', 'value4'] },
  598. { '{label1="value1", label2="value2"}': [{ label5: 'value5' }] }
  599. );
  600. it(`should return label names for Loki`, async () => {
  601. const { ds } = getTestContext(mock);
  602. const res = await ds.metricFindQuery('label_names()');
  603. expect(res).toEqual([{ text: 'label1' }, { text: 'label2' }]);
  604. });
  605. it(`should return label values for Loki when no matcher`, async () => {
  606. const { ds } = getTestContext(mock);
  607. const res = await ds.metricFindQuery('label_values(label1)');
  608. expect(res).toEqual([{ text: 'value1' }, { text: 'value2' }]);
  609. });
  610. it(`should return label values for Loki with matcher`, async () => {
  611. const { ds } = getTestContext(mock);
  612. const res = await ds.metricFindQuery('label_values({label1="value1", label2="value2"},label5)');
  613. expect(res).toEqual([{ text: 'value5' }]);
  614. });
  615. it(`should return empty array when incorrect query for Loki`, async () => {
  616. const { ds } = getTestContext(mock);
  617. const res = await ds.metricFindQuery('incorrect_query');
  618. expect(res).toEqual([]);
  619. });
  620. });
  621. describe('modifyQuery', () => {
  622. describe('when called with ADD_FILTER', () => {
  623. describe('and query has no parser', () => {
  624. it('then the correct label should be added for logs query', () => {
  625. const query: LokiQuery = { refId: 'A', expr: '{bar="baz"}' };
  626. const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER' };
  627. const ds = createLokiDSForTests();
  628. const result = ds.modifyQuery(query, action);
  629. expect(result.refId).toEqual('A');
  630. expect(result.expr).toEqual('{bar="baz",job="grafana"}');
  631. });
  632. it('then the correctly escaped label should be added for logs query', () => {
  633. const query: LokiQuery = { refId: 'A', expr: '{bar="baz"}' };
  634. const action = { key: 'job', value: '\\test', type: 'ADD_FILTER' };
  635. const ds = createLokiDSForTests();
  636. const result = ds.modifyQuery(query, action);
  637. expect(result.refId).toEqual('A');
  638. expect(result.expr).toEqual('{bar="baz",job="\\\\test"}');
  639. });
  640. it('then the correct label should be added for metrics query', () => {
  641. const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' };
  642. const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER' };
  643. const ds = createLokiDSForTests();
  644. const result = ds.modifyQuery(query, action);
  645. expect(result.refId).toEqual('A');
  646. expect(result.expr).toEqual('rate({bar="baz",job="grafana"}[5m])');
  647. });
  648. describe('and query has parser', () => {
  649. it('then the correct label should be added for logs query', () => {
  650. const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt' };
  651. const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER' };
  652. const ds = createLokiDSForTests();
  653. const result = ds.modifyQuery(query, action);
  654. expect(result.refId).toEqual('A');
  655. expect(result.expr).toEqual('{bar="baz"} | logfmt | job="grafana"');
  656. });
  657. it('then the correct label should be added for metrics query', () => {
  658. const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"} | logfmt [5m])' };
  659. const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER' };
  660. const ds = createLokiDSForTests();
  661. const result = ds.modifyQuery(query, action);
  662. expect(result.refId).toEqual('A');
  663. expect(result.expr).toEqual('rate({bar="baz",job="grafana"} | logfmt [5m])');
  664. });
  665. });
  666. });
  667. });
  668. describe('when called with ADD_FILTER_OUT', () => {
  669. describe('and query has no parser', () => {
  670. it('then the correct label should be added for logs query', () => {
  671. const query: LokiQuery = { refId: 'A', expr: '{bar="baz"}' };
  672. const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER_OUT' };
  673. const ds = createLokiDSForTests();
  674. const result = ds.modifyQuery(query, action);
  675. expect(result.refId).toEqual('A');
  676. expect(result.expr).toEqual('{bar="baz",job!="grafana"}');
  677. });
  678. it('then the correctly escaped label should be added for logs query', () => {
  679. const query: LokiQuery = { refId: 'A', expr: '{bar="baz"}' };
  680. const action = { key: 'job', value: '"test', type: 'ADD_FILTER_OUT' };
  681. const ds = createLokiDSForTests();
  682. const result = ds.modifyQuery(query, action);
  683. expect(result.refId).toEqual('A');
  684. expect(result.expr).toEqual('{bar="baz",job!="\\"test"}');
  685. });
  686. it('then the correct label should be added for metrics query', () => {
  687. const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' };
  688. const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER_OUT' };
  689. const ds = createLokiDSForTests();
  690. const result = ds.modifyQuery(query, action);
  691. expect(result.refId).toEqual('A');
  692. expect(result.expr).toEqual('rate({bar="baz",job!="grafana"}[5m])');
  693. });
  694. describe('and query has parser', () => {
  695. it('then the correct label should be added for logs query', () => {
  696. const query: LokiQuery = { refId: 'A', expr: '{bar="baz"} | logfmt' };
  697. const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER_OUT' };
  698. const ds = createLokiDSForTests();
  699. const result = ds.modifyQuery(query, action);
  700. expect(result.refId).toEqual('A');
  701. expect(result.expr).toEqual('{bar="baz"} | logfmt | job!="grafana"');
  702. });
  703. it('then the correct label should be added for metrics query', () => {
  704. const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"} | logfmt [5m])' };
  705. const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER_OUT' };
  706. const ds = createLokiDSForTests();
  707. const result = ds.modifyQuery(query, action);
  708. expect(result.refId).toEqual('A');
  709. expect(result.expr).toEqual('rate({bar="baz",job!="grafana"} | logfmt [5m])');
  710. });
  711. });
  712. });
  713. });
  714. });
  715. describe('addAdHocFilters', () => {
  716. let ds: LokiDatasource;
  717. let adHocFilters: AdHocFilter[];
  718. describe('when called with "=" operator', () => {
  719. beforeEach(() => {
  720. adHocFilters = [
  721. {
  722. condition: '',
  723. key: 'job',
  724. operator: '=',
  725. value: 'grafana',
  726. },
  727. ];
  728. const templateSrvMock = {
  729. getAdhocFilters: (): AdHocFilter[] => adHocFilters,
  730. replace: (a: string) => a,
  731. } as unknown as TemplateSrv;
  732. ds = createLokiDSForTests(templateSrvMock);
  733. });
  734. describe('and query has no parser', () => {
  735. it('then the correct label should be added for logs query', () => {
  736. assertAdHocFilters('{bar="baz"}', '{bar="baz",job="grafana"}', ds);
  737. });
  738. it('then the correct label should be added for metrics query', () => {
  739. assertAdHocFilters('rate({bar="baz"}[5m])', 'rate({bar="baz",job="grafana"}[5m])', ds);
  740. });
  741. });
  742. describe('and query has parser', () => {
  743. it('then the correct label should be added for logs query', () => {
  744. assertAdHocFilters('{bar="baz"} | logfmt', '{bar="baz",job="grafana"} | logfmt', ds);
  745. });
  746. it('then the correct label should be added for metrics query', () => {
  747. assertAdHocFilters('rate({bar="baz"} | logfmt [5m])', 'rate({bar="baz",job="grafana"} | logfmt [5m])', ds);
  748. });
  749. });
  750. });
  751. describe('when called with "!=" operator', () => {
  752. beforeEach(() => {
  753. adHocFilters = [
  754. {
  755. condition: '',
  756. key: 'job',
  757. operator: '!=',
  758. value: 'grafana',
  759. },
  760. ];
  761. const templateSrvMock = {
  762. getAdhocFilters: (): AdHocFilter[] => adHocFilters,
  763. replace: (a: string) => a,
  764. } as unknown as TemplateSrv;
  765. ds = createLokiDSForTests(templateSrvMock);
  766. });
  767. describe('and query has no parser', () => {
  768. it('then the correct label should be added for logs query', () => {
  769. assertAdHocFilters('{bar="baz"}', '{bar="baz",job!="grafana"}', ds);
  770. });
  771. it('then the correct label should be added for metrics query', () => {
  772. assertAdHocFilters('rate({bar="baz"}[5m])', 'rate({bar="baz",job!="grafana"}[5m])', ds);
  773. });
  774. });
  775. describe('and query has parser', () => {
  776. it('then the correct label should be added for logs query', () => {
  777. assertAdHocFilters('{bar="baz"} | logfmt', '{bar="baz",job!="grafana"} | logfmt', ds);
  778. });
  779. it('then the correct label should be added for metrics query', () => {
  780. assertAdHocFilters('rate({bar="baz"} | logfmt [5m])', 'rate({bar="baz",job!="grafana"} | logfmt [5m])', ds);
  781. });
  782. });
  783. });
  784. });
  785. describe('adjustInterval', () => {
  786. const dynamicInterval = 15;
  787. const range = 1642;
  788. const resolution = 1;
  789. const ds = createLokiDSForTests();
  790. it('should return the interval as a factor of dynamicInterval and resolution', () => {
  791. let interval = ds.adjustInterval(dynamicInterval, resolution, range);
  792. expect(interval).toBe(resolution * dynamicInterval);
  793. });
  794. it('should not return a value less than the safe interval', () => {
  795. let safeInterval = range / 11000;
  796. if (safeInterval > 1) {
  797. safeInterval = Math.ceil(safeInterval);
  798. }
  799. const unsafeInterval = safeInterval - 0.01;
  800. let interval = ds.adjustInterval(unsafeInterval, resolution, range);
  801. expect(interval).toBeGreaterThanOrEqual(safeInterval);
  802. });
  803. });
  804. describe('prepareLogRowContextQueryTarget', () => {
  805. const ds = createLokiDSForTests();
  806. it('creates query with only labels from /labels API', () => {
  807. const row: LogRowModel = {
  808. rowIndex: 0,
  809. dataFrame: new MutableDataFrame({
  810. fields: [
  811. {
  812. name: 'ts',
  813. type: FieldType.time,
  814. values: [0],
  815. },
  816. ],
  817. }),
  818. labels: { bar: 'baz', foo: 'uniqueParsedLabel' },
  819. uid: '1',
  820. } as any;
  821. //Mock stored labels to only include "bar" label
  822. jest.spyOn(ds.languageProvider, 'getLabelKeys').mockImplementation(() => ['bar']);
  823. const contextQuery = ds.prepareLogRowContextQueryTarget(row, 10, 'BACKWARD');
  824. expect(contextQuery.query.expr).toContain('baz');
  825. expect(contextQuery.query.expr).not.toContain('uniqueParsedLabel');
  826. });
  827. });
  828. describe('logs volume data provider', () => {
  829. it('creates provider for logs query', () => {
  830. const ds = createLokiDSForTests();
  831. const options = getQueryOptions<LokiQuery>({
  832. targets: [{ expr: '{label=value}', refId: 'A' }],
  833. });
  834. expect(ds.getLogsVolumeDataProvider(options)).toBeDefined();
  835. });
  836. it('does not create provider for metrics query', () => {
  837. const ds = createLokiDSForTests();
  838. const options = getQueryOptions<LokiQuery>({
  839. targets: [{ expr: 'rate({label=value}[1m])', refId: 'A' }],
  840. });
  841. expect(ds.getLogsVolumeDataProvider(options)).not.toBeDefined();
  842. });
  843. it('creates provider if at least one query is a logs query', () => {
  844. const ds = createLokiDSForTests();
  845. const options = getQueryOptions<LokiQuery>({
  846. targets: [
  847. { expr: 'rate({label=value}[1m])', refId: 'A' },
  848. { expr: '{label=value}', refId: 'B' },
  849. ],
  850. });
  851. expect(ds.getLogsVolumeDataProvider(options)).toBeDefined();
  852. });
  853. it('does not create provider if there is only an instant logs query', () => {
  854. const ds = createLokiDSForTests();
  855. const options = getQueryOptions<LokiQuery>({
  856. targets: [{ expr: '{label=value', refId: 'A', queryType: LokiQueryType.Instant }],
  857. });
  858. expect(ds.getLogsVolumeDataProvider(options)).not.toBeDefined();
  859. });
  860. });
  861. describe('importing queries', () => {
  862. it('keeps all labels when no labels are loaded', async () => {
  863. const ds = createLokiDSForTests();
  864. fetchMock.mockImplementation(() => of(createFetchResponse({ data: [] })));
  865. const queries = await ds.importFromAbstractQueries([
  866. {
  867. refId: 'A',
  868. labelMatchers: [
  869. { name: 'foo', operator: AbstractLabelOperator.Equal, value: 'bar' },
  870. { name: 'foo2', operator: AbstractLabelOperator.Equal, value: 'bar2' },
  871. ],
  872. },
  873. ]);
  874. expect(queries[0].expr).toBe('{foo="bar", foo2="bar2"}');
  875. });
  876. it('filters out non existing labels', async () => {
  877. const ds = createLokiDSForTests();
  878. fetchMock.mockImplementation(() => of(createFetchResponse({ data: ['foo'] })));
  879. const queries = await ds.importFromAbstractQueries([
  880. {
  881. refId: 'A',
  882. labelMatchers: [
  883. { name: 'foo', operator: AbstractLabelOperator.Equal, value: 'bar' },
  884. { name: 'foo2', operator: AbstractLabelOperator.Equal, value: 'bar2' },
  885. ],
  886. },
  887. ]);
  888. expect(queries[0].expr).toBe('{foo="bar"}');
  889. });
  890. });
  891. });
  892. describe('isMetricsQuery', () => {
  893. it('should return true for metrics query', () => {
  894. const query = 'rate({label=value}[1m])';
  895. expect(isMetricsQuery(query)).toBeTruthy();
  896. });
  897. it('should return false for logs query', () => {
  898. const query = '{label=value}';
  899. expect(isMetricsQuery(query)).toBeFalsy();
  900. });
  901. it('should not blow up on empty query', () => {
  902. const query = '';
  903. expect(isMetricsQuery(query)).toBeFalsy();
  904. });
  905. });
  906. describe('applyTemplateVariables', () => {
  907. it('should add the adhoc filter to the query', () => {
  908. const ds = createLokiDSForTests();
  909. const spy = jest.spyOn(ds, 'addAdHocFilters');
  910. ds.applyTemplateVariables({ expr: '{test}', refId: 'A' }, {});
  911. expect(spy).toHaveBeenCalledWith('{test}');
  912. });
  913. });
  914. function assertAdHocFilters(query: string, expectedResults: string, ds: LokiDatasource) {
  915. const lokiQuery: LokiQuery = { refId: 'A', expr: query };
  916. const result = ds.addAdHocFilters(lokiQuery.expr);
  917. expect(result).toEqual(expectedResults);
  918. }
  919. function createLokiDSForTests(
  920. templateSrvMock = {
  921. getAdhocFilters: (): any[] => [],
  922. replace: (a: string) => a,
  923. } as unknown as TemplateSrv
  924. ): LokiDatasource {
  925. const instanceSettings: any = {
  926. url: 'myloggingurl',
  927. };
  928. const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
  929. const customSettings = { ...instanceSettings, jsonData: customData };
  930. return new LokiDatasource(customSettings, templateSrvMock, timeSrvStub as any);
  931. }
  932. function makeAnnotationQueryRequest(options: any): AnnotationQueryRequest<LokiQuery> {
  933. const timeRange = {
  934. from: dateTime(),
  935. to: dateTime(),
  936. };
  937. return {
  938. annotation: {
  939. expr: '{test=test}',
  940. refId: '',
  941. datasource: 'loki',
  942. enable: true,
  943. name: 'test-annotation',
  944. iconColor: 'red',
  945. ...options,
  946. },
  947. dashboard: {
  948. id: 1,
  949. } as any,
  950. range: {
  951. ...timeRange,
  952. raw: timeRange,
  953. },
  954. rangeRaw: timeRange,
  955. };
  956. }