diff --git a/public/app/plugins/datasource/loki/LanguageProvider.test.ts b/public/app/plugins/datasource/loki/LanguageProvider.test.ts index 7702a424ee5..5a63f43c2e2 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.test.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.test.ts @@ -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({}); diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index c63d7ef58a0..b833308422d 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -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 { + // 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 { + private async fetchLabelsByLabelsEndpoint(options?: { timeRange?: TimeRange }): Promise { 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 { 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(); diff --git a/public/app/plugins/datasource/loki/LogContextProvider.test.ts b/public/app/plugins/datasource/loki/LogContextProvider.test.ts index 7d6216a8b10..b734e0106ad 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.test.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.test.ts @@ -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 () => { diff --git a/public/app/plugins/datasource/loki/LogContextProvider.ts b/public/app/plugins/datasource/loki/LogContextProvider.ts index ada99fad88c..337e17dd631 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.ts @@ -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[] = []; diff --git a/public/app/plugins/datasource/loki/__mocks__/metadataRequest.ts b/public/app/plugins/datasource/loki/__mocks__/metadataRequest.ts index 94e5fedcb07..da7c292df20 100644 --- a/public/app/plugins/datasource/loki/__mocks__/metadataRequest.ts +++ b/public/app/plugins/datasource/loki/__mocks__/metadataRequest.ts @@ -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 { diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts index 5b2d1a62eb0..e67b3e779bc 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts @@ -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, - }); - }); }); diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts index f982d524855..53e718bc0df 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts @@ -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 ?? {}); - } } diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 2066bece06a..273622cc082 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -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', diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 65713bfa887..cb7fefab8c1 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -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 })); } diff --git a/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md b/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md index 2d50b981916..5a1f1b685e7 100644 --- a/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md +++ b/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md @@ -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; +async function fetchLabels(options?: { streamSelector?: string; timeRange?: TimeRange }): Promise; /** * Example usage: diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.test.tsx new file mode 100644 index 00000000000..d6745f0ba89 --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.test.tsx @@ -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(); + 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(); + const input = screen.getByRole('combobox'); + await userEvent.click(input); + expect(screen.getByText('No labels found')).toBeInTheDocument(); + }); +}); + +const createProps = (propsOverrides?: Partial>, 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; +}; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.tsx index ea9186ebdd7..b92c3bec3b2 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.tsx @@ -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, })); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx index 6bc26bd8da4..d7a5b3318b3 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx @@ -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(); 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(); 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(); await userEvent.click(screen.getByLabelText('Add')); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index f112874988d..1e0fee2efed 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -65,16 +65,15 @@ export const LokiQueryBuilder = React.memo( 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) => { @@ -91,9 +90,11 @@ export const LokiQueryBuilder = React.memo( 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 diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx index 8e110352ff7..783155fdab8 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx @@ -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(); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.test.tsx index 2277f31a640..19ea23c2bc5 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.test.tsx @@ -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', };