language_provider.test.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import Plain from 'slate-plain-serializer';
  2. import { Editor as SlateEditor } from 'slate';
  3. import LanguageProvider, { LABEL_REFRESH_INTERVAL, LokiHistoryItem, rangeToParams } from './language_provider';
  4. import { AbsoluteTimeRange } from '@grafana/data';
  5. import { advanceTo, clear, advanceBy } from 'jest-date-mock';
  6. import { beforeEach } from 'test/lib/common';
  7. import { TypeaheadInput } from '../../../types';
  8. import { makeMockLokiDatasource } from './mocks';
  9. import LokiDatasource from './datasource';
  10. describe('Language completion provider', () => {
  11. const datasource = makeMockLokiDatasource({});
  12. const rangeMock: AbsoluteTimeRange = {
  13. from: 1560153109000,
  14. to: 1560163909000,
  15. };
  16. describe('empty query suggestions', () => {
  17. it('returns no suggestions on empty context', async () => {
  18. const instance = new LanguageProvider(datasource);
  19. const value = Plain.deserialize('');
  20. const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
  21. expect(result.context).toBeUndefined();
  22. expect(result.suggestions.length).toEqual(0);
  23. });
  24. it('returns default suggestions with history on empty context when history was provided', async () => {
  25. const instance = new LanguageProvider(datasource);
  26. const value = Plain.deserialize('');
  27. const history: LokiHistoryItem[] = [
  28. {
  29. query: { refId: '1', expr: '{app="foo"}' },
  30. ts: 1,
  31. },
  32. ];
  33. const result = await instance.provideCompletionItems(
  34. { text: '', prefix: '', value, wrapperClasses: [] },
  35. { history, absoluteRange: rangeMock }
  36. );
  37. expect(result.context).toBeUndefined();
  38. expect(result.suggestions).toMatchObject([
  39. {
  40. label: 'History',
  41. items: [
  42. {
  43. label: '{app="foo"}',
  44. },
  45. ],
  46. },
  47. ]);
  48. });
  49. it('returns no suggestions within regexp', async () => {
  50. const instance = new LanguageProvider(datasource);
  51. const input = createTypeaheadInput('{} ()', '', undefined, 4, []);
  52. const history: LokiHistoryItem[] = [
  53. {
  54. query: { refId: '1', expr: '{app="foo"}' },
  55. ts: 1,
  56. },
  57. ];
  58. const result = await instance.provideCompletionItems(input, { history });
  59. expect(result.context).toBeUndefined();
  60. expect(result.suggestions.length).toEqual(0);
  61. });
  62. });
  63. describe('label suggestions', () => {
  64. it('returns default label suggestions on label context', async () => {
  65. const instance = new LanguageProvider(datasource);
  66. const value = Plain.deserialize('{}');
  67. const ed = new SlateEditor({ value });
  68. const valueWithSelection = ed.moveForward(1).value;
  69. const result = await instance.provideCompletionItems(
  70. {
  71. text: '',
  72. prefix: '',
  73. wrapperClasses: ['context-labels'],
  74. value: valueWithSelection,
  75. },
  76. { absoluteRange: rangeMock }
  77. );
  78. expect(result.context).toBe('context-labels');
  79. expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]);
  80. });
  81. it('returns label suggestions from Loki', async () => {
  82. const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
  83. const provider = await getLanguageProvider(datasource);
  84. const input = createTypeaheadInput('{}', '');
  85. const result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock });
  86. expect(result.context).toBe('context-labels');
  87. expect(result.suggestions).toEqual([{ items: [{ label: 'label1' }, { label: 'label2' }], label: 'Labels' }]);
  88. });
  89. it('returns label values suggestions from Loki', async () => {
  90. const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] });
  91. const provider = await getLanguageProvider(datasource);
  92. const input = createTypeaheadInput('{label1=}', '=', 'label1');
  93. let result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock });
  94. result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock });
  95. expect(result.context).toBe('context-label-values');
  96. expect(result.suggestions).toEqual([
  97. { items: [{ label: 'label1_val1' }, { label: 'label1_val2' }], label: 'Label values for "label1"' },
  98. ]);
  99. });
  100. });
  101. });
  102. describe('Request URL', () => {
  103. it('should contain range params', async () => {
  104. const rangeMock: AbsoluteTimeRange = {
  105. from: 1560153109000,
  106. to: 1560163909000,
  107. };
  108. const datasourceWithLabels = makeMockLokiDatasource({ other: [] });
  109. const datasourceSpy = jest.spyOn(datasourceWithLabels as any, 'metadataRequest');
  110. const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
  111. await instance.refreshLogLabels(rangeMock, true);
  112. const expectedUrl = '/api/prom/label';
  113. expect(datasourceSpy).toHaveBeenCalledWith(expectedUrl, rangeToParams(rangeMock));
  114. });
  115. });
  116. describe('Query imports', () => {
  117. const datasource = makeMockLokiDatasource({});
  118. const rangeMock: AbsoluteTimeRange = {
  119. from: 1560153109000,
  120. to: 1560163909000,
  121. };
  122. it('returns empty queries for unknown origin datasource', async () => {
  123. const instance = new LanguageProvider(datasource, { initialRange: rangeMock });
  124. const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown');
  125. expect(result).toEqual([{ refId: 'bar', expr: '' }]);
  126. });
  127. describe('prometheus query imports', () => {
  128. it('returns empty query from metric-only query', async () => {
  129. const instance = new LanguageProvider(datasource, { initialRange: rangeMock });
  130. const result = await instance.importPrometheusQuery('foo');
  131. expect(result).toEqual('');
  132. });
  133. it('returns empty query from selector query if label is not available', async () => {
  134. const datasourceWithLabels = makeMockLokiDatasource({ other: [] });
  135. const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
  136. const result = await instance.importPrometheusQuery('{foo="bar"}');
  137. expect(result).toEqual('{}');
  138. });
  139. it('returns selector query from selector query with common labels', async () => {
  140. const datasourceWithLabels = makeMockLokiDatasource({ foo: [] });
  141. const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
  142. const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
  143. expect(result).toEqual('{foo="bar"}');
  144. });
  145. it('returns selector query from selector query with all labels if logging label list is empty', async () => {
  146. const datasourceWithLabels = makeMockLokiDatasource({});
  147. const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
  148. const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
  149. expect(result).toEqual('{baz="42",foo="bar"}');
  150. });
  151. });
  152. });
  153. describe('Labels refresh', () => {
  154. const datasource = makeMockLokiDatasource({});
  155. const instance = new LanguageProvider(datasource);
  156. const rangeMock: AbsoluteTimeRange = {
  157. from: 1560153109000,
  158. to: 1560163909000,
  159. };
  160. beforeEach(() => {
  161. instance.fetchLogLabels = jest.fn();
  162. });
  163. afterEach(() => {
  164. jest.clearAllMocks();
  165. clear();
  166. });
  167. it("should not refresh labels if refresh interval hasn't passed", () => {
  168. advanceTo(new Date(2019, 1, 1, 0, 0, 0));
  169. instance.logLabelFetchTs = Date.now();
  170. advanceBy(LABEL_REFRESH_INTERVAL / 2);
  171. instance.refreshLogLabels(rangeMock);
  172. expect(instance.fetchLogLabels).not.toBeCalled();
  173. });
  174. it('should refresh labels if refresh interval passed', () => {
  175. advanceTo(new Date(2019, 1, 1, 0, 0, 0));
  176. instance.logLabelFetchTs = Date.now();
  177. advanceBy(LABEL_REFRESH_INTERVAL + 1);
  178. instance.refreshLogLabels(rangeMock);
  179. expect(instance.fetchLogLabels).toBeCalled();
  180. });
  181. });
  182. async function getLanguageProvider(datasource: LokiDatasource) {
  183. const instance = new LanguageProvider(datasource);
  184. instance.initialRange = {
  185. from: Date.now() - 10000,
  186. to: Date.now(),
  187. };
  188. await instance.start();
  189. return instance;
  190. }
  191. /**
  192. * @param value Value of the full input
  193. * @param text Last piece of text (not sure but in case of {label=} this would be just '=')
  194. * @param labelKey Label by which to search for values. Cutting corners a bit here as this should be inferred from value
  195. */
  196. function createTypeaheadInput(
  197. value: string,
  198. text: string,
  199. labelKey?: string,
  200. anchorOffset?: number,
  201. wrapperClasses?: string[]
  202. ): TypeaheadInput {
  203. const deserialized = Plain.deserialize(value);
  204. const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1));
  205. const valueWithSelection = deserialized.setSelection(range);
  206. return {
  207. text,
  208. prefix: '',
  209. wrapperClasses: wrapperClasses || ['context-labels'],
  210. value: valueWithSelection,
  211. labelKey,
  212. };
  213. }