mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
* Loki: Pass only wanted query props when importing from Prometheus * Update * Update public/app/plugins/datasource/loki/language_provider.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Add test * Fix strict error Co-authored-by: Giordano Ricci <me@giordanoricci.com>
328 lines
13 KiB
TypeScript
328 lines
13 KiB
TypeScript
import Plain from 'slate-plain-serializer';
|
|
|
|
import LanguageProvider, { LokiHistoryItem } from './language_provider';
|
|
import { TypeaheadInput } from '@grafana/ui';
|
|
|
|
import { makeMockLokiDatasource } from './mocks';
|
|
import LokiDatasource from './datasource';
|
|
import { DataQuery, DataSourceApi } from '@grafana/data';
|
|
|
|
jest.mock('app/store/store', () => ({
|
|
store: {
|
|
getState: jest.fn().mockReturnValue({
|
|
explore: {
|
|
left: {
|
|
mode: 'Logs',
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}));
|
|
|
|
describe('Language completion provider', () => {
|
|
const datasource = makeMockLokiDatasource({});
|
|
|
|
describe('query suggestions', () => {
|
|
it('returns no suggestions on empty context', async () => {
|
|
const instance = new LanguageProvider(datasource);
|
|
const value = Plain.deserialize('');
|
|
const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
|
expect(result.context).toBeUndefined();
|
|
|
|
expect(result.suggestions.length).toEqual(0);
|
|
});
|
|
|
|
it('returns history on empty context when history was provided', async () => {
|
|
const instance = new LanguageProvider(datasource);
|
|
const value = Plain.deserialize('');
|
|
const history: LokiHistoryItem[] = [
|
|
{
|
|
query: { refId: '1', expr: '{app="foo"}' },
|
|
ts: 1,
|
|
},
|
|
];
|
|
const result = await instance.provideCompletionItems(
|
|
{ text: '', prefix: '', value, wrapperClasses: [] },
|
|
{ history }
|
|
);
|
|
expect(result.context).toBeUndefined();
|
|
|
|
expect(result.suggestions).toMatchObject([
|
|
{
|
|
label: 'History',
|
|
items: [
|
|
{
|
|
label: '{app="foo"}',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns function and history suggestions', async () => {
|
|
const instance = new LanguageProvider(datasource);
|
|
const input = createTypeaheadInput('m', 'm', undefined, 1, [], instance);
|
|
// Historic expressions don't have to match input, filtering is done in field
|
|
const history: LokiHistoryItem[] = [
|
|
{
|
|
query: { refId: '1', expr: '{app="foo"}' },
|
|
ts: 1,
|
|
},
|
|
];
|
|
const result = await instance.provideCompletionItems(input, { history });
|
|
expect(result.context).toBeUndefined();
|
|
expect(result.suggestions.length).toEqual(2);
|
|
expect(result.suggestions[0].label).toEqual('History');
|
|
expect(result.suggestions[1].label).toEqual('Functions');
|
|
});
|
|
|
|
it('returns pipe operations on pipe context', async () => {
|
|
const instance = new LanguageProvider(datasource);
|
|
const input = createTypeaheadInput('{app="test"} | ', ' ', '', 15, ['context-pipe']);
|
|
const result = await instance.provideCompletionItems(input);
|
|
expect(result.context).toBeUndefined();
|
|
expect(result.suggestions.length).toEqual(2);
|
|
expect(result.suggestions[0].label).toEqual('Operators');
|
|
expect(result.suggestions[1].label).toEqual('Parsers');
|
|
});
|
|
});
|
|
|
|
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,
|
|
});
|
|
});
|
|
});
|
|
|
|
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);
|
|
const result = await provider.provideCompletionItems(input);
|
|
expect(result.context).toBe('context-labels');
|
|
expect(result.suggestions).toEqual([
|
|
{
|
|
items: [
|
|
{ label: 'label1', filterText: '"label1"' },
|
|
{ label: 'label2', filterText: '"label2"' },
|
|
],
|
|
label: 'Labels',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns all label suggestions on selector when starting to type', async () => {
|
|
const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
|
|
const provider = await getLanguageProvider(datasource);
|
|
const input = createTypeaheadInput('{l}', '', '', 2);
|
|
const result = await provider.provideCompletionItems(input);
|
|
expect(result.context).toBe('context-labels');
|
|
expect(result.suggestions).toEqual([
|
|
{
|
|
items: [
|
|
{ label: 'label1', filterText: '"label1"' },
|
|
{ label: 'label2', filterText: '"label2"' },
|
|
],
|
|
label: 'Labels',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
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);
|
|
const result = await provider.provideCompletionItems(input);
|
|
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);
|
|
const result = await provider.provideCompletionItems(input);
|
|
expect(result.context).toBe('context-labels');
|
|
expect(result.suggestions).toEqual([{ items: [{ label: 'label2' }], label: 'Labels' }]);
|
|
});
|
|
});
|
|
|
|
describe('label suggestions', () => {
|
|
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');
|
|
let result = await provider.provideCompletionItems(input);
|
|
|
|
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"',
|
|
},
|
|
]);
|
|
});
|
|
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"',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
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');
|
|
const labelValues = await provider.fetchLabelValues('testkey');
|
|
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');
|
|
const labelValues = await provider.fetchLabelValues('testkey');
|
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
|
expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
|
|
|
|
const nextLabelValues = await provider.fetchLabelValues('testkey');
|
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
|
expect(nextLabelValues).toEqual(['label1_val1', 'label1_val2']);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Request URL', () => {
|
|
it('should contain range params', async () => {
|
|
const datasourceWithLabels = makeMockLokiDatasource({ other: [] });
|
|
const rangeParams = datasourceWithLabels.getTimeRangeParams();
|
|
const datasourceSpy = jest.spyOn(datasourceWithLabels as any, 'metadataRequest');
|
|
|
|
const instance = new LanguageProvider(datasourceWithLabels);
|
|
instance.fetchLabels();
|
|
const expectedUrl = '/loki/api/v1/label';
|
|
expect(datasourceSpy).toHaveBeenCalledWith(expectedUrl, rangeParams);
|
|
});
|
|
});
|
|
|
|
describe('Query imports', () => {
|
|
const datasource = makeMockLokiDatasource({});
|
|
|
|
it('returns empty queries for unknown origin datasource', async () => {
|
|
const instance = new LanguageProvider(datasource);
|
|
const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' } as DataQuery], {
|
|
meta: { id: 'unknown' },
|
|
} as DataSourceApi);
|
|
expect(result).toEqual([{ refId: 'bar', expr: '' }]);
|
|
});
|
|
|
|
describe('prometheus query imports', () => {
|
|
it('always results in range query type', async () => {
|
|
const instance = new LanguageProvider(datasource);
|
|
const result = await instance.importQueries(
|
|
[{ refId: 'bar', expr: '{job="grafana"}', instant: true, range: false } as DataQuery],
|
|
{
|
|
meta: { id: 'prometheus' },
|
|
} as DataSourceApi
|
|
);
|
|
expect(result).toEqual([{ refId: 'bar', expr: '{job="grafana"}', range: true }]);
|
|
expect(result).not.toHaveProperty('instant');
|
|
});
|
|
|
|
it('returns empty query from metric-only query', async () => {
|
|
const instance = new LanguageProvider(datasource);
|
|
const result = await instance.importPrometheusQuery('foo');
|
|
expect(result).toEqual('');
|
|
});
|
|
|
|
it('returns empty query from selector query if label is not available', async () => {
|
|
const datasourceWithLabels = makeMockLokiDatasource({ other: [] });
|
|
const instance = new LanguageProvider(datasourceWithLabels);
|
|
const result = await instance.importPrometheusQuery('{foo="bar"}');
|
|
expect(result).toEqual('{}');
|
|
});
|
|
|
|
it('returns selector query from selector query with common labels', async () => {
|
|
const datasourceWithLabels = makeMockLokiDatasource({ foo: [] });
|
|
const instance = new LanguageProvider(datasourceWithLabels);
|
|
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
|
|
expect(result).toEqual('{foo="bar"}');
|
|
});
|
|
|
|
it('returns selector query from selector query with all labels if logging label list is empty', async () => {
|
|
const datasourceWithLabels = makeMockLokiDatasource({});
|
|
const instance = new LanguageProvider(datasourceWithLabels);
|
|
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
|
|
expect(result).toEqual('{baz="42",foo="bar"}');
|
|
});
|
|
});
|
|
});
|
|
|
|
async function getLanguageProvider(datasource: LokiDatasource) {
|
|
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,
|
|
wrapperClasses?: string[],
|
|
instance?: LanguageProvider
|
|
): TypeaheadInput {
|
|
const deserialized = Plain.deserialize(value);
|
|
const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1));
|
|
const valueWithSelection = deserialized.setSelection(range);
|
|
return {
|
|
text,
|
|
prefix: instance ? instance.cleanText(text) : '',
|
|
wrapperClasses: wrapperClasses || ['context-labels'],
|
|
value: valueWithSelection,
|
|
labelKey,
|
|
};
|
|
}
|