2018-11-13 15:35:20 +00:00
|
|
|
import Plain from 'slate-plain-serializer';
|
|
|
|
|
|
2021-03-02 16:58:14 +01:00
|
|
|
import LanguageProvider, { LokiHistoryItem } from './language_provider';
|
2019-10-31 08:27:01 +00:00
|
|
|
import { TypeaheadInput } from '@grafana/ui';
|
2019-09-23 12:26:05 +01:00
|
|
|
|
2019-09-12 10:02:49 +02:00
|
|
|
import { makeMockLokiDatasource } from './mocks';
|
2019-09-23 12:26:05 +01:00
|
|
|
import LokiDatasource from './datasource';
|
2021-12-14 14:36:47 +01:00
|
|
|
import { AbstractLabelOperator } from '@grafana/data';
|
2018-11-13 15:35:20 +00:00
|
|
|
|
2019-11-15 15:38:25 +00:00
|
|
|
jest.mock('app/store/store', () => ({
|
|
|
|
|
store: {
|
|
|
|
|
getState: jest.fn().mockReturnValue({
|
|
|
|
|
explore: {
|
|
|
|
|
left: {
|
|
|
|
|
mode: 'Logs',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
2018-11-13 15:35:20 +00:00
|
|
|
describe('Language completion provider', () => {
|
2019-09-12 10:02:49 +02:00
|
|
|
const datasource = makeMockLokiDatasource({});
|
2018-11-13 15:35:20 +00:00
|
|
|
|
2019-12-31 09:53:30 +01:00
|
|
|
describe('query suggestions', () => {
|
|
|
|
|
it('returns no suggestions on empty context', async () => {
|
2018-11-22 11:02:53 +01:00
|
|
|
const instance = new LanguageProvider(datasource);
|
2018-11-30 15:13:53 +01:00
|
|
|
const value = Plain.deserialize('');
|
2019-09-23 12:26:05 +01:00
|
|
|
const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
2018-11-22 11:02:53 +01:00
|
|
|
expect(result.context).toBeUndefined();
|
2019-09-23 12:26:05 +01:00
|
|
|
|
2019-12-31 09:53:30 +01:00
|
|
|
expect(result.suggestions.length).toEqual(0);
|
2018-11-22 11:02:53 +01:00
|
|
|
});
|
|
|
|
|
|
2019-12-31 09:53:30 +01:00
|
|
|
it('returns history on empty context when history was provided', async () => {
|
2018-11-22 11:02:53 +01:00
|
|
|
const instance = new LanguageProvider(datasource);
|
|
|
|
|
const value = Plain.deserialize('');
|
2019-09-12 10:02:49 +02:00
|
|
|
const history: LokiHistoryItem[] = [
|
2018-11-22 11:02:53 +01:00
|
|
|
{
|
|
|
|
|
query: { refId: '1', expr: '{app="foo"}' },
|
2019-09-12 10:02:49 +02:00
|
|
|
ts: 1,
|
2018-11-22 11:02:53 +01:00
|
|
|
},
|
|
|
|
|
];
|
2019-09-23 12:26:05 +01:00
|
|
|
const result = await instance.provideCompletionItems(
|
2019-07-08 16:14:48 +01:00
|
|
|
{ text: '', prefix: '', value, wrapperClasses: [] },
|
2021-03-02 16:58:14 +01:00
|
|
|
{ history }
|
2019-07-08 16:14:48 +01:00
|
|
|
);
|
2018-11-22 11:02:53 +01:00
|
|
|
expect(result.context).toBeUndefined();
|
2019-09-23 12:26:05 +01:00
|
|
|
|
2018-11-22 11:02:53 +01:00
|
|
|
expect(result.suggestions).toMatchObject([
|
|
|
|
|
{
|
|
|
|
|
label: 'History',
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
label: '{app="foo"}',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
2018-11-30 15:13:53 +01:00
|
|
|
|
2019-12-31 09:53:30 +01:00
|
|
|
it('returns function and history suggestions', async () => {
|
2018-11-30 15:13:53 +01:00
|
|
|
const instance = new LanguageProvider(datasource);
|
2019-12-31 09:53:30 +01:00
|
|
|
const input = createTypeaheadInput('m', 'm', undefined, 1, [], instance);
|
|
|
|
|
// Historic expressions don't have to match input, filtering is done in field
|
2019-09-12 10:02:49 +02:00
|
|
|
const history: LokiHistoryItem[] = [
|
2018-11-30 15:13:53 +01:00
|
|
|
{
|
|
|
|
|
query: { refId: '1', expr: '{app="foo"}' },
|
2019-09-12 10:02:49 +02:00
|
|
|
ts: 1,
|
2018-11-30 15:13:53 +01:00
|
|
|
},
|
|
|
|
|
];
|
2019-09-23 12:26:05 +01:00
|
|
|
const result = await instance.provideCompletionItems(input, { history });
|
2018-11-30 15:13:53 +01:00
|
|
|
expect(result.context).toBeUndefined();
|
2019-12-31 09:53:30 +01:00
|
|
|
expect(result.suggestions.length).toEqual(2);
|
|
|
|
|
expect(result.suggestions[0].label).toEqual('History');
|
|
|
|
|
expect(result.suggestions[1].label).toEqual('Functions');
|
2018-11-30 15:13:53 +01:00
|
|
|
});
|
2020-10-14 10:22:39 +02:00
|
|
|
|
|
|
|
|
it('returns pipe operations on pipe context', async () => {
|
|
|
|
|
const instance = new LanguageProvider(datasource);
|
|
|
|
|
const input = createTypeaheadInput('{app="test"} | ', ' ', '', 15, ['context-pipe']);
|
2021-03-02 16:58:14 +01:00
|
|
|
const result = await instance.provideCompletionItems(input);
|
2020-10-14 10:22:39 +02:00
|
|
|
expect(result.context).toBeUndefined();
|
|
|
|
|
expect(result.suggestions.length).toEqual(2);
|
|
|
|
|
expect(result.suggestions[0].label).toEqual('Operators');
|
|
|
|
|
expect(result.suggestions[1].label).toEqual('Parsers');
|
|
|
|
|
});
|
2018-11-13 15:35:20 +00:00
|
|
|
});
|
|
|
|
|
|
2021-09-07 09:44:45 -04:00
|
|
|
describe('fetchSeries', () => {
|
|
|
|
|
it('should use match[] parameter', () => {
|
|
|
|
|
const datasource = makeMockLokiDatasource({}, { '{foo="bar"}': [{ label1: 'label_val1' }] });
|
|
|
|
|
const languageProvider = new LanguageProvider(datasource);
|
|
|
|
|
const fetchSeries = languageProvider.fetchSeries;
|
|
|
|
|
const requestSpy = jest.spyOn(languageProvider, 'request');
|
|
|
|
|
fetchSeries('{job="grafana"}');
|
|
|
|
|
expect(requestSpy).toHaveBeenCalledWith('/loki/api/v1/series', {
|
|
|
|
|
end: 1560163909000,
|
|
|
|
|
'match[]': '{job="grafana"}',
|
|
|
|
|
start: 1560153109000,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2020-03-04 17:17:02 +01:00
|
|
|
describe('label key suggestions', () => {
|
|
|
|
|
it('returns all label suggestions on empty selector', async () => {
|
|
|
|
|
const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
|
|
|
|
|
const provider = await getLanguageProvider(datasource);
|
|
|
|
|
const input = createTypeaheadInput('{}', '', '', 1);
|
2021-03-02 16:58:14 +01:00
|
|
|
const result = await provider.provideCompletionItems(input);
|
2018-11-13 15:35:20 +00:00
|
|
|
expect(result.context).toBe('context-labels');
|
2020-09-21 08:56:20 -07:00
|
|
|
expect(result.suggestions).toEqual([
|
|
|
|
|
{
|
|
|
|
|
items: [
|
|
|
|
|
{ label: 'label1', filterText: '"label1"' },
|
|
|
|
|
{ label: 'label2', filterText: '"label2"' },
|
|
|
|
|
],
|
|
|
|
|
label: 'Labels',
|
|
|
|
|
},
|
|
|
|
|
]);
|
2018-11-13 15:35:20 +00:00
|
|
|
});
|
2019-09-12 10:02:49 +02:00
|
|
|
|
2020-03-04 17:17:02 +01:00
|
|
|
it('returns all label suggestions on selector when starting to type', async () => {
|
2019-09-12 10:02:49 +02:00
|
|
|
const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
|
|
|
|
|
const provider = await getLanguageProvider(datasource);
|
2020-03-04 17:17:02 +01:00
|
|
|
const input = createTypeaheadInput('{l}', '', '', 2);
|
2021-03-02 16:58:14 +01:00
|
|
|
const result = await provider.provideCompletionItems(input);
|
2019-09-12 10:02:49 +02:00
|
|
|
expect(result.context).toBe('context-labels');
|
2020-09-21 08:56:20 -07:00
|
|
|
expect(result.suggestions).toEqual([
|
|
|
|
|
{
|
|
|
|
|
items: [
|
|
|
|
|
{ label: 'label1', filterText: '"label1"' },
|
|
|
|
|
{ label: 'label2', filterText: '"label2"' },
|
|
|
|
|
],
|
|
|
|
|
label: 'Labels',
|
|
|
|
|
},
|
|
|
|
|
]);
|
2019-09-12 10:02:49 +02:00
|
|
|
});
|
2020-03-04 17:17:02 +01:00
|
|
|
});
|
2019-09-12 10:02:49 +02:00
|
|
|
|
2020-03-04 17:17:02 +01:00
|
|
|
describe('label suggestions facetted', () => {
|
|
|
|
|
it('returns facetted label suggestions based on selector', async () => {
|
|
|
|
|
const datasource = makeMockLokiDatasource(
|
|
|
|
|
{ label1: [], label2: [] },
|
|
|
|
|
{ '{foo="bar"}': [{ label1: 'label_val1' }] }
|
|
|
|
|
);
|
|
|
|
|
const provider = await getLanguageProvider(datasource);
|
|
|
|
|
const input = createTypeaheadInput('{foo="bar",}', '', '', 11);
|
2021-03-02 16:58:14 +01:00
|
|
|
const result = await provider.provideCompletionItems(input);
|
2020-03-04 17:17:02 +01:00
|
|
|
expect(result.context).toBe('context-labels');
|
|
|
|
|
expect(result.suggestions).toEqual([{ items: [{ label: 'label1' }], label: 'Labels' }]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns facetted label suggestions for multipule selectors', async () => {
|
|
|
|
|
const datasource = makeMockLokiDatasource(
|
|
|
|
|
{ label1: [], label2: [] },
|
|
|
|
|
{ '{baz="42",foo="bar"}': [{ label2: 'label_val2' }] }
|
|
|
|
|
);
|
|
|
|
|
const provider = await getLanguageProvider(datasource);
|
|
|
|
|
const input = createTypeaheadInput('{baz="42",foo="bar",}', '', '', 20);
|
2021-03-02 16:58:14 +01:00
|
|
|
const result = await provider.provideCompletionItems(input);
|
2020-03-04 17:17:02 +01:00
|
|
|
expect(result.context).toBe('context-labels');
|
|
|
|
|
expect(result.suggestions).toEqual([{ items: [{ label: 'label2' }], label: 'Labels' }]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('label suggestions', () => {
|
2019-09-12 10:02:49 +02:00
|
|
|
it('returns label values suggestions from Loki', async () => {
|
|
|
|
|
const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] });
|
|
|
|
|
const provider = await getLanguageProvider(datasource);
|
|
|
|
|
const input = createTypeaheadInput('{label1=}', '=', 'label1');
|
2021-03-02 16:58:14 +01:00
|
|
|
let result = await provider.provideCompletionItems(input);
|
2019-09-23 12:26:05 +01:00
|
|
|
|
2021-03-02 16:58:14 +01:00
|
|
|
result = await provider.provideCompletionItems(input);
|
2019-09-12 10:02:49 +02:00
|
|
|
expect(result.context).toBe('context-label-values');
|
|
|
|
|
expect(result.suggestions).toEqual([
|
2020-09-21 08:56:20 -07:00
|
|
|
{
|
|
|
|
|
items: [
|
|
|
|
|
{ label: 'label1_val1', filterText: '"label1_val1"' },
|
|
|
|
|
{ label: 'label1_val2', filterText: '"label1_val2"' },
|
|
|
|
|
],
|
|
|
|
|
label: 'Label values for "label1"',
|
|
|
|
|
},
|
2019-09-12 10:02:49 +02:00
|
|
|
]);
|
|
|
|
|
});
|
2021-03-15 14:11:43 +01:00
|
|
|
it('returns label values suggestions from Loki when re-editing', async () => {
|
|
|
|
|
const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] });
|
|
|
|
|
const provider = await getLanguageProvider(datasource);
|
|
|
|
|
const input = createTypeaheadInput('{label1="label1_v"}', 'label1_v', 'label1', 17, [
|
|
|
|
|
'attr-value',
|
|
|
|
|
'context-labels',
|
|
|
|
|
]);
|
|
|
|
|
let result = await provider.provideCompletionItems(input);
|
|
|
|
|
expect(result.context).toBe('context-label-values');
|
|
|
|
|
expect(result.suggestions).toEqual([
|
|
|
|
|
{
|
|
|
|
|
items: [
|
|
|
|
|
{ label: 'label1_val1', filterText: '"label1_val1"' },
|
|
|
|
|
{ label: 'label1_val2', filterText: '"label1_val2"' },
|
|
|
|
|
],
|
|
|
|
|
label: 'Label values for "label1"',
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
2018-11-13 15:35:20 +00:00
|
|
|
});
|
2020-05-05 13:19:52 +01:00
|
|
|
|
|
|
|
|
describe('label values', () => {
|
|
|
|
|
it('should fetch label values if not cached', async () => {
|
|
|
|
|
const datasource = makeMockLokiDatasource({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
|
|
|
|
|
const provider = await getLanguageProvider(datasource);
|
|
|
|
|
const requestSpy = jest.spyOn(provider, 'request');
|
2021-03-02 16:58:14 +01:00
|
|
|
const labelValues = await provider.fetchLabelValues('testkey');
|
2020-05-05 13:19:52 +01:00
|
|
|
expect(requestSpy).toHaveBeenCalled();
|
|
|
|
|
expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return cached values', async () => {
|
|
|
|
|
const datasource = makeMockLokiDatasource({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
|
|
|
|
|
const provider = await getLanguageProvider(datasource);
|
|
|
|
|
const requestSpy = jest.spyOn(provider, 'request');
|
2021-03-02 16:58:14 +01:00
|
|
|
const labelValues = await provider.fetchLabelValues('testkey');
|
2020-05-05 13:19:52 +01:00
|
|
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
|
|
|
|
|
|
2021-03-02 16:58:14 +01:00
|
|
|
const nextLabelValues = await provider.fetchLabelValues('testkey');
|
2020-05-05 13:19:52 +01:00
|
|
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(nextLabelValues).toEqual(['label1_val1', 'label1_val2']);
|
|
|
|
|
});
|
|
|
|
|
});
|
2018-11-13 15:35:20 +00:00
|
|
|
});
|
|
|
|
|
|
2019-07-08 16:14:48 +01:00
|
|
|
describe('Request URL', () => {
|
|
|
|
|
it('should contain range params', async () => {
|
2019-09-12 10:02:49 +02:00
|
|
|
const datasourceWithLabels = makeMockLokiDatasource({ other: [] });
|
2021-03-02 16:58:14 +01:00
|
|
|
const rangeParams = datasourceWithLabels.getTimeRangeParams();
|
2019-09-12 10:02:49 +02:00
|
|
|
const datasourceSpy = jest.spyOn(datasourceWithLabels as any, 'metadataRequest');
|
2019-07-08 16:14:48 +01:00
|
|
|
|
2021-03-02 16:58:14 +01:00
|
|
|
const instance = new LanguageProvider(datasourceWithLabels);
|
2021-05-12 11:49:20 +02:00
|
|
|
instance.fetchLabels();
|
2020-04-22 12:59:06 +01:00
|
|
|
const expectedUrl = '/loki/api/v1/label';
|
2021-03-02 16:58:14 +01:00
|
|
|
expect(datasourceSpy).toHaveBeenCalledWith(expectedUrl, rangeParams);
|
2019-07-08 16:14:48 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2018-11-13 15:35:20 +00:00
|
|
|
describe('Query imports', () => {
|
2019-09-12 10:02:49 +02:00
|
|
|
const datasource = makeMockLokiDatasource({});
|
2018-11-13 15:35:20 +00:00
|
|
|
|
2021-12-14 14:36:47 +01:00
|
|
|
it('returns empty queries', async () => {
|
2021-03-02 16:58:14 +01:00
|
|
|
const instance = new LanguageProvider(datasource);
|
2021-12-14 14:36:47 +01:00
|
|
|
const result = await instance.importFromAbstractQuery({ refId: 'bar', labelMatchers: [] });
|
|
|
|
|
expect(result).toEqual({ refId: 'bar', expr: '', range: true });
|
2018-11-13 15:35:20 +00:00
|
|
|
});
|
|
|
|
|
|
2021-12-14 14:36:47 +01:00
|
|
|
describe('exporting to abstract query', () => {
|
|
|
|
|
it('exports labels', async () => {
|
2021-10-12 13:05:57 +02:00
|
|
|
const instance = new LanguageProvider(datasource);
|
2021-12-14 14:36:47 +01:00
|
|
|
const abstractQuery = instance.exportToAbstractQuery({
|
|
|
|
|
refId: 'bar',
|
|
|
|
|
expr: '{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
|
|
|
|
|
instant: true,
|
|
|
|
|
range: false,
|
|
|
|
|
});
|
|
|
|
|
expect(abstractQuery).toMatchObject({
|
|
|
|
|
refId: 'bar',
|
|
|
|
|
labelMatchers: [
|
|
|
|
|
{ name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
|
|
|
|
|
{ name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
|
|
|
|
|
{ name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
|
|
|
|
|
{ name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
|
|
|
|
|
],
|
|
|
|
|
});
|
2018-11-23 11:15:25 +01:00
|
|
|
});
|
2018-11-13 15:35:20 +00:00
|
|
|
});
|
|
|
|
|
});
|
2019-03-25 12:08:28 +01:00
|
|
|
|
2019-09-23 12:26:05 +01:00
|
|
|
async function getLanguageProvider(datasource: LokiDatasource) {
|
2019-09-12 10:02:49 +02:00
|
|
|
const instance = new LanguageProvider(datasource);
|
|
|
|
|
await instance.start();
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param value Value of the full input
|
|
|
|
|
* @param text Last piece of text (not sure but in case of {label=} this would be just '=')
|
|
|
|
|
* @param labelKey Label by which to search for values. Cutting corners a bit here as this should be inferred from value
|
|
|
|
|
*/
|
|
|
|
|
function createTypeaheadInput(
|
|
|
|
|
value: string,
|
|
|
|
|
text: string,
|
|
|
|
|
labelKey?: string,
|
|
|
|
|
anchorOffset?: number,
|
2019-12-31 09:53:30 +01:00
|
|
|
wrapperClasses?: string[],
|
|
|
|
|
instance?: LanguageProvider
|
2019-09-12 10:02:49 +02:00
|
|
|
): TypeaheadInput {
|
|
|
|
|
const deserialized = Plain.deserialize(value);
|
2019-09-23 12:26:05 +01:00
|
|
|
const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1));
|
|
|
|
|
const valueWithSelection = deserialized.setSelection(range);
|
2019-09-12 10:02:49 +02:00
|
|
|
return {
|
|
|
|
|
text,
|
2019-12-31 09:53:30 +01:00
|
|
|
prefix: instance ? instance.cleanText(text) : '',
|
2019-09-12 10:02:49 +02:00
|
|
|
wrapperClasses: wrapperClasses || ['context-labels'],
|
|
|
|
|
value: valueWithSelection,
|
|
|
|
|
labelKey,
|
|
|
|
|
};
|
|
|
|
|
}
|