mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Loki: Use label/<name>/values API instead of series API for label values discovery (#83044)
* Loki: Add label values API selector to setting and use label_values API when selected * remove CHANGELOG change * add docs * Support Loki 2.7+ only, so unconditionally use /labels API * Correct doc for fetchLabels and fetchLabelValues functions * Fixes after merge * Do not encode query parameter twice * return getLabelKeys in Completion Provider * docs * Add test for LabelParamEditor * Update public/app/plugins/datasource/loki/LogContextProvider.test.ts Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * toHaveBeenCalledWith --------- Co-authored-by: Matias Chomicki <matyax@gmail.com> Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
This commit is contained in:
parent
9c254c7e1e
commit
33170c4d07
@ -151,7 +151,7 @@ describe('Language completion provider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('label values', () => {
|
||||
describe('fetchLabelValues', () => {
|
||||
it('should fetch label values if not cached', async () => {
|
||||
const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
|
||||
const provider = await getLanguageProvider(datasource);
|
||||
@ -171,7 +171,7 @@ describe('Language completion provider', () => {
|
||||
const labelValues = await provider.fetchLabelValues('testkey', { streamSelector: '{foo="bar"}' });
|
||||
expect(requestSpy).toHaveBeenCalledWith('label/testkey/values', {
|
||||
end: 1560163909000,
|
||||
query: '%7Bfoo%3D%22bar%22%7D',
|
||||
query: '{foo="bar"}',
|
||||
start: 1560153109000,
|
||||
});
|
||||
expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
|
||||
@ -237,7 +237,7 @@ describe('Language completion provider', () => {
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith('label/testkey/values', {
|
||||
end: 1560163909000,
|
||||
query: '%7Bfoo%3D%22bar%22%7D',
|
||||
query: '{foo="bar"}',
|
||||
start: 1560153109000,
|
||||
});
|
||||
expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
|
||||
@ -263,12 +263,70 @@ describe('Language completion provider', () => {
|
||||
await provider.fetchLabelValues('`\\"testkey', { streamSelector: '{foo="\\bar"}' });
|
||||
|
||||
expect(requestSpy).toHaveBeenCalledWith(expect.any(String), {
|
||||
query: '%7Bfoo%3D%22%5Cbar%22%7D',
|
||||
query: '{foo="\\bar"}',
|
||||
start: expect.any(Number),
|
||||
end: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLabels', () => {
|
||||
it('should return labels', async () => {
|
||||
const datasourceWithLabels = setup({ other: [] });
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
const labels = await instance.fetchLabels();
|
||||
expect(labels).toEqual(['other']);
|
||||
});
|
||||
|
||||
it('should set labels', async () => {
|
||||
const datasourceWithLabels = setup({ other: [] });
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
await instance.fetchLabels();
|
||||
expect(instance.labelKeys).toEqual(['other']);
|
||||
});
|
||||
|
||||
it('should return empty array', async () => {
|
||||
const datasourceWithLabels = setup({});
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
const labels = await instance.fetchLabels();
|
||||
expect(labels).toEqual([]);
|
||||
});
|
||||
|
||||
it('should set empty array', async () => {
|
||||
const datasourceWithLabels = setup({});
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
await instance.fetchLabels();
|
||||
expect(instance.labelKeys).toEqual([]);
|
||||
});
|
||||
|
||||
it('should use time range param', async () => {
|
||||
const datasourceWithLabels = setup({});
|
||||
datasourceWithLabels.languageProvider.request = jest.fn();
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
instance.request = jest.fn();
|
||||
await instance.fetchLabels({ timeRange: mockTimeRange });
|
||||
expect(instance.request).toHaveBeenCalledWith('labels', datasourceWithLabels.getTimeRangeParams(mockTimeRange));
|
||||
});
|
||||
|
||||
it('should use series endpoint for request with stream selector', async () => {
|
||||
const datasourceWithLabels = setup({});
|
||||
datasourceWithLabels.languageProvider.request = jest.fn();
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
instance.request = jest.fn();
|
||||
await instance.fetchLabels({ streamSelector: '{foo="bar"}' });
|
||||
expect(instance.request).toHaveBeenCalledWith('series', {
|
||||
end: 1560163909000,
|
||||
'match[]': '{foo="bar"}',
|
||||
start: 1560153109000,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request URL', () => {
|
||||
@ -284,50 +342,6 @@ describe('Request URL', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLabels', () => {
|
||||
it('should return labels', async () => {
|
||||
const datasourceWithLabels = setup({ other: [] });
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
const labels = await instance.fetchLabels();
|
||||
expect(labels).toEqual(['other']);
|
||||
});
|
||||
|
||||
it('should set labels', async () => {
|
||||
const datasourceWithLabels = setup({ other: [] });
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
await instance.fetchLabels();
|
||||
expect(instance.labelKeys).toEqual(['other']);
|
||||
});
|
||||
|
||||
it('should return empty array', async () => {
|
||||
const datasourceWithLabels = setup({});
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
const labels = await instance.fetchLabels();
|
||||
expect(labels).toEqual([]);
|
||||
});
|
||||
|
||||
it('should set empty array', async () => {
|
||||
const datasourceWithLabels = setup({});
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
await instance.fetchLabels();
|
||||
expect(instance.labelKeys).toEqual([]);
|
||||
});
|
||||
|
||||
it('should use time range param', async () => {
|
||||
const datasourceWithLabels = setup({});
|
||||
datasourceWithLabels.languageProvider.request = jest.fn();
|
||||
|
||||
const instance = new LanguageProvider(datasourceWithLabels);
|
||||
instance.request = jest.fn();
|
||||
await instance.fetchLabels({ timeRange: mockTimeRange });
|
||||
expect(instance.request).toBeCalledWith('labels', datasourceWithLabels.getTimeRangeParams(mockTimeRange));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query imports', () => {
|
||||
const datasource = setup({});
|
||||
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
import { ParserAndLabelKeysResult, LokiQuery, LokiQueryType, LabelType } from './types';
|
||||
|
||||
const NS_IN_MS = 1000000;
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
|
||||
export default class LokiLanguageProvider extends LanguageProvider {
|
||||
labelKeys: string[];
|
||||
@ -118,6 +119,28 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch label keys using the best applicable endpoint.
|
||||
*
|
||||
* This asynchronous function returns all available label keys from the data source.
|
||||
* It returns a promise that resolves to an array of strings containing the label keys.
|
||||
*
|
||||
* @param options - (Optional) An object containing additional options.
|
||||
* @param options.streamSelector - (Optional) The stream selector to filter label keys. If not provided, all label keys are fetched.
|
||||
* @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used.
|
||||
* @returns A promise containing an array of label keys.
|
||||
* @throws An error if the fetch operation fails.
|
||||
*/
|
||||
async fetchLabels(options?: { streamSelector?: string; timeRange?: TimeRange }): Promise<string[]> {
|
||||
// If there is no stream selector - use /labels endpoint (https://github.com/grafana/loki/pull/11982)
|
||||
if (!options || !options.streamSelector) {
|
||||
return this.fetchLabelsByLabelsEndpoint(options);
|
||||
} else {
|
||||
const data = await this.fetchSeriesLabels(options.streamSelector, { timeRange: options.timeRange });
|
||||
return Object.keys(data ?? {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all label keys
|
||||
* This asynchronous function returns all available label keys from the data source.
|
||||
@ -128,7 +151,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
* @returns A promise containing an array of label keys.
|
||||
* @throws An error if the fetch operation fails.
|
||||
*/
|
||||
async fetchLabels(options?: { timeRange?: TimeRange }): Promise<string[]> {
|
||||
private async fetchLabelsByLabelsEndpoint(options?: { timeRange?: TimeRange }): Promise<string[]> {
|
||||
const url = 'labels';
|
||||
const range = options?.timeRange ?? this.getDefaultTimeRange();
|
||||
const timeRange = this.datasource.getTimeRangeParams(range);
|
||||
@ -229,9 +252,11 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
options?: { streamSelector?: string; timeRange?: TimeRange }
|
||||
): Promise<string[]> {
|
||||
const label = encodeURIComponent(this.datasource.interpolateString(labelName));
|
||||
const streamParam = options?.streamSelector
|
||||
? encodeURIComponent(this.datasource.interpolateString(options.streamSelector))
|
||||
: undefined;
|
||||
// Loki doesn't allow empty streamSelector {}, so we should not send it.
|
||||
const streamParam =
|
||||
options?.streamSelector && options.streamSelector !== EMPTY_SELECTOR
|
||||
? this.datasource.interpolateString(options.streamSelector)
|
||||
: undefined;
|
||||
|
||||
const url = `label/${label}/values`;
|
||||
const range = options?.timeRange ?? this.getDefaultTimeRange();
|
||||
|
@ -20,7 +20,7 @@ import { LokiQuery } from './types';
|
||||
|
||||
const defaultLanguageProviderMock = {
|
||||
start: jest.fn(),
|
||||
fetchSeriesLabels: jest.fn(() => ({ bar: ['baz'], xyz: ['abc'] })),
|
||||
fetchLabels: jest.fn(() => ['bar', 'xyz']),
|
||||
getLabelKeys: jest.fn(() => ['bar', 'xyz']),
|
||||
} as unknown as LokiLanguageProvider;
|
||||
|
||||
@ -425,14 +425,14 @@ describe('LogContextProvider', () => {
|
||||
expect(filters).toEqual([]);
|
||||
});
|
||||
|
||||
it('should call fetchSeriesLabels if parser', async () => {
|
||||
it('should call fetchLabels with stream selector if parser', async () => {
|
||||
await logContextProvider.getInitContextFilters(defaultLogRow, queryWithParser);
|
||||
expect(defaultLanguageProviderMock.fetchSeriesLabels).toBeCalled();
|
||||
expect(defaultLanguageProviderMock.fetchLabels).toBeCalledWith({ streamSelector: `{bar="baz"}` });
|
||||
});
|
||||
|
||||
it('should call fetchSeriesLabels with given time range', async () => {
|
||||
it('should call fetchLabels with given time range', async () => {
|
||||
await logContextProvider.getInitContextFilters(defaultLogRow, queryWithParser, timeRange);
|
||||
expect(defaultLanguageProviderMock.fetchSeriesLabels).toBeCalledWith(`{bar="baz"}`, { timeRange });
|
||||
expect(defaultLanguageProviderMock.fetchLabels).toBeCalledWith({ streamSelector: `{bar="baz"}`, timeRange });
|
||||
});
|
||||
|
||||
it('should call `languageProvider.start` if no parser with given time range', async () => {
|
||||
|
@ -330,11 +330,10 @@ export class LogContextProvider {
|
||||
await this.datasource.languageProvider.start(timeRange);
|
||||
allLabels = this.datasource.languageProvider.getLabelKeys();
|
||||
} else {
|
||||
// If we have parser, we use fetchSeriesLabels to fetch actual labels for selected stream
|
||||
// If we have parser, we use fetchLabels to fetch actual labels for selected stream
|
||||
const stream = getStreamSelectorsFromQuery(query.expr);
|
||||
// We are using stream[0] as log query can always have just 1 stream selector
|
||||
const series = await this.datasource.languageProvider.fetchSeriesLabels(stream[0], { timeRange });
|
||||
allLabels = Object.keys(series);
|
||||
allLabels = await this.datasource.languageProvider.fetchLabels({ streamSelector: stream[0], timeRange });
|
||||
}
|
||||
|
||||
const contextFilters: ContextFilter[] = [];
|
||||
|
@ -15,7 +15,12 @@ export function createMetadataRequest(
|
||||
const labelsMatch = url.match(lokiLabelsAndValuesEndpointRegex);
|
||||
const seriesMatch = url.match(lokiSeriesEndpointRegex);
|
||||
if (labelsMatch) {
|
||||
return labelsAndValues[labelsMatch[1]] || [];
|
||||
if (series && params && params['query']) {
|
||||
const labelAndValue = series[params['query'] as string];
|
||||
return labelAndValue.map((s) => s[labelsMatch[1]]) || [];
|
||||
} else {
|
||||
return labelsAndValues[labelsMatch[1]] || [];
|
||||
}
|
||||
} else if (seriesMatch && series && params) {
|
||||
return series[params['match[]']] || [];
|
||||
} else {
|
||||
|
@ -47,7 +47,6 @@ const otherLabels: Label[] = [
|
||||
op: '=',
|
||||
},
|
||||
];
|
||||
const seriesLabels = { place: ['series', 'labels'], source: [], other: [] };
|
||||
const parserAndLabelKeys = {
|
||||
extractedLabelKeys: ['extracted', 'label', 'keys'],
|
||||
unwrapLabelKeys: ['unwrap', 'labels'],
|
||||
@ -77,8 +76,8 @@ describe('CompletionDataProvider', () => {
|
||||
completionProvider = new CompletionDataProvider(languageProvider, historyRef, mockTimeRange);
|
||||
|
||||
jest.spyOn(languageProvider, 'getLabelKeys').mockReturnValue(labelKeys);
|
||||
jest.spyOn(languageProvider, 'fetchLabels').mockResolvedValue(labelKeys);
|
||||
jest.spyOn(languageProvider, 'fetchLabelValues').mockResolvedValue(labelValues);
|
||||
jest.spyOn(languageProvider, 'fetchSeriesLabels').mockResolvedValue(seriesLabels);
|
||||
jest.spyOn(languageProvider, 'getParserAndLabelKeys').mockResolvedValue(parserAndLabelKeys);
|
||||
});
|
||||
|
||||
@ -102,21 +101,32 @@ describe('CompletionDataProvider', () => {
|
||||
expect(completionProvider.getHistory()).toEqual(['{value="other"}']);
|
||||
});
|
||||
|
||||
test('Returns the expected label names with no other labels', async () => {
|
||||
test('Returns the expected label names', async () => {
|
||||
expect(await completionProvider.getLabelNames([])).toEqual(labelKeys);
|
||||
});
|
||||
|
||||
test('Returns the expected label names with other labels', async () => {
|
||||
expect(await completionProvider.getLabelNames(otherLabels)).toEqual(['source', 'other']);
|
||||
test('Returns the list of label names without labels used in selector', async () => {
|
||||
expect(await completionProvider.getLabelNames(otherLabels)).toEqual(['source']);
|
||||
});
|
||||
|
||||
test('Returns the expected label values with no other labels', async () => {
|
||||
test('Correctly build stream selector in getLabelNames and pass it to fetchLabels call', async () => {
|
||||
await completionProvider.getLabelNames([{ name: 'job', op: '=', value: '"a\\b\n' }]);
|
||||
expect(languageProvider.fetchLabels).toHaveBeenCalledWith({
|
||||
streamSelector: '{job="\\"a\\\\b\\n"}',
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
});
|
||||
|
||||
test('Returns the expected label values', async () => {
|
||||
expect(await completionProvider.getLabelValues('label', [])).toEqual(labelValues);
|
||||
});
|
||||
|
||||
test('Returns the expected label values with other labels', async () => {
|
||||
expect(await completionProvider.getLabelValues('place', otherLabels)).toEqual(['series', 'labels']);
|
||||
expect(await completionProvider.getLabelValues('other label', otherLabels)).toEqual([]);
|
||||
test('Correctly build stream selector in getLabelValues and pass it to fetchLabelValues call', async () => {
|
||||
await completionProvider.getLabelValues('place', [{ name: 'job', op: '=', value: '"a\\b\n' }]);
|
||||
expect(languageProvider.fetchLabelValues).toHaveBeenCalledWith('place', {
|
||||
streamSelector: '{job="\\"a\\\\b\\n"}',
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
});
|
||||
|
||||
test('Returns the expected parser and label keys', async () => {
|
||||
@ -178,15 +188,4 @@ describe('CompletionDataProvider', () => {
|
||||
completionProvider.getParserAndLabelKeys('');
|
||||
expect(languageProvider.getParserAndLabelKeys).toHaveBeenCalledWith('', { timeRange: mockTimeRange });
|
||||
});
|
||||
|
||||
test('Returns the expected series labels', async () => {
|
||||
expect(await completionProvider.getSeriesLabels([])).toEqual(seriesLabels);
|
||||
});
|
||||
|
||||
test('Escapes correct characters when building stream selector in getSeriesLabels', async () => {
|
||||
completionProvider.getSeriesLabels([{ name: 'job', op: '=', value: '"a\\b\n' }]);
|
||||
expect(languageProvider.fetchSeriesLabels).toHaveBeenCalledWith('{job="\\"a\\\\b\\n"}', {
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -40,23 +40,24 @@ export class CompletionDataProvider {
|
||||
|
||||
async getLabelNames(otherLabels: Label[] = []) {
|
||||
if (otherLabels.length === 0) {
|
||||
// if there is no filtering, we have to use a special endpoint
|
||||
// If there is no filtering, we use getLabelKeys because it has better caching
|
||||
// and all labels should already be fetched
|
||||
await this.languageProvider.start(this.timeRange);
|
||||
return this.languageProvider.getLabelKeys();
|
||||
}
|
||||
const data = await this.getSeriesLabels(otherLabels);
|
||||
const possibleLabelNames = Object.keys(data); // all names from datasource
|
||||
const possibleLabelNames = await this.languageProvider.fetchLabels({
|
||||
streamSelector: this.buildSelector(otherLabels),
|
||||
timeRange: this.timeRange,
|
||||
});
|
||||
const usedLabelNames = new Set(otherLabels.map((l) => l.name)); // names used in the query
|
||||
return possibleLabelNames.filter((label) => !usedLabelNames.has(label));
|
||||
}
|
||||
|
||||
async getLabelValues(labelName: string, otherLabels: Label[]) {
|
||||
if (otherLabels.length === 0) {
|
||||
// if there is no filtering, we have to use a special endpoint
|
||||
return await this.languageProvider.fetchLabelValues(labelName, { timeRange: this.timeRange });
|
||||
}
|
||||
|
||||
const data = await this.getSeriesLabels(otherLabels);
|
||||
return data[labelName] ?? [];
|
||||
return await this.languageProvider.fetchLabelValues(labelName, {
|
||||
streamSelector: this.buildSelector(otherLabels),
|
||||
timeRange: this.timeRange,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,10 +90,4 @@ export class CompletionDataProvider {
|
||||
return labelKeys;
|
||||
}
|
||||
}
|
||||
|
||||
async getSeriesLabels(labels: Label[]) {
|
||||
return await this.languageProvider
|
||||
.fetchSeriesLabels(this.buildSelector(labels), { timeRange: this.timeRange })
|
||||
.then((data) => data ?? {});
|
||||
}
|
||||
}
|
||||
|
@ -490,9 +490,9 @@ describe('LokiDatasource', () => {
|
||||
return { ds };
|
||||
};
|
||||
|
||||
it('should return empty array if /series returns empty', async () => {
|
||||
it('should return empty array if label values returns empty', async () => {
|
||||
const ds = createLokiDatasource(templateSrvStub);
|
||||
const spy = jest.spyOn(ds.languageProvider, 'fetchSeriesLabels').mockResolvedValue({});
|
||||
const spy = jest.spyOn(ds.languageProvider, 'fetchLabelValues').mockResolvedValue([]);
|
||||
|
||||
const result = await ds.metricFindQuery({
|
||||
refId: 'test',
|
||||
|
@ -680,16 +680,10 @@ export class LokiDatasource
|
||||
return [];
|
||||
}
|
||||
|
||||
// If we have stream selector, use /series endpoint
|
||||
if (query.stream) {
|
||||
const result = await this.languageProvider.fetchSeriesLabels(query.stream, { timeRange });
|
||||
if (!result[query.label]) {
|
||||
return [];
|
||||
}
|
||||
return result[query.label].map((value: string) => ({ text: value }));
|
||||
}
|
||||
|
||||
const result = await this.languageProvider.fetchLabelValues(query.label, { timeRange });
|
||||
const result = await this.languageProvider.fetchLabelValues(query.label, {
|
||||
streamSelector: query.stream,
|
||||
timeRange,
|
||||
});
|
||||
return result.map((value: string) => ({ text: value }));
|
||||
}
|
||||
|
||||
|
@ -25,16 +25,18 @@ We strongly advise using these recommended methods instead of direct API calls b
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Fetch all label keys
|
||||
* This asynchronous function is designed to retrieve all available label keys from the data source.
|
||||
* Fetch label keys using the best applicable endpoint.
|
||||
*
|
||||
* This asynchronous function returns all available label keys from the data source.
|
||||
* It returns a promise that resolves to an array of strings containing the label keys.
|
||||
*
|
||||
* @param options - (Optional) An object containing additional options - currently only time range.
|
||||
* @param options - (Optional) An object containing additional options.
|
||||
* @param options.streamSelector - (Optional) The stream selector to filter label keys. If not provided, all label keys are fetched.
|
||||
* @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used.
|
||||
* @returns A promise containing an array of label keys.
|
||||
* @throws An error if the fetch operation fails.
|
||||
*/
|
||||
async function fetchLabels(options?: { timeRange?: TimeRange }): Promise<string[]>;
|
||||
async function fetchLabels(options?: { streamSelector?: string; timeRange?: TimeRange }): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Example usage:
|
||||
|
@ -0,0 +1,71 @@
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React, { ComponentProps } from 'react';
|
||||
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { QueryBuilderOperation, QueryBuilderOperationParamDef } from '@grafana/experimental';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { createLokiDatasource } from '../../__mocks__/datasource';
|
||||
import { LokiDatasource } from '../../datasource';
|
||||
import { LokiQueryModeller } from '../LokiQueryModeller';
|
||||
import { LokiOperationId } from '../types';
|
||||
|
||||
import { LabelParamEditor } from './LabelParamEditor';
|
||||
|
||||
describe('LabelParamEditor', () => {
|
||||
const queryHintsFeatureToggle = config.featureToggles.lokiQueryHints;
|
||||
beforeAll(() => {
|
||||
config.featureToggles.lokiQueryHints = true;
|
||||
});
|
||||
afterAll(() => {
|
||||
config.featureToggles.lokiQueryHints = queryHintsFeatureToggle;
|
||||
});
|
||||
|
||||
it('shows label options', async () => {
|
||||
const props = createProps({}, ['label1', 'label2']);
|
||||
render(<LabelParamEditor {...props} />);
|
||||
const input = screen.getByRole('combobox');
|
||||
await userEvent.click(input);
|
||||
expect(screen.getByText('label1')).toBeInTheDocument();
|
||||
expect(screen.getByText('label2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no label options if no samples are returned', async () => {
|
||||
const props = createProps();
|
||||
render(<LabelParamEditor {...props} />);
|
||||
const input = screen.getByRole('combobox');
|
||||
await userEvent.click(input);
|
||||
expect(screen.getByText('No labels found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const createProps = (propsOverrides?: Partial<ComponentProps<typeof LabelParamEditor>>, mockedSample?: string[]) => {
|
||||
const propsDefault = {
|
||||
value: undefined,
|
||||
onChange: jest.fn(),
|
||||
onRunQuery: jest.fn(),
|
||||
index: 1,
|
||||
operationId: '1',
|
||||
query: {
|
||||
labels: [{ op: '=', label: 'foo', value: 'bar' }],
|
||||
operations: [
|
||||
{ id: LokiOperationId.CountOverTime, params: ['5m'] },
|
||||
{ id: '__sum_by', params: ['job'] },
|
||||
],
|
||||
},
|
||||
paramDef: {} as QueryBuilderOperationParamDef,
|
||||
operation: {} as QueryBuilderOperation,
|
||||
datasource: createLokiDatasource() as DataSourceApi,
|
||||
queryModeller: {
|
||||
renderLabels: jest.fn().mockReturnValue('sum by(job) (count_over_time({foo="bar"} [5m]))'),
|
||||
} as unknown as LokiQueryModeller,
|
||||
};
|
||||
const props = { ...propsDefault, ...propsOverrides };
|
||||
|
||||
if (props.datasource instanceof LokiDatasource) {
|
||||
const resolvedValue = mockedSample ?? [];
|
||||
props.datasource.languageProvider.fetchLabels = jest.fn().mockResolvedValue(resolvedValue);
|
||||
}
|
||||
return props;
|
||||
};
|
@ -55,9 +55,9 @@ async function loadGroupByLabels(
|
||||
let labels: QueryBuilderLabelFilter[] = query.labels;
|
||||
|
||||
const queryString = queryModeller.renderLabels(labels);
|
||||
const result = await datasource.languageProvider.fetchSeriesLabels(queryString);
|
||||
const result: string[] = await datasource.languageProvider.fetchLabels({ streamSelector: queryString });
|
||||
|
||||
return Object.keys(result).map((x) => ({
|
||||
return result.map((x) => ({
|
||||
label: x,
|
||||
value: x,
|
||||
}));
|
||||
|
@ -49,26 +49,26 @@ describe('LokiQueryBuilder', () => {
|
||||
afterEach(() => {
|
||||
config.featureToggles.lokiQueryHints = originalLokiQueryHints;
|
||||
});
|
||||
it('tries to load labels when no labels are selected', async () => {
|
||||
it('tries to load label names', async () => {
|
||||
const props = createDefaultProps();
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
props.datasource.languageProvider.fetchSeriesLabels = jest.fn().mockReturnValue({ job: ['a'], instance: ['b'] });
|
||||
props.datasource.languageProvider.fetchLabels = jest.fn().mockReturnValue(['job', 'instance']);
|
||||
|
||||
render(<LokiQueryBuilder {...props} query={defaultQuery} />);
|
||||
await userEvent.click(screen.getByLabelText('Add'));
|
||||
const labels = screen.getByText(/Label filters/);
|
||||
const selects = getAllByRole(getSelectParent(labels)!, 'combobox');
|
||||
await userEvent.click(selects[3]);
|
||||
expect(props.datasource.languageProvider.fetchSeriesLabels).toHaveBeenCalledWith('{baz="bar"}', {
|
||||
expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledWith({
|
||||
streamSelector: '{baz="bar"}',
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
await waitFor(() => expect(screen.getByText('job')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('uses fetchLabelValues preselected labels have no equality matcher', async () => {
|
||||
it('uses fetchLabelValues if preselected labels have no equality matcher', async () => {
|
||||
const props = createDefaultProps();
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
props.datasource.languageProvider.fetchSeriesLabels = jest.fn();
|
||||
props.datasource.languageProvider.fetchLabelValues = jest.fn().mockReturnValue(['a', 'b']);
|
||||
|
||||
const query: LokiVisualQuery = {
|
||||
@ -85,13 +85,11 @@ describe('LokiQueryBuilder', () => {
|
||||
expect(props.datasource.languageProvider.fetchLabelValues).toHaveBeenCalledWith('job', {
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
expect(props.datasource.languageProvider.fetchSeriesLabels).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('uses fetchLabelValues preselected label have regex equality matcher with match everything value (.*)', async () => {
|
||||
it('no streamSelector in fetchLabelValues if preselected label have regex equality matcher with match everything value (.*)', async () => {
|
||||
const props = createDefaultProps();
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
props.datasource.languageProvider.fetchSeriesLabels = jest.fn();
|
||||
props.datasource.languageProvider.fetchLabelValues = jest.fn().mockReturnValue(['a', 'b']);
|
||||
|
||||
const query: LokiVisualQuery = {
|
||||
@ -108,13 +106,11 @@ describe('LokiQueryBuilder', () => {
|
||||
expect(props.datasource.languageProvider.fetchLabelValues).toHaveBeenCalledWith('job', {
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
expect(props.datasource.languageProvider.fetchSeriesLabels).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('uses fetchLabels preselected label have regex equality matcher with match everything value (.*)', async () => {
|
||||
it('no streamSelector in fetchLabels if preselected label have regex equality matcher with match everything value (.*)', async () => {
|
||||
const props = createDefaultProps();
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
props.datasource.languageProvider.fetchSeriesLabels = jest.fn();
|
||||
props.datasource.languageProvider.fetchLabels = jest.fn().mockReturnValue(['a', 'b']);
|
||||
|
||||
const query: LokiVisualQuery = {
|
||||
@ -129,14 +125,12 @@ describe('LokiQueryBuilder', () => {
|
||||
const selects = getAllByRole(getSelectParent(labels)!, 'combobox');
|
||||
await userEvent.click(selects[3]);
|
||||
expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledWith({ timeRange: mockTimeRange });
|
||||
expect(props.datasource.languageProvider.fetchSeriesLabels).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('uses fetchSeriesLabels preselected label have regex equality matcher', async () => {
|
||||
it('uses streamSelector in fetchLabelValues if preselected label have regex equality matcher', async () => {
|
||||
const props = createDefaultProps();
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
props.datasource.languageProvider.fetchSeriesLabels = jest.fn().mockReturnValue({ job: ['a'], instance: ['b'] });
|
||||
props.datasource.languageProvider.fetchLabelValues = jest.fn();
|
||||
props.datasource.languageProvider.fetchLabelValues = jest.fn().mockReturnValue(['a', 'b']);
|
||||
|
||||
const query: LokiVisualQuery = {
|
||||
labels: [
|
||||
@ -149,18 +143,17 @@ describe('LokiQueryBuilder', () => {
|
||||
const labels = screen.getByText(/Label filters/);
|
||||
const selects = getAllByRole(getSelectParent(labels)!, 'combobox');
|
||||
await userEvent.click(selects[5]);
|
||||
expect(props.datasource.languageProvider.fetchSeriesLabels).toHaveBeenCalledWith('{cluster=~"cluster1|cluster2"}', {
|
||||
expect(props.datasource.languageProvider.fetchLabelValues).toHaveBeenCalledWith('job', {
|
||||
streamSelector: '{cluster=~"cluster1|cluster2"}',
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
expect(props.datasource.languageProvider.fetchLabelValues).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does refetch label values with the correct time range', async () => {
|
||||
const props = createDefaultProps();
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
props.datasource.languageProvider.fetchSeriesLabels = jest
|
||||
.fn()
|
||||
.mockReturnValue({ job: ['a'], instance: ['b'], baz: ['bar'] });
|
||||
props.datasource.languageProvider.fetchLabels = jest.fn().mockReturnValue(['job', 'instance', 'baz']);
|
||||
props.datasource.languageProvider.fetchLabelValues = jest.fn().mockReturnValue(['a', 'b', 'c']);
|
||||
|
||||
render(<LokiQueryBuilder {...props} query={defaultQuery} />);
|
||||
await userEvent.click(screen.getByLabelText('Add'));
|
||||
@ -170,7 +163,12 @@ describe('LokiQueryBuilder', () => {
|
||||
await waitFor(() => expect(screen.getByText('job')).toBeInTheDocument());
|
||||
await userEvent.click(screen.getByText('job'));
|
||||
await userEvent.click(selects[5]);
|
||||
expect(props.datasource.languageProvider.fetchSeriesLabels).toHaveBeenNthCalledWith(2, '{baz="bar"}', {
|
||||
expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledWith({
|
||||
streamSelector: '{baz="bar"}',
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
expect(props.datasource.languageProvider.fetchLabelValues).toHaveBeenCalledWith('job', {
|
||||
streamSelector: '{baz="bar"}',
|
||||
timeRange: mockTimeRange,
|
||||
});
|
||||
});
|
||||
@ -178,9 +176,7 @@ describe('LokiQueryBuilder', () => {
|
||||
it('does not show already existing label names as option in label filter', async () => {
|
||||
const props = createDefaultProps();
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
props.datasource.languageProvider.fetchSeriesLabels = jest
|
||||
.fn()
|
||||
.mockReturnValue({ job: ['a'], instance: ['b'], baz: ['bar'] });
|
||||
props.datasource.languageProvider.fetchLabels = jest.fn().mockReturnValue(['job', 'instance', 'baz']);
|
||||
|
||||
render(<LokiQueryBuilder {...props} query={defaultQuery} />);
|
||||
await userEvent.click(screen.getByLabelText('Add'));
|
||||
|
@ -65,16 +65,15 @@ export const LokiQueryBuilder = React.memo<Props>(
|
||||
return await datasource.languageProvider.fetchLabels({ timeRange });
|
||||
}
|
||||
|
||||
const expr = lokiQueryModeller.renderLabels(labelsToConsider);
|
||||
const series = await datasource.languageProvider.fetchSeriesLabels(expr, { timeRange });
|
||||
const streamSelector = lokiQueryModeller.renderLabels(labelsToConsider);
|
||||
const possibleLabelNames = await datasource.languageProvider.fetchLabels({
|
||||
streamSelector,
|
||||
timeRange,
|
||||
});
|
||||
const labelsNamesToConsider = labelsToConsider.map((l) => l.label);
|
||||
|
||||
const labelNames = Object.keys(series)
|
||||
// Filter out label names that are already selected
|
||||
.filter((name) => !labelsNamesToConsider.includes(name))
|
||||
.sort();
|
||||
|
||||
return labelNames;
|
||||
// Filter out label names that are already selected
|
||||
return possibleLabelNames.filter((label) => !labelsNamesToConsider.includes(label)).sort();
|
||||
};
|
||||
|
||||
const onGetLabelValues = async (forLabel: Partial<QueryBuilderLabelFilter>) => {
|
||||
@ -91,9 +90,11 @@ export const LokiQueryBuilder = React.memo<Props>(
|
||||
if (labelsToConsider.length === 0 || !hasEqualityOperation) {
|
||||
values = await datasource.languageProvider.fetchLabelValues(forLabel.label, { timeRange });
|
||||
} else {
|
||||
const expr = lokiQueryModeller.renderLabels(labelsToConsider);
|
||||
const result = await datasource.languageProvider.fetchSeriesLabels(expr, { timeRange });
|
||||
values = result[datasource.interpolateString(forLabel.label)];
|
||||
const streamSelector = lokiQueryModeller.renderLabels(labelsToConsider);
|
||||
values = await datasource.languageProvider.fetchLabelValues(forLabel.label, {
|
||||
streamSelector,
|
||||
timeRange,
|
||||
});
|
||||
}
|
||||
|
||||
return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return
|
||||
|
@ -42,7 +42,8 @@ describe('LokiQueryBuilderContainer', () => {
|
||||
showExplain: false,
|
||||
};
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
props.datasource.languageProvider.fetchSeriesLabels = jest.fn().mockReturnValue({ job: ['grafana', 'loki'] });
|
||||
props.datasource.languageProvider.fetchLabels = jest.fn().mockReturnValue(['job']);
|
||||
props.datasource.languageProvider.fetchLabelValues = jest.fn().mockReturnValue(['grafana', 'loki']);
|
||||
props.onChange = jest.fn();
|
||||
|
||||
render(<LokiQueryBuilderContainer {...props} />);
|
||||
|
@ -8,7 +8,7 @@ import { EXPLAIN_LABEL_FILTER_CONTENT } from './LokiQueryBuilderExplained';
|
||||
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
|
||||
|
||||
const defaultQuery: LokiQuery = {
|
||||
expr: '{job="bar}',
|
||||
expr: '{job="bar"}',
|
||||
refId: 'A',
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user