language_provider.test.ts 8.9 KB

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