language_provider.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import Plain from 'slate-plain-serializer';
  2. import { AbstractLabelOperator } from '@grafana/data';
  3. import { TypeaheadInput } from '@grafana/ui';
  4. import { LokiDatasource } from './datasource';
  5. import LanguageProvider, { LokiHistoryItem } from './language_provider';
  6. import { makeMockLokiDatasource } from './mocks';
  7. import { LokiQueryType } from './types';
  8. jest.mock('app/store/store', () => ({
  9. store: {
  10. getState: jest.fn().mockReturnValue({
  11. explore: {
  12. left: {
  13. mode: 'Logs',
  14. },
  15. },
  16. }),
  17. },
  18. }));
  19. describe('Language completion provider', () => {
  20. const datasource = makeMockLokiDatasource({});
  21. describe('query suggestions', () => {
  22. it('returns no suggestions on empty context', async () => {
  23. const instance = new LanguageProvider(datasource);
  24. const value = Plain.deserialize('');
  25. const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
  26. expect(result.context).toBeUndefined();
  27. expect(result.suggestions.length).toEqual(0);
  28. });
  29. it('returns history on empty context when history was provided', async () => {
  30. const instance = new LanguageProvider(datasource);
  31. const value = Plain.deserialize('');
  32. const history: LokiHistoryItem[] = [
  33. {
  34. query: { refId: '1', expr: '{app="foo"}' },
  35. ts: 1,
  36. },
  37. ];
  38. const result = await instance.provideCompletionItems(
  39. { text: '', prefix: '', value, wrapperClasses: [] },
  40. { history }
  41. );
  42. expect(result.context).toBeUndefined();
  43. expect(result.suggestions).toMatchObject([
  44. {
  45. label: 'History',
  46. items: [
  47. {
  48. label: '{app="foo"}',
  49. },
  50. ],
  51. },
  52. ]);
  53. });
  54. it('returns function and history suggestions', async () => {
  55. const instance = new LanguageProvider(datasource);
  56. const input = createTypeaheadInput('m', 'm', undefined, 1, [], instance);
  57. // Historic expressions don't have to match input, filtering is done in field
  58. const history: LokiHistoryItem[] = [
  59. {
  60. query: { refId: '1', expr: '{app="foo"}' },
  61. ts: 1,
  62. },
  63. ];
  64. const result = await instance.provideCompletionItems(input, { history });
  65. expect(result.context).toBeUndefined();
  66. expect(result.suggestions.length).toEqual(2);
  67. expect(result.suggestions[0].label).toEqual('History');
  68. expect(result.suggestions[1].label).toEqual('Functions');
  69. });
  70. it('returns pipe operations on pipe context', async () => {
  71. const instance = new LanguageProvider(datasource);
  72. const input = createTypeaheadInput('{app="test"} | ', ' ', '', 15, ['context-pipe']);
  73. const result = await instance.provideCompletionItems(input);
  74. expect(result.context).toBeUndefined();
  75. expect(result.suggestions.length).toEqual(2);
  76. expect(result.suggestions[0].label).toEqual('Operators');
  77. expect(result.suggestions[1].label).toEqual('Parsers');
  78. });
  79. });
  80. describe('fetchSeries', () => {
  81. it('should use match[] parameter', () => {
  82. const datasource = makeMockLokiDatasource({}, { '{foo="bar"}': [{ label1: 'label_val1' }] });
  83. const languageProvider = new LanguageProvider(datasource);
  84. const fetchSeries = languageProvider.fetchSeries;
  85. const requestSpy = jest.spyOn(languageProvider, 'request');
  86. fetchSeries('{job="grafana"}');
  87. expect(requestSpy).toHaveBeenCalledWith('series', {
  88. end: 1560163909000,
  89. 'match[]': '{job="grafana"}',
  90. start: 1560153109000,
  91. });
  92. });
  93. });
  94. describe('fetchSeriesLabels', () => {
  95. it('should interpolate variable in series', () => {
  96. const datasource: LokiDatasource = {
  97. metadataRequest: () => ({ data: { data: [] as any[] } }),
  98. getTimeRangeParams: () => ({ start: 0, end: 1 }),
  99. interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'),
  100. } as any as LokiDatasource;
  101. const languageProvider = new LanguageProvider(datasource);
  102. const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
  103. const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue([]);
  104. fetchSeriesLabels('$stream');
  105. expect(requestSpy).toHaveBeenCalled();
  106. expect(requestSpy).toHaveBeenCalledWith('series', {
  107. end: 1,
  108. 'match[]': 'interpolated-stream',
  109. start: 0,
  110. });
  111. });
  112. });
  113. describe('label key suggestions', () => {
  114. it('returns all label suggestions on empty selector', async () => {
  115. const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
  116. const provider = await getLanguageProvider(datasource);
  117. const input = createTypeaheadInput('{}', '', '', 1);
  118. const result = await provider.provideCompletionItems(input);
  119. expect(result.context).toBe('context-labels');
  120. expect(result.suggestions).toEqual([
  121. {
  122. items: [
  123. { label: 'label1', filterText: '"label1"' },
  124. { label: 'label2', filterText: '"label2"' },
  125. ],
  126. label: 'Labels',
  127. },
  128. ]);
  129. });
  130. it('returns all label suggestions on selector when starting to type', async () => {
  131. const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
  132. const provider = await getLanguageProvider(datasource);
  133. const input = createTypeaheadInput('{l}', '', '', 2);
  134. const result = await provider.provideCompletionItems(input);
  135. expect(result.context).toBe('context-labels');
  136. expect(result.suggestions).toEqual([
  137. {
  138. items: [
  139. { label: 'label1', filterText: '"label1"' },
  140. { label: 'label2', filterText: '"label2"' },
  141. ],
  142. label: 'Labels',
  143. },
  144. ]);
  145. });
  146. });
  147. describe('label suggestions facetted', () => {
  148. it('returns facetted label suggestions based on selector', async () => {
  149. const datasource = makeMockLokiDatasource(
  150. { label1: [], label2: [] },
  151. { '{foo="bar"}': [{ label1: 'label_val1' }] }
  152. );
  153. const provider = await getLanguageProvider(datasource);
  154. const input = createTypeaheadInput('{foo="bar",}', '', '', 11);
  155. const result = await provider.provideCompletionItems(input);
  156. expect(result.context).toBe('context-labels');
  157. expect(result.suggestions).toEqual([{ items: [{ label: 'label1' }], label: 'Labels' }]);
  158. });
  159. it('returns facetted label suggestions for multipule selectors', async () => {
  160. const datasource = makeMockLokiDatasource(
  161. { label1: [], label2: [] },
  162. { '{baz="42",foo="bar"}': [{ label2: 'label_val2' }] }
  163. );
  164. const provider = await getLanguageProvider(datasource);
  165. const input = createTypeaheadInput('{baz="42",foo="bar",}', '', '', 20);
  166. const result = await provider.provideCompletionItems(input);
  167. expect(result.context).toBe('context-labels');
  168. expect(result.suggestions).toEqual([{ items: [{ label: 'label2' }], label: 'Labels' }]);
  169. });
  170. });
  171. describe('label suggestions', () => {
  172. it('returns label values suggestions from Loki', async () => {
  173. const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] });
  174. const provider = await getLanguageProvider(datasource);
  175. const input = createTypeaheadInput('{label1=}', '=', 'label1');
  176. let result = await provider.provideCompletionItems(input);
  177. result = await provider.provideCompletionItems(input);
  178. expect(result.context).toBe('context-label-values');
  179. expect(result.suggestions).toEqual([
  180. {
  181. items: [
  182. { label: 'label1_val1', filterText: '"label1_val1"' },
  183. { label: 'label1_val2', filterText: '"label1_val2"' },
  184. ],
  185. label: 'Label values for "label1"',
  186. },
  187. ]);
  188. });
  189. it('returns label values suggestions from Loki when re-editing', async () => {
  190. const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] });
  191. const provider = await getLanguageProvider(datasource);
  192. const input = createTypeaheadInput('{label1="label1_v"}', 'label1_v', 'label1', 17, [
  193. 'attr-value',
  194. 'context-labels',
  195. ]);
  196. let result = await provider.provideCompletionItems(input);
  197. expect(result.context).toBe('context-label-values');
  198. expect(result.suggestions).toEqual([
  199. {
  200. items: [
  201. { label: 'label1_val1', filterText: '"label1_val1"' },
  202. { label: 'label1_val2', filterText: '"label1_val2"' },
  203. ],
  204. label: 'Label values for "label1"',
  205. },
  206. ]);
  207. });
  208. });
  209. describe('label values', () => {
  210. it('should fetch label values if not cached', async () => {
  211. const datasource = makeMockLokiDatasource({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
  212. const provider = await getLanguageProvider(datasource);
  213. const requestSpy = jest.spyOn(provider, 'request');
  214. const labelValues = await provider.fetchLabelValues('testkey');
  215. expect(requestSpy).toHaveBeenCalled();
  216. expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
  217. });
  218. it('should return cached values', async () => {
  219. const datasource = makeMockLokiDatasource({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
  220. const provider = await getLanguageProvider(datasource);
  221. const requestSpy = jest.spyOn(provider, 'request');
  222. const labelValues = await provider.fetchLabelValues('testkey');
  223. expect(requestSpy).toHaveBeenCalledTimes(1);
  224. expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
  225. const nextLabelValues = await provider.fetchLabelValues('testkey');
  226. expect(requestSpy).toHaveBeenCalledTimes(1);
  227. expect(nextLabelValues).toEqual(['label1_val1', 'label1_val2']);
  228. });
  229. it('should encode special characters', async () => {
  230. const datasource = makeMockLokiDatasource({ '`\\"testkey': ['label1_val1', 'label1_val2'], label2: [] });
  231. const provider = await getLanguageProvider(datasource);
  232. const requestSpy = jest.spyOn(provider, 'request');
  233. await provider.fetchLabelValues('`\\"testkey');
  234. expect(requestSpy).toHaveBeenCalledWith('label/%60%5C%22testkey/values', expect.any(Object));
  235. });
  236. });
  237. });
  238. describe('Request URL', () => {
  239. it('should contain range params', async () => {
  240. const datasourceWithLabels = makeMockLokiDatasource({ other: [] });
  241. const rangeParams = datasourceWithLabels.getTimeRangeParams();
  242. const datasourceSpy = jest.spyOn(datasourceWithLabels as any, 'metadataRequest');
  243. const instance = new LanguageProvider(datasourceWithLabels);
  244. instance.fetchLabels();
  245. const expectedUrl = 'labels';
  246. expect(datasourceSpy).toHaveBeenCalledWith(expectedUrl, rangeParams);
  247. });
  248. });
  249. describe('Query imports', () => {
  250. const datasource = makeMockLokiDatasource({});
  251. it('returns empty queries', async () => {
  252. const instance = new LanguageProvider(datasource);
  253. const result = await instance.importFromAbstractQuery({ refId: 'bar', labelMatchers: [] });
  254. expect(result).toEqual({ refId: 'bar', expr: '', queryType: LokiQueryType.Range });
  255. });
  256. describe('exporting to abstract query', () => {
  257. it('exports labels', async () => {
  258. const instance = new LanguageProvider(datasource);
  259. const abstractQuery = instance.exportToAbstractQuery({
  260. refId: 'bar',
  261. expr: '{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
  262. instant: true,
  263. range: false,
  264. });
  265. expect(abstractQuery).toMatchObject({
  266. refId: 'bar',
  267. labelMatchers: [
  268. { name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
  269. { name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
  270. { name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
  271. { name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
  272. ],
  273. });
  274. });
  275. });
  276. });
  277. async function getLanguageProvider(datasource: LokiDatasource) {
  278. const instance = new LanguageProvider(datasource);
  279. await instance.start();
  280. return instance;
  281. }
  282. /**
  283. * @param value Value of the full input
  284. * @param text Last piece of text (not sure but in case of {label=} this would be just '=')
  285. * @param labelKey Label by which to search for values. Cutting corners a bit here as this should be inferred from value
  286. */
  287. function createTypeaheadInput(
  288. value: string,
  289. text: string,
  290. labelKey?: string,
  291. anchorOffset?: number,
  292. wrapperClasses?: string[],
  293. instance?: LanguageProvider
  294. ): TypeaheadInput {
  295. const deserialized = Plain.deserialize(value);
  296. const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1));
  297. const valueWithSelection = deserialized.setSelection(range);
  298. return {
  299. text,
  300. prefix: instance ? instance.cleanText(text) : '',
  301. wrapperClasses: wrapperClasses || ['context-labels'],
  302. value: valueWithSelection,
  303. labelKey,
  304. };
  305. }