Loki: Remove relying on timeSrv.timeRange in LanguageProvider (#78450)

* Loki: Allow setting of timeRange when using languageProvider functions

* Loki: Use timerange where available for start

* Loki: Use timerange where available for fetchLabels

* Loki: Use timerange where available for fetchSeriesLabels

* Loki: Use timerange where available for fetchLabelValues

* Loki: Use timerange where available for getParserAndLabelKeys

* Loki: Update and add tests for fetchLabels

* Loki: Update and add tests for fetchSeriesLabels

* Loki: Update and add tests for fetchSeries

* Loki: Update and add tests for fetchLabelValues

* Loki: Update and add tests for fetchLabelValues

* Loki: Update and add tests for getParserAndLabelKeys

* Update public/app/plugins/datasource/loki/LanguageProvider.test.ts

Co-authored-by: Matias Chomicki <matyax@gmail.com>

* Update public/app/plugins/datasource/loki/LanguageProvider.test.ts

Co-authored-by: Matias Chomicki <matyax@gmail.com>

* Not needing to use languageProvider.getDefaultTime in Monaco

* Update comment

* Update getDefaultTimeRange to be ptivate

---------

Co-authored-by: Matias Chomicki <matyax@gmail.com>
This commit is contained in:
Ivana Huckova
2023-11-22 14:35:15 +01:00
committed by GitHub
parent 0de66a8099
commit 4fd1d92332
18 changed files with 470 additions and 225 deletions

View File

@@ -243,8 +243,8 @@ lineage: schemas: [{
// `4`: Numerical DESC // `4`: Numerical DESC
// `5`: Alphabetical Case Insensitive ASC // `5`: Alphabetical Case Insensitive ASC
// `6`: Alphabetical Case Insensitive DESC // `6`: Alphabetical Case Insensitive DESC
// `7`: Natural ASC // `7`: Natural ASC
// `8`: Natural DESC // `8`: Natural DESC
#VariableSort: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 @cuetsy(kind="enum",memberNames="disabled|alphabeticalAsc|alphabeticalDesc|numericalAsc|numericalDesc|alphabeticalCaseInsensitiveAsc|alphabeticalCaseInsensitiveDesc|naturalAsc|naturalDesc") #VariableSort: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 @cuetsy(kind="enum",memberNames="disabled|alphabeticalAsc|alphabeticalDesc|numericalAsc|numericalDesc|alphabeticalCaseInsensitiveAsc|alphabeticalCaseInsensitiveDesc|naturalAsc|naturalDesc")
// Ref to a DataSource instance // Ref to a DataSource instance

View File

@@ -1,4 +1,4 @@
import { AbstractLabelOperator, DataFrame } from '@grafana/data'; import { AbstractLabelOperator, DataFrame, TimeRange, dateTime, getDefaultTimeRange } from '@grafana/data';
import LanguageProvider from './LanguageProvider'; import LanguageProvider from './LanguageProvider';
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource'; import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
@@ -24,6 +24,26 @@ jest.mock('app/store/store', () => ({
}, },
})); }));
const mockTimeRange = {
from: dateTime(1546372800000),
to: dateTime(1546380000000),
raw: {
from: dateTime(1546372800000),
to: dateTime(1546380000000),
},
};
jest.mock('@grafana/data', () => ({
...jest.requireActual('@grafana/data'),
getDefaultTimeRange: jest.fn().mockReturnValue({
from: 0,
to: 1,
raw: {
from: 0,
to: 1,
},
}),
}));
describe('Language completion provider', () => { describe('Language completion provider', () => {
describe('fetchSeries', () => { describe('fetchSeries', () => {
it('should use match[] parameter', () => { it('should use match[] parameter', () => {
@@ -38,6 +58,25 @@ describe('Language completion provider', () => {
start: 1560153109000, start: 1560153109000,
}); });
}); });
it('should use provided time range', () => {
const datasource = setup({});
datasource.getTimeRangeParams = jest
.fn()
.mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() }));
const languageProvider = new LanguageProvider(datasource);
languageProvider.request = jest.fn();
languageProvider.fetchSeries('{job="grafana"}', { timeRange: mockTimeRange });
// time range was passed to getTimeRangeParams
expect(datasource.getTimeRangeParams).toHaveBeenCalledWith(mockTimeRange);
// time range was passed to request
expect(languageProvider.request).toHaveBeenCalled();
expect(languageProvider.request).toHaveBeenCalledWith('series', {
end: 1546380000000,
'match[]': '{job="grafana"}',
start: 1546372800000,
});
});
}); });
describe('fetchSeriesLabels', () => { describe('fetchSeriesLabels', () => {
@@ -59,6 +98,26 @@ describe('Language completion provider', () => {
start: 0, start: 0,
}); });
}); });
it('should be called with time range params if provided', () => {
const datasource = setup({});
datasource.getTimeRangeParams = jest
.fn()
.mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() }));
const languageProvider = new LanguageProvider(datasource);
languageProvider.request = jest.fn().mockResolvedValue([]);
languageProvider.fetchSeriesLabels('stream', { timeRange: mockTimeRange });
// time range was passed to getTimeRangeParams
expect(datasource.getTimeRangeParams).toHaveBeenCalled();
expect(datasource.getTimeRangeParams).toHaveBeenCalledWith(mockTimeRange);
// time range was passed to request
expect(languageProvider.request).toHaveBeenCalled();
expect(languageProvider.request).toHaveBeenCalledWith('series', {
end: 1546380000000,
'match[]': 'stream',
start: 1546372800000,
});
});
}); });
describe('label values', () => { describe('label values', () => {
@@ -87,6 +146,41 @@ describe('Language completion provider', () => {
expect(labelValues).toEqual(['label1_val1', 'label1_val2']); expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
}); });
it('fetch label with options.timeRange when provided and values is not cached', async () => {
const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
datasource.getTimeRangeParams = jest
.fn()
.mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() }));
const languageProvider = new LanguageProvider(datasource);
languageProvider.request = jest.fn().mockResolvedValue([]);
languageProvider.fetchLabelValues('testKey', { timeRange: mockTimeRange });
// time range was passed to getTimeRangeParams
expect(datasource.getTimeRangeParams).toHaveBeenCalled();
expect(datasource.getTimeRangeParams).toHaveBeenCalledWith(mockTimeRange);
// time range was passed to request
expect(languageProvider.request).toHaveBeenCalled();
expect(languageProvider.request).toHaveBeenCalledWith('label/testKey/values', {
end: 1546380000000,
start: 1546372800000,
});
});
it('uses default time range if fetch label does not receive options.timeRange', async () => {
const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
datasource.getTimeRangeParams = jest
.fn()
.mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() }));
const languageProvider = new LanguageProvider(datasource);
languageProvider.request = jest.fn().mockResolvedValue([]);
languageProvider.fetchLabelValues('testKey');
expect(getDefaultTimeRange).toHaveBeenCalled();
expect(languageProvider.request).toHaveBeenCalled();
expect(languageProvider.request).toHaveBeenCalledWith('label/testKey/values', {
end: 1,
start: 0,
});
});
it('should return cached values', async () => { it('should return cached values', async () => {
const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] }); const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
const provider = await getLanguageProvider(datasource); const provider = await getLanguageProvider(datasource);
@@ -149,7 +243,7 @@ describe('Language completion provider', () => {
describe('Request URL', () => { describe('Request URL', () => {
it('should contain range params', async () => { it('should contain range params', async () => {
const datasourceWithLabels = setup({ other: [] }); const datasourceWithLabels = setup({ other: [] });
const rangeParams = datasourceWithLabels.getTimeRangeParams(); const rangeParams = datasourceWithLabels.getTimeRangeParams(mockTimeRange);
const datasourceSpy = jest.spyOn(datasourceWithLabels, 'metadataRequest'); const datasourceSpy = jest.spyOn(datasourceWithLabels, 'metadataRequest');
const instance = new LanguageProvider(datasourceWithLabels); const instance = new LanguageProvider(datasourceWithLabels);
@@ -191,6 +285,16 @@ describe('fetchLabels', () => {
await instance.fetchLabels(); await instance.fetchLabels();
expect(instance.labelKeys).toEqual([]); 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', () => { describe('Query imports', () => {
@@ -286,11 +390,14 @@ describe('Query imports', () => {
hasLogfmt: false, hasLogfmt: false,
hasPack: false, hasPack: false,
}); });
expect(datasource.getDataSamples).toHaveBeenCalledWith({ expect(datasource.getDataSamples).toHaveBeenCalledWith(
expr: '{place="luna"}', {
maxLines: DEFAULT_MAX_LINES_SAMPLE, expr: '{place="luna"}',
refId: 'data-samples', maxLines: DEFAULT_MAX_LINES_SAMPLE,
}); refId: 'data-samples',
},
undefined
);
}); });
it('calls dataSample with correctly set sampleSize', async () => { it('calls dataSample with correctly set sampleSize', async () => {
@@ -303,11 +410,27 @@ describe('Query imports', () => {
hasLogfmt: false, hasLogfmt: false,
hasPack: false, hasPack: false,
}); });
expect(datasource.getDataSamples).toHaveBeenCalledWith({ expect(datasource.getDataSamples).toHaveBeenCalledWith(
expr: '{place="luna"}', {
maxLines: 5, expr: '{place="luna"}',
refId: 'data-samples', maxLines: 5,
}); refId: 'data-samples',
},
undefined
);
});
it('calls dataSample with correctly set time range', async () => {
jest.spyOn(datasource, 'getDataSamples').mockResolvedValue([]);
languageProvider.getParserAndLabelKeys('{place="luna"}', { timeRange: mockTimeRange });
expect(datasource.getDataSamples).toHaveBeenCalledWith(
{
expr: '{place="luna"}',
maxLines: 10,
refId: 'data-samples',
},
mockTimeRange
);
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { LRUCache } from 'lru-cache'; import { LRUCache } from 'lru-cache';
import Prism from 'prismjs'; import Prism from 'prismjs';
import { LanguageProvider, AbstractQuery, KeyValue } from '@grafana/data'; import { LanguageProvider, AbstractQuery, KeyValue, getDefaultTimeRange, TimeRange } from '@grafana/data';
import { extractLabelMatchers, processLabels, toPromLikeExpr } from 'app/plugins/datasource/prometheus/language_utils'; import { extractLabelMatchers, processLabels, toPromLikeExpr } from 'app/plugins/datasource/prometheus/language_utils';
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource'; import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
@@ -50,9 +50,10 @@ export default class LokiLanguageProvider extends LanguageProvider {
/** /**
* Initialize the language provider by fetching set of labels. * Initialize the language provider by fetching set of labels.
*/ */
start = () => { start = (timeRange?: TimeRange) => {
const range = timeRange ?? this.getDefaultTimeRange();
if (!this.startTask) { if (!this.startTask) {
this.startTask = this.fetchLabels().then(() => { this.startTask = this.fetchLabels({ timeRange: range }).then(() => {
this.started = true; this.started = true;
return []; return [];
}); });
@@ -101,12 +102,15 @@ export default class LokiLanguageProvider extends LanguageProvider {
* This asynchronous function returns all available label keys from the data source. * 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. * 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.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. * @returns A promise containing an array of label keys.
* @throws An error if the fetch operation fails. * @throws An error if the fetch operation fails.
*/ */
async fetchLabels(): Promise<string[]> { async fetchLabels(options?: { timeRange?: TimeRange }): Promise<string[]> {
const url = 'labels'; const url = 'labels';
const timeRange = this.datasource.getTimeRangeParams(); const range = options?.timeRange ?? this.getDefaultTimeRange();
const timeRange = this.datasource.getTimeRangeParams(range);
const res = await this.request(url, timeRange); const res = await this.request(url, timeRange);
if (Array.isArray(res)) { if (Array.isArray(res)) {
@@ -128,13 +132,19 @@ export default class LokiLanguageProvider extends LanguageProvider {
* It returns a promise that resolves to a record mapping label names to their corresponding values. * It returns a promise that resolves to a record mapping label names to their corresponding values.
* *
* @param streamSelector - The stream selector for which you want to retrieve labels. * @param streamSelector - The stream selector for which you want to retrieve labels.
* @param options - (Optional) An object containing additional options - currently only time range.
* @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 a record of label names and their values. * @returns A promise containing a record of label names and their values.
* @throws An error if the fetch operation fails. * @throws An error if the fetch operation fails.
*/ */
fetchSeriesLabels = async (streamSelector: string): Promise<Record<string, string[]>> => { fetchSeriesLabels = async (
streamSelector: string,
options?: { timeRange?: TimeRange }
): Promise<Record<string, string[]>> => {
const interpolatedMatch = this.datasource.interpolateString(streamSelector); const interpolatedMatch = this.datasource.interpolateString(streamSelector);
const url = 'series'; const url = 'series';
const { start, end } = this.datasource.getTimeRangeParams(); const range = options?.timeRange ?? this.getDefaultTimeRange();
const { start, end } = this.datasource.getTimeRangeParams(range);
const cacheKey = this.generateCacheKey(url, start, end, interpolatedMatch); const cacheKey = this.generateCacheKey(url, start, end, interpolatedMatch);
let value = this.seriesCache.get(cacheKey); let value = this.seriesCache.get(cacheKey);
@@ -151,10 +161,15 @@ export default class LokiLanguageProvider extends LanguageProvider {
/** /**
* Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels. * Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels.
* @param match * @param match
* @param streamSelector - The stream selector for which you want to retrieve labels.
* @param options - (Optional) An object containing additional options.
* @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 array with records of label names and their value.
*/ */
fetchSeries = async (match: string): Promise<Array<Record<string, string>>> => { fetchSeries = async (match: string, options?: { timeRange?: TimeRange }): Promise<Array<Record<string, string>>> => {
const url = 'series'; const url = 'series';
const { start, end } = this.datasource.getTimeRangeParams(); const range = options?.timeRange ?? this.getDefaultTimeRange();
const { start, end } = this.datasource.getTimeRangeParams(range);
const params = { 'match[]': match, start, end }; const params = { 'match[]': match, start, end };
return await this.request(url, params); return await this.request(url, params);
}; };
@@ -179,19 +194,24 @@ export default class LokiLanguageProvider extends LanguageProvider {
* It returns a promise that resolves to an array of strings containing the label values. * It returns a promise that resolves to an array of strings containing the label values.
* *
* @param labelName - The name of the label for which you want to retrieve values. * @param labelName - The name of the label for which you want to retrieve values.
* @param options - (Optional) An object containing additional options - currently only stream selector. * @param options - (Optional) An object containing additional options.
* @param options.streamSelector - (Optional) The stream selector to filter label values. If not provided, all label values are fetched. * @param options.streamSelector - (Optional) The stream selector to filter label values. If not provided, all label values are fetched.
* @param options.timeRange - (Optional) The time range for which you want to retrieve label values. If not provided, the default time range is used.
* @returns A promise containing an array of label values. * @returns A promise containing an array of label values.
* @throws An error if the fetch operation fails. * @throws An error if the fetch operation fails.
*/ */
async fetchLabelValues(labelName: string, options?: { streamSelector?: string }): Promise<string[]> { async fetchLabelValues(
labelName: string,
options?: { streamSelector?: string; timeRange?: TimeRange }
): Promise<string[]> {
const label = encodeURIComponent(this.datasource.interpolateString(labelName)); const label = encodeURIComponent(this.datasource.interpolateString(labelName));
const streamParam = options?.streamSelector const streamParam = options?.streamSelector
? encodeURIComponent(this.datasource.interpolateString(options.streamSelector)) ? encodeURIComponent(this.datasource.interpolateString(options.streamSelector))
: undefined; : undefined;
const url = `label/${label}/values`; const url = `label/${label}/values`;
const rangeParams = this.datasource.getTimeRangeParams(); const range = options?.timeRange ?? this.getDefaultTimeRange();
const rangeParams = this.datasource.getTimeRangeParams(range);
const { start, end } = rangeParams; const { start, end } = rangeParams;
const params: KeyValue<string | number> = { start, end }; const params: KeyValue<string | number> = { start, end };
let paramCacheKey = label; let paramCacheKey = label;
@@ -230,21 +250,25 @@ export default class LokiLanguageProvider extends LanguageProvider {
* - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data. * - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data.
* *
* @param streamSelector - The selector for the log stream you want to analyze. * @param streamSelector - The selector for the log stream you want to analyze.
* @param {Object} [options] - Optional parameters. * @param options - (Optional) An object containing additional options.
* @param {number} [options.maxLines] - The number of log lines requested when determining parsers and label keys. * @param options.maxLines - (Optional) The number of log lines requested when determining parsers and label keys.
* @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used.
* Smaller maxLines is recommended for improved query performance. The default count is 10. * Smaller maxLines is recommended for improved query performance. The default count is 10.
* @returns A promise containing an object with parser and label key information. * @returns A promise containing an object with parser and label key information.
* @throws An error if the fetch operation fails. * @throws An error if the fetch operation fails.
*/ */
async getParserAndLabelKeys( async getParserAndLabelKeys(
streamSelector: string, streamSelector: string,
options?: { maxLines?: number } options?: { maxLines?: number; timeRange?: TimeRange }
): Promise<ParserAndLabelKeysResult> { ): Promise<ParserAndLabelKeysResult> {
const series = await this.datasource.getDataSamples({ const series = await this.datasource.getDataSamples(
expr: streamSelector, {
refId: 'data-samples', expr: streamSelector,
maxLines: options?.maxLines || DEFAULT_MAX_LINES_SAMPLE, refId: 'data-samples',
}); maxLines: options?.maxLines || DEFAULT_MAX_LINES_SAMPLE,
},
options?.timeRange
);
if (!series.length) { if (!series.length) {
return { extractedLabelKeys: [], unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false, hasPack: false }; return { extractedLabelKeys: [], unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false, hasPack: false };
@@ -260,4 +284,13 @@ export default class LokiLanguageProvider extends LanguageProvider {
hasLogfmt, hasLogfmt,
}; };
} }
/**
* Get the default time range
*
* @returns {TimeRange} The default time range
*/
private getDefaultTimeRange(): TimeRange {
return getDefaultTimeRange();
}
} }

View File

@@ -3,7 +3,7 @@ import { sortBy } from 'lodash';
import React, { ChangeEvent } from 'react'; import React, { ChangeEvent } from 'react';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { CoreApp, GrafanaTheme2 } from '@grafana/data'; import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { import {
Button, Button,
@@ -17,7 +17,6 @@ import {
fuzzyMatch, fuzzyMatch,
} from '@grafana/ui'; } from '@grafana/ui';
import PromQlLanguageProvider from '../../prometheus/language_provider';
import LokiLanguageProvider from '../LanguageProvider'; import LokiLanguageProvider from '../LanguageProvider';
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../languageUtils'; import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../languageUtils';
@@ -28,12 +27,12 @@ const MAX_AUTO_SELECT = 4;
const EMPTY_SELECTOR = '{}'; const EMPTY_SELECTOR = '{}';
export interface BrowserProps { export interface BrowserProps {
// TODO #33976: Is it possible to use a common interface here? For example: LabelsLanguageProvider languageProvider: LokiLanguageProvider;
languageProvider: LokiLanguageProvider | PromQlLanguageProvider;
onChange: (selector: string) => void; onChange: (selector: string) => void;
theme: GrafanaTheme2; theme: GrafanaTheme2;
app?: CoreApp; app?: CoreApp;
autoSelect?: number; autoSelect?: number;
timeRange?: TimeRange;
hide?: () => void; hide?: () => void;
lastUsedLabels: string[]; lastUsedLabels: string[];
storeLastUsedLabels: (labels: string[]) => void; storeLastUsedLabels: (labels: string[]) => void;
@@ -283,10 +282,10 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
} }
componentDidMount() { componentDidMount() {
const { languageProvider, autoSelect = MAX_AUTO_SELECT, lastUsedLabels } = this.props; const { languageProvider, autoSelect = MAX_AUTO_SELECT, lastUsedLabels, timeRange } = this.props;
if (languageProvider) { if (languageProvider) {
const selectedLabels: string[] = lastUsedLabels; const selectedLabels: string[] = lastUsedLabels;
languageProvider.start().then(() => { languageProvider.start(timeRange).then(() => {
let rawLabels: string[] = languageProvider.getLabelKeys(); let rawLabels: string[] = languageProvider.getLabelKeys();
if (rawLabels.length > MAX_LABEL_COUNT) { if (rawLabels.length > MAX_LABEL_COUNT) {
const error = `Too many labels found (showing only ${MAX_LABEL_COUNT} of ${rawLabels.length})`; const error = `Too many labels found (showing only ${MAX_LABEL_COUNT} of ${rawLabels.length})`;
@@ -347,10 +346,10 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
}; };
async fetchValues(name: string, selector: string) { async fetchValues(name: string, selector: string) {
const { languageProvider } = this.props; const { languageProvider, timeRange } = this.props;
this.updateLabelState(name, { loading: true }, `Fetching values for ${name}`); this.updateLabelState(name, { loading: true }, `Fetching values for ${name}`);
try { try {
let rawValues = await languageProvider.fetchLabelValues(name); let rawValues = await languageProvider.fetchLabelValues(name, { timeRange });
// If selector changed, clear loading state and discard result by returning early // If selector changed, clear loading state and discard result by returning early
if (selector !== buildSelector(this.state.labels)) { if (selector !== buildSelector(this.state.labels)) {
this.updateLabelState(name, { loading: false }, ''); this.updateLabelState(name, { loading: false }, '');
@@ -369,12 +368,12 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
} }
async fetchSeries(selector: string, lastFacetted?: string) { async fetchSeries(selector: string, lastFacetted?: string) {
const { languageProvider } = this.props; const { languageProvider, timeRange } = this.props;
if (lastFacetted) { if (lastFacetted) {
this.updateLabelState(lastFacetted, { loading: true }, `Loading labels for ${selector}`); this.updateLabelState(lastFacetted, { loading: true }, `Loading labels for ${selector}`);
} }
try { try {
const possibleLabels = await languageProvider.fetchSeriesLabels(selector, true); const possibleLabels = await languageProvider.fetchSeriesLabels(selector, { timeRange });
// If selector changed, clear loading state and discard result by returning early // If selector changed, clear loading state and discard result by returning early
if (selector !== buildSelector(this.state.labels)) { if (selector !== buildSelector(this.state.labels)) {
if (lastFacetted) { if (lastFacetted) {
@@ -397,9 +396,9 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
} }
async validateSelector(selector: string) { async validateSelector(selector: string) {
const { languageProvider } = this.props; const { languageProvider, timeRange } = this.props;
this.setState({ validationStatus: `Validating selector ${selector}`, error: '' }); this.setState({ validationStatus: `Validating selector ${selector}`, error: '' });
const streams = await languageProvider.fetchSeries(selector); const streams = await languageProvider.fetchSeries(selector, { timeRange });
this.setState({ validationStatus: `Selector is valid (${streams.length} streams found)` }); this.setState({ validationStatus: `Selector is valid (${streams.length} streams found)` });
} }

View File

@@ -143,6 +143,7 @@ export const LokiQueryEditor = React.memo<LokiQueryEditorProps>((props) => {
onClose={() => setLabelBrowserVisible(false)} onClose={() => setLabelBrowserVisible(false)}
onChange={onChangeInternal} onChange={onChangeInternal}
onRunQuery={onRunQuery} onRunQuery={onRunQuery}
timeRange={timeRange}
/> />
<EditorHeader> <EditorHeader>
<Stack gap={1}> <Stack gap={1}>
@@ -196,6 +197,7 @@ export const LokiQueryEditor = React.memo<LokiQueryEditorProps>((props) => {
onChange={onChangeInternal} onChange={onChangeInternal}
onRunQuery={props.onRunQuery} onRunQuery={props.onRunQuery}
showExplain={explain} showExplain={explain}
timeRange={timeRange}
/> />
)} )}
<LokiQueryBuilderOptions <LokiQueryBuilderOptions

View File

@@ -49,6 +49,7 @@ describe('LokiQueryField', () => {
rerender(<LokiQueryField {...props} range={newRange} />); rerender(<LokiQueryField {...props} range={newRange} />);
expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1); expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1);
expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledWith({ timeRange: newRange });
}); });
it('does not refreshes metrics when time range change by less than 1 minute', async () => { it('does not refreshes metrics when time range change by less than 1 minute', async () => {

View File

@@ -29,7 +29,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
async componentDidMount() { async componentDidMount() {
this._isMounted = true; this._isMounted = true;
await this.props.datasource.languageProvider.start(); await this.props.datasource.languageProvider.start(this.props.range);
if (this._isMounted) { if (this._isMounted) {
this.setState({ labelsLoaded: true }); this.setState({ labelsLoaded: true });
} }
@@ -47,7 +47,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
const refreshLabels = shouldRefreshLabels(range, prevProps.range); const refreshLabels = shouldRefreshLabels(range, prevProps.range);
// We want to refresh labels when range changes (we round up intervals to a minute) // We want to refresh labels when range changes (we round up intervals to a minute)
if (refreshLabels) { if (refreshLabels) {
languageProvider.fetchLabels(); languageProvider.fetchLabels({ timeRange: range });
} }
} }
@@ -65,7 +65,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
}; };
render() { render() {
const { ExtraFieldElement, query, datasource, history, onRunQuery } = this.props; const { ExtraFieldElement, query, datasource, history, onRunQuery, range } = this.props;
const placeholder = this.props.placeholder ?? 'Enter a Loki query (run with Shift+Enter)'; const placeholder = this.props.placeholder ?? 'Enter a Loki query (run with Shift+Enter)';
return ( return (
@@ -82,6 +82,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
onRunQuery={onRunQuery} onRunQuery={onRunQuery}
initialValue={query.expr ?? ''} initialValue={query.expr ?? ''}
placeholder={placeholder} placeholder={placeholder}
timeRange={range}
/> />
</div> </div>
</div> </div>

View File

@@ -100,7 +100,16 @@ const getStyles = (theme: GrafanaTheme2, placeholder: string) => {
}; };
}; };
const MonacoQueryField = ({ history, onBlur, onRunQuery, initialValue, datasource, placeholder, onChange }: Props) => { const MonacoQueryField = ({
history,
onBlur,
onRunQuery,
initialValue,
datasource,
placeholder,
onChange,
timeRange,
}: Props) => {
const id = uuidv4(); const id = uuidv4();
// we need only one instance of `overrideServices` during the lifetime of the react component // we need only one instance of `overrideServices` during the lifetime of the react component
const overrideServicesRef = useRef(getOverrideServices()); const overrideServicesRef = useRef(getOverrideServices());
@@ -203,7 +212,7 @@ const MonacoQueryField = ({ history, onBlur, onRunQuery, initialValue, datasourc
onTypeDebounced(query); onTypeDebounced(query);
monaco.editor.setModelMarkers(model, 'owner', markers); monaco.editor.setModelMarkers(model, 'owner', markers);
}); });
const dataProvider = new CompletionDataProvider(langProviderRef.current, historyRef); const dataProvider = new CompletionDataProvider(langProviderRef.current, historyRef, timeRange);
const completionProvider = getCompletionProvider(monaco, dataProvider); const completionProvider = getCompletionProvider(monaco, dataProvider);
// completion-providers in monaco are not registered directly to editor-instances, // completion-providers in monaco are not registered directly to editor-instances,

View File

@@ -1,4 +1,4 @@
import { HistoryItem } from '@grafana/data'; import { HistoryItem, TimeRange } from '@grafana/data';
import { LokiDatasource } from '../../datasource'; import { LokiDatasource } from '../../datasource';
import { LokiQuery } from '../../types'; import { LokiQuery } from '../../types';
@@ -15,4 +15,5 @@ export type Props = {
placeholder: string; placeholder: string;
datasource: LokiDatasource; datasource: LokiDatasource;
onChange: (query: string) => void; onChange: (query: string) => void;
timeRange?: TimeRange;
}; };

View File

@@ -1,4 +1,4 @@
import { HistoryItem } from '@grafana/data'; import { HistoryItem, dateTime } from '@grafana/data';
import LokiLanguageProvider from '../../../LanguageProvider'; import LokiLanguageProvider from '../../../LanguageProvider';
import { LokiDatasource } from '../../../datasource'; import { LokiDatasource } from '../../../datasource';
@@ -56,6 +56,15 @@ const parserAndLabelKeys = {
hasPack: false, hasPack: false,
}; };
const mockTimeRange = {
from: dateTime(1546372800000),
to: dateTime(1546380000000),
raw: {
from: dateTime(1546372800000),
to: dateTime(1546380000000),
},
};
describe('CompletionDataProvider', () => { describe('CompletionDataProvider', () => {
let completionProvider: CompletionDataProvider, languageProvider: LokiLanguageProvider, datasource: LokiDatasource; let completionProvider: CompletionDataProvider, languageProvider: LokiLanguageProvider, datasource: LokiDatasource;
let historyRef: { current: Array<HistoryItem<LokiQuery>> } = { current: [] }; let historyRef: { current: Array<HistoryItem<LokiQuery>> } = { current: [] };
@@ -63,7 +72,8 @@ describe('CompletionDataProvider', () => {
datasource = createLokiDatasource(); datasource = createLokiDatasource();
languageProvider = new LokiLanguageProvider(datasource); languageProvider = new LokiLanguageProvider(datasource);
historyRef.current = history; historyRef.current = history;
completionProvider = new CompletionDataProvider(languageProvider, historyRef);
completionProvider = new CompletionDataProvider(languageProvider, historyRef, mockTimeRange);
jest.spyOn(languageProvider, 'getLabelKeys').mockReturnValue(labelKeys); jest.spyOn(languageProvider, 'getLabelKeys').mockReturnValue(labelKeys);
jest.spyOn(languageProvider, 'fetchLabelValues').mockResolvedValue(labelValues); jest.spyOn(languageProvider, 'fetchLabelValues').mockResolvedValue(labelValues);
@@ -163,6 +173,11 @@ describe('CompletionDataProvider', () => {
expect(languageProvider.getParserAndLabelKeys).toHaveBeenCalledTimes(4); expect(languageProvider.getParserAndLabelKeys).toHaveBeenCalledTimes(4);
}); });
test('Uses time range from CompletionProvider', async () => {
completionProvider.getParserAndLabelKeys('');
expect(languageProvider.getParserAndLabelKeys).toHaveBeenCalledWith('', { timeRange: mockTimeRange });
});
test('Returns the expected series labels', async () => { test('Returns the expected series labels', async () => {
expect(await completionProvider.getSeriesLabels([])).toEqual(seriesLabels); expect(await completionProvider.getSeriesLabels([])).toEqual(seriesLabels);
}); });

View File

@@ -1,6 +1,6 @@
import { chain } from 'lodash'; import { chain } from 'lodash';
import { HistoryItem } from '@grafana/data'; import { HistoryItem, TimeRange } from '@grafana/data';
import { escapeLabelValueInExactSelector } from 'app/plugins/datasource/prometheus/language_utils'; import { escapeLabelValueInExactSelector } from 'app/plugins/datasource/prometheus/language_utils';
import LanguageProvider from '../../../LanguageProvider'; import LanguageProvider from '../../../LanguageProvider';
@@ -15,7 +15,8 @@ interface HistoryRef {
export class CompletionDataProvider { export class CompletionDataProvider {
constructor( constructor(
private languageProvider: LanguageProvider, private languageProvider: LanguageProvider,
private historyRef: HistoryRef = { current: [] } private historyRef: HistoryRef = { current: [] },
private timeRange: TimeRange | undefined
) { ) {
this.queryToLabelKeysCache = new Map(); this.queryToLabelKeysCache = new Map();
} }
@@ -51,7 +52,7 @@ export class CompletionDataProvider {
async getLabelValues(labelName: string, otherLabels: Label[]) { async getLabelValues(labelName: string, otherLabels: Label[]) {
if (otherLabels.length === 0) { if (otherLabels.length === 0) {
// if there is no filtering, we have to use a special endpoint // if there is no filtering, we have to use a special endpoint
return await this.languageProvider.fetchLabelValues(labelName); return await this.languageProvider.fetchLabelValues(labelName, { timeRange: this.timeRange });
} }
const data = await this.getSeriesLabels(otherLabels); const data = await this.getSeriesLabels(otherLabels);
@@ -82,7 +83,7 @@ export class CompletionDataProvider {
this.queryToLabelKeysCache.delete(firstKey); this.queryToLabelKeysCache.delete(firstKey);
} }
// Fetch a fresh result from the backend // Fetch a fresh result from the backend
const labelKeys = await this.languageProvider.getParserAndLabelKeys(logQuery); const labelKeys = await this.languageProvider.getParserAndLabelKeys(logQuery, { timeRange: this.timeRange });
// Add the result to the cache // Add the result to the cache
this.queryToLabelKeysCache.set(logQuery, labelKeys); this.queryToLabelKeysCache.set(logQuery, labelKeys);
return labelKeys; return labelKeys;
@@ -90,6 +91,8 @@ export class CompletionDataProvider {
} }
async getSeriesLabels(labels: Label[]) { async getSeriesLabels(labels: Label[]) {
return await this.languageProvider.fetchSeriesLabels(this.buildSelector(labels)).then((data) => data ?? {}); return await this.languageProvider
.fetchSeriesLabels(this.buildSelector(labels), { timeRange: this.timeRange })
.then((data) => data ?? {});
} }
} }

View File

@@ -1,3 +1,4 @@
import { dateTime } from '@grafana/data';
import { Monaco, monacoTypes } from '@grafana/ui/src'; import { Monaco, monacoTypes } from '@grafana/ui/src';
import LokiLanguageProvider from '../../../LanguageProvider'; import LokiLanguageProvider from '../../../LanguageProvider';
@@ -31,6 +32,15 @@ const history = [
}, },
]; ];
const mockTimeRange = {
from: dateTime(1546372800000),
to: dateTime(1546380000000),
raw: {
from: dateTime(1546372800000),
to: dateTime(1546380000000),
},
};
const labelNames = ['place', 'source']; const labelNames = ['place', 'source'];
const labelValues = ['moon', 'luna', 'server\\1']; const labelValues = ['moon', 'luna', 'server\\1'];
// Source is duplicated to test handling duplicated labels // Source is duplicated to test handling duplicated labels
@@ -195,9 +205,13 @@ describe('getCompletions', () => {
beforeEach(() => { beforeEach(() => {
datasource = createLokiDatasource(); datasource = createLokiDatasource();
languageProvider = new LokiLanguageProvider(datasource); languageProvider = new LokiLanguageProvider(datasource);
completionProvider = new CompletionDataProvider(languageProvider, { completionProvider = new CompletionDataProvider(
current: history, languageProvider,
}); {
current: history,
},
mockTimeRange
);
jest.spyOn(completionProvider, 'getLabelNames').mockResolvedValue(labelNames); jest.spyOn(completionProvider, 'getLabelNames').mockResolvedValue(labelNames);
jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues); jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues);
@@ -433,9 +447,13 @@ describe('getAfterSelectorCompletions', () => {
beforeEach(() => { beforeEach(() => {
datasource = createLokiDatasource(); datasource = createLokiDatasource();
languageProvider = new LokiLanguageProvider(datasource); languageProvider = new LokiLanguageProvider(datasource);
completionProvider = new CompletionDataProvider(languageProvider, { completionProvider = new CompletionDataProvider(
current: history, languageProvider,
}); {
current: history,
},
mockTimeRange
);
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
extractedLabelKeys: ['abc', 'def'], extractedLabelKeys: ['abc', 'def'],
@@ -524,9 +542,13 @@ describe('IN_LOGFMT completions', () => {
beforeEach(() => { beforeEach(() => {
datasource = createLokiDatasource(); datasource = createLokiDatasource();
languageProvider = new LokiLanguageProvider(datasource); languageProvider = new LokiLanguageProvider(datasource);
completionProvider = new CompletionDataProvider(languageProvider, { completionProvider = new CompletionDataProvider(
current: history, languageProvider,
}); {
current: history,
},
mockTimeRange
);
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
extractedLabelKeys: ['label1', 'label2'], extractedLabelKeys: ['label1', 'label2'],

View File

@@ -458,12 +458,12 @@ export class LokiDatasource
} }
/** /**
* Retrieve the current time range as Loki parameters. * Given a time range, returns it as Loki parameters.
* @returns An object containing the start and end times in nanoseconds since the Unix epoch. * @returns An object containing the start and end times in nanoseconds since the Unix epoch.
*/ */
getTimeRangeParams() { getTimeRangeParams(timeRange?: TimeRange) {
const timeRange = this.getTimeRange(); const range = timeRange ?? this.getTimeRange();
return { start: timeRange.from.valueOf() * NS_IN_MS, end: timeRange.to.valueOf() * NS_IN_MS }; return { start: range.from.valueOf() * NS_IN_MS, end: range.to.valueOf() * NS_IN_MS };
} }
/** /**
@@ -758,7 +758,7 @@ export class LokiDatasource
* Currently, it works for logs data only. * Currently, it works for logs data only.
* @returns A Promise that resolves to an array of DataFrames containing data samples. * @returns A Promise that resolves to an array of DataFrames containing data samples.
*/ */
async getDataSamples(query: LokiQuery): Promise<DataFrame[]> { async getDataSamples(query: LokiQuery, timeRange?: TimeRange): Promise<DataFrame[]> {
// Currently works only for logs sample // Currently works only for logs sample
if (!isLogsQuery(query.expr) || isQueryWithError(this.interpolateString(query.expr, placeHolderScopedVars))) { if (!isLogsQuery(query.expr) || isQueryWithError(this.interpolateString(query.expr, placeHolderScopedVars))) {
return []; return [];
@@ -772,8 +772,8 @@ export class LokiDatasource
supportingQueryType: SupportingQueryType.DataSample, supportingQueryType: SupportingQueryType.DataSample,
}; };
const timeRange = this.getTimeRange(); const range = timeRange ?? this.getTimeRange();
const request = makeRequest(lokiLogsQuery, timeRange, CoreApp.Unknown, REF_ID_DATA_SAMPLES, true); const request = makeRequest(lokiLogsQuery, range, CoreApp.Unknown, REF_ID_DATA_SAMPLES, true);
return await lastValueFrom(this.query(request).pipe(switchMap((res) => of(res.data)))); return await lastValueFrom(this.query(request).pipe(switchMap((res) => of(res.data))));
} }

View File

@@ -29,10 +29,12 @@ We strongly advise using these recommended methods instead of direct API calls b
* This asynchronous function is designed to retrieve all available label keys from the data source. * This asynchronous function is designed to retrieve all available label keys from the data source.
* It returns a promise that resolves to an array of strings containing the label keys. * 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.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. * @returns A promise containing an array of label keys.
* @throws An error if the fetch operation fails. * @throws An error if the fetch operation fails.
*/ */
async function fetchLabels(): Promise<string[]>; async function fetchLabels(options?: { timeRange?: TimeRange }): Promise<string[]>;
/** /**
* Example usage: * Example usage:
@@ -58,12 +60,16 @@ The `datasource.languageProvider.fetchLabelValues()` method is designed for fetc
* It returns a promise that resolves to an array of strings containing the label values. * It returns a promise that resolves to an array of strings containing the label values.
* *
* @param labelName - The name of the label for which you want to retrieve values. * @param labelName - The name of the label for which you want to retrieve values.
* @param options - (Optional) An object containing additional options - currently only stream selector. * @param options - (Optional) An object containing additional options.
* @param options.streamSelector - (Optional) The stream selector to filter label values. If not provided, all label values are fetched. * @param options.streamSelector - (Optional) The stream selector to filter label values. If not provided, all label values are fetched.
* @param options.timeRange - (Optional) The time range for which you want to retrieve label values. If not provided, the default time range is used.
* @returns A promise containing an array of label values. * @returns A promise containing an array of label values.
* @throws An error if the fetch operation fails. * @throws An error if the fetch operation fails.
*/ */
async function fetchLabelValues(labelName: string, options?: { streamSelector?: string }): Promise<string[]>; async function fetchLabelValues(
labelName: string,
options?: { streamSelector?: string; timeRange?: TimeRange }
): Promise<string[]>;
/** /**
* Example usage without stream selector: * Example usage without stream selector:
@@ -103,10 +109,15 @@ try {
* It returns a promise that resolves to a record mapping label names to their corresponding values. * It returns a promise that resolves to a record mapping label names to their corresponding values.
* *
* @param streamSelector - The stream selector for which you want to retrieve labels. * @param streamSelector - The stream selector for which you want to retrieve labels.
* @param options - (Optional) An object containing additional options - currently only time range.
* @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 a record of label names and their values. * @returns A promise containing a record of label names and their values.
* @throws An error if the fetch operation fails. * @throws An error if the fetch operation fails.
*/ */
async function fetchSeriesLabels(streamSelector: string): Promise<Record<string, string[]>>; async function fetchSeriesLabels(
streamSelector: string,
options?: { timeRange?: TimeRange }
): Promise<Record<string, string[]>>;
/** /**
* Example usage: * Example usage:
@@ -138,15 +149,16 @@ try {
* - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data. * - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data.
* *
* @param streamSelector - The selector for the log stream you want to analyze. * @param streamSelector - The selector for the log stream you want to analyze.
* @param {Object} [options] - Optional parameters. * @param options - (Optional) An object containing additional options.
* @param {number} [options.maxLines] - The number of log lines requested when determining parsers and label keys. * @param options.maxLines - (Optional) The number of log lines requested when determining parsers and label keys.
* @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used.
* Smaller maxLines is recommended for improved query performance. The default count is 10. * Smaller maxLines is recommended for improved query performance. The default count is 10.
* @returns A promise containing an object with parser and label key information. * @returns A promise containing an object with parser and label key information.
* @throws An error if the fetch operation fails. * @throws An error if the fetch operation fails.
*/ */
async function getParserAndLabelKeys( async function getParserAndLabelKeys(
streamSelector: string, streamSelector: string,
options?: { maxLines?: number } options?: { maxLines?: number; timeRange?: TimeRange }
): Promise<{ ): Promise<{
extractedLabelKeys: string[]; extractedLabelKeys: string[];
hasJSON: boolean; hasJSON: boolean;

View File

@@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { CoreApp, GrafanaTheme2 } from '@grafana/data'; import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui'; import { LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui';
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
@@ -15,13 +15,14 @@ export interface Props {
datasource: LokiDatasource; datasource: LokiDatasource;
query: LokiQuery; query: LokiQuery;
app?: CoreApp; app?: CoreApp;
timeRange?: TimeRange;
onClose: () => void; onClose: () => void;
onChange: (query: LokiQuery) => void; onChange: (query: LokiQuery) => void;
onRunQuery: () => void; onRunQuery: () => void;
} }
export const LabelBrowserModal = (props: Props) => { export const LabelBrowserModal = (props: Props) => {
const { isOpen, onClose, datasource, app } = props; const { isOpen, onClose, datasource, app, timeRange } = props;
const [labelsLoaded, setLabelsLoaded] = useState(false); const [labelsLoaded, setLabelsLoaded] = useState(false);
const [hasLogLabels, setHasLogLabels] = useState(false); const [hasLogLabels, setHasLogLabels] = useState(false);
const LAST_USED_LABELS_KEY = 'grafana.datasources.loki.browser.labels'; const LAST_USED_LABELS_KEY = 'grafana.datasources.loki.browser.labels';
@@ -33,11 +34,11 @@ export const LabelBrowserModal = (props: Props) => {
return; return;
} }
datasource.languageProvider.fetchLabels().then((labels) => { datasource.languageProvider.fetchLabels({ timeRange }).then((labels) => {
setLabelsLoaded(true); setLabelsLoaded(true);
setHasLogLabels(labels.length > 0); setHasLogLabels(labels.length > 0);
}); });
}, [datasource, isOpen]); }, [datasource, isOpen, timeRange]);
const changeQuery = (value: string) => { const changeQuery = (value: string) => {
const { query, onChange, onRunQuery } = props; const { query, onChange, onRunQuery } = props;
@@ -74,6 +75,7 @@ export const LabelBrowserModal = (props: Props) => {
storeLastUsedLabels={onLastUsedLabelsSave} storeLastUsedLabels={onLastUsedLabelsSave}
deleteLastUsedLabels={onLastUsedLabelsDelete} deleteLastUsedLabels={onLastUsedLabelsDelete}
app={app} app={app}
timeRange={timeRange}
/> />
); );
}} }}

View File

@@ -3,6 +3,8 @@ import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { getSelectParent } from 'test/helpers/selectOptionInTest'; import { getSelectParent } from 'test/helpers/selectOptionInTest';
import { dateTime } from '@grafana/data';
import { MISSING_LABEL_FILTER_ERROR_MESSAGE } from '../../../prometheus/querybuilder/shared/LabelFilters'; import { MISSING_LABEL_FILTER_ERROR_MESSAGE } from '../../../prometheus/querybuilder/shared/LabelFilters';
import { createLokiDatasource } from '../../mocks'; import { createLokiDatasource } from '../../mocks';
import { LokiOperationId, LokiVisualQuery } from '../types'; import { LokiOperationId, LokiVisualQuery } from '../types';
@@ -15,6 +17,15 @@ const defaultQuery: LokiVisualQuery = {
operations: [], operations: [],
}; };
const mockTimeRange = {
from: dateTime(1546372800000),
to: dateTime(1546380000000),
raw: {
from: dateTime(1546372800000),
to: dateTime(1546380000000),
},
};
const createDefaultProps = () => { const createDefaultProps = () => {
const datasource = createLokiDatasource(); const datasource = createLokiDatasource();
@@ -23,6 +34,7 @@ const createDefaultProps = () => {
onRunQuery: () => {}, onRunQuery: () => {},
onChange: () => {}, onChange: () => {},
showExplain: false, showExplain: false,
timeRange: mockTimeRange,
}; };
return props; return props;
@@ -39,6 +51,9 @@ describe('LokiQueryBuilder', () => {
const labels = screen.getByText(/Label filters/); const labels = screen.getByText(/Label filters/);
const selects = getAllByRole(getSelectParent(labels)!, 'combobox'); const selects = getAllByRole(getSelectParent(labels)!, 'combobox');
await userEvent.click(selects[3]); await userEvent.click(selects[3]);
expect(props.datasource.languageProvider.fetchSeriesLabels).toBeCalledWith('{baz="bar"}', {
timeRange: mockTimeRange,
});
await waitFor(() => expect(screen.getByText('job')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('job')).toBeInTheDocument());
}); });

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { DataSourceApi, getDefaultTimeRange, LoadingState, PanelData, SelectableValue } from '@grafana/data'; import { DataSourceApi, getDefaultTimeRange, LoadingState, PanelData, SelectableValue, TimeRange } from '@grafana/data';
import { EditorRow } from '@grafana/experimental'; import { EditorRow } from '@grafana/experimental';
import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters'; import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters';
import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox'; import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox';
@@ -29,149 +29,152 @@ export interface Props {
query: LokiVisualQuery; query: LokiVisualQuery;
datasource: LokiDatasource; datasource: LokiDatasource;
showExplain: boolean; showExplain: boolean;
timeRange?: TimeRange;
onChange: (update: LokiVisualQuery) => void; onChange: (update: LokiVisualQuery) => void;
onRunQuery: () => void; onRunQuery: () => void;
} }
export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange, onRunQuery, showExplain }) => { export const LokiQueryBuilder = React.memo<Props>(
const [sampleData, setSampleData] = useState<PanelData>(); ({ datasource, query, onChange, onRunQuery, showExplain, timeRange }) => {
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>(undefined); const [sampleData, setSampleData] = useState<PanelData>();
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>(undefined);
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => { const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
onChange({ ...query, labels }); onChange({ ...query, labels });
};
const withTemplateVariableOptions = async (optionsPromise: Promise<string[]>): Promise<SelectableValue[]> => {
const options = await optionsPromise;
return [...datasource.getVariables(), ...options].map((value) => ({ label: value, value }));
};
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<string[]> => {
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
if (labelsToConsider.length === 0) {
return await datasource.languageProvider.fetchLabels();
}
const expr = lokiQueryModeller.renderLabels(labelsToConsider);
const series = await datasource.languageProvider.fetchSeriesLabels(expr);
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;
};
const onGetLabelValues = async (forLabel: Partial<QueryBuilderLabelFilter>) => {
if (!forLabel.label) {
return [];
}
let values;
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
if (labelsToConsider.length === 0) {
values = await datasource.languageProvider.fetchLabelValues(forLabel.label);
} else {
const expr = lokiQueryModeller.renderLabels(labelsToConsider);
const result = await datasource.languageProvider.fetchSeriesLabels(expr);
values = result[datasource.interpolateString(forLabel.label)];
}
return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return
};
const labelFilterRequired: boolean = useMemo(() => {
const { labels, operations: op } = query;
if (!labels.length && op.length) {
// Filter is required when operations are present (empty line contains operation is exception)
if (op.length === 1 && op[0].id === LokiOperationId.LineContains && op[0].params[0] === '') {
return false;
}
return true;
}
return false;
}, [query]);
useEffect(() => {
const onGetSampleData = async () => {
const lokiQuery = { expr: lokiQueryModeller.renderQuery(query), refId: 'data-samples' };
const series = await datasource.getDataSamples(lokiQuery);
const sampleData = { series, state: LoadingState.Done, timeRange: getDefaultTimeRange() };
setSampleData(sampleData);
}; };
onGetSampleData().catch(console.error); const withTemplateVariableOptions = async (optionsPromise: Promise<string[]>): Promise<SelectableValue[]> => {
}, [datasource, query]); const options = await optionsPromise;
return [...datasource.getVariables(), ...options].map((value) => ({ label: value, value }));
};
const lang = { grammar: logqlGrammar, name: 'logql' }; const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<string[]> => {
return ( const labelsToConsider = query.labels.filter((x) => x !== forLabel);
<div data-testid={testIds.editor}>
<EditorRow> if (labelsToConsider.length === 0) {
<LabelFilters return await datasource.languageProvider.fetchLabels({ timeRange });
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) => }
withTemplateVariableOptions(onGetLabelNames(forLabel))
} const expr = lokiQueryModeller.renderLabels(labelsToConsider);
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) => const series = await datasource.languageProvider.fetchSeriesLabels(expr, { timeRange });
withTemplateVariableOptions(onGetLabelValues(forLabel)) const labelsNamesToConsider = labelsToConsider.map((l) => l.label);
}
labelsFilters={query.labels} const labelNames = Object.keys(series)
onChange={onChangeLabels} // Filter out label names that are already selected
labelFilterRequired={labelFilterRequired} .filter((name) => !labelsNamesToConsider.includes(name))
/> .sort();
</EditorRow>
{showExplain && ( return labelNames;
<OperationExplainedBox };
stepNumber={1}
title={<RawQuery query={`${lokiQueryModeller.renderLabels(query.labels)}`} lang={lang} />} const onGetLabelValues = async (forLabel: Partial<QueryBuilderLabelFilter>) => {
> if (!forLabel.label) {
{EXPLAIN_LABEL_FILTER_CONTENT} return [];
</OperationExplainedBox> }
)}
<OperationsEditorRow> let values;
<OperationList const labelsToConsider = query.labels.filter((x) => x !== forLabel);
queryModeller={lokiQueryModeller} if (labelsToConsider.length === 0) {
query={query} values = await datasource.languageProvider.fetchLabelValues(forLabel.label, { timeRange });
onChange={onChange} } else {
onRunQuery={onRunQuery} const expr = lokiQueryModeller.renderLabels(labelsToConsider);
datasource={datasource as DataSourceApi} const result = await datasource.languageProvider.fetchSeriesLabels(expr);
highlightedOp={highlightedOp} values = result[datasource.interpolateString(forLabel.label)];
/> }
<QueryBuilderHints<LokiVisualQuery>
datasource={datasource} return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return
query={query} };
onChange={onChange}
data={sampleData} const labelFilterRequired: boolean = useMemo(() => {
queryModeller={lokiQueryModeller} const { labels, operations: op } = query;
buildVisualQueryFromString={buildVisualQueryFromString} if (!labels.length && op.length) {
/> // Filter is required when operations are present (empty line contains operation is exception)
</OperationsEditorRow> if (op.length === 1 && op[0].id === LokiOperationId.LineContains && op[0].params[0] === '') {
{showExplain && ( return false;
<OperationListExplained<LokiVisualQuery> }
stepNumber={2} return true;
queryModeller={lokiQueryModeller} }
query={query} return false;
lang={lang} }, [query]);
onMouseEnter={(op) => {
setHighlightedOp(op); useEffect(() => {
}} const onGetSampleData = async () => {
onMouseLeave={() => { const lokiQuery = { expr: lokiQueryModeller.renderQuery(query), refId: 'data-samples' };
setHighlightedOp(undefined); const series = await datasource.getDataSamples(lokiQuery);
}} const sampleData = { series, state: LoadingState.Done, timeRange: getDefaultTimeRange() };
/> setSampleData(sampleData);
)} };
{query.binaryQueries && query.binaryQueries.length > 0 && (
<NestedQueryList onGetSampleData().catch(console.error);
query={query} }, [datasource, query]);
datasource={datasource}
onChange={onChange} const lang = { grammar: logqlGrammar, name: 'logql' };
onRunQuery={onRunQuery} return (
showExplain={showExplain} <div data-testid={testIds.editor}>
/> <EditorRow>
)} <LabelFilters
</div> onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
); withTemplateVariableOptions(onGetLabelNames(forLabel))
}); }
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) =>
withTemplateVariableOptions(onGetLabelValues(forLabel))
}
labelsFilters={query.labels}
onChange={onChangeLabels}
labelFilterRequired={labelFilterRequired}
/>
</EditorRow>
{showExplain && (
<OperationExplainedBox
stepNumber={1}
title={<RawQuery query={`${lokiQueryModeller.renderLabels(query.labels)}`} lang={lang} />}
>
{EXPLAIN_LABEL_FILTER_CONTENT}
</OperationExplainedBox>
)}
<OperationsEditorRow>
<OperationList
queryModeller={lokiQueryModeller}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
datasource={datasource as DataSourceApi}
highlightedOp={highlightedOp}
/>
<QueryBuilderHints<LokiVisualQuery>
datasource={datasource}
query={query}
onChange={onChange}
data={sampleData}
queryModeller={lokiQueryModeller}
buildVisualQueryFromString={buildVisualQueryFromString}
/>
</OperationsEditorRow>
{showExplain && (
<OperationListExplained<LokiVisualQuery>
stepNumber={2}
queryModeller={lokiQueryModeller}
query={query}
lang={lang}
onMouseEnter={(op) => {
setHighlightedOp(op);
}}
onMouseLeave={() => {
setHighlightedOp(undefined);
}}
/>
)}
{query.binaryQueries && query.binaryQueries.length > 0 && (
<NestedQueryList
query={query}
datasource={datasource}
onChange={onChange}
onRunQuery={onRunQuery}
showExplain={showExplain}
/>
)}
</div>
);
}
);
LokiQueryBuilder.displayName = 'LokiQueryBuilder'; LokiQueryBuilder.displayName = 'LokiQueryBuilder';

View File

@@ -1,6 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import React, { useEffect, useReducer } from 'react'; import React, { useEffect, useReducer } from 'react';
import { TimeRange } from '@grafana/data';
import { testIds } from '../../components/LokiQueryEditor'; import { testIds } from '../../components/LokiQueryEditor';
import { LokiDatasource } from '../../datasource'; import { LokiDatasource } from '../../datasource';
import { LokiQuery } from '../../types'; import { LokiQuery } from '../../types';
@@ -17,6 +19,7 @@ export interface Props {
onChange: (update: LokiQuery) => void; onChange: (update: LokiQuery) => void;
onRunQuery: () => void; onRunQuery: () => void;
showExplain: boolean; showExplain: boolean;
timeRange?: TimeRange;
} }
export interface State { export interface State {
@@ -28,7 +31,7 @@ export interface State {
* This component is here just to contain the translation logic between string query and the visual query builder model. * This component is here just to contain the translation logic between string query and the visual query builder model.
*/ */
export function LokiQueryBuilderContainer(props: Props) { export function LokiQueryBuilderContainer(props: Props) {
const { query, onChange, onRunQuery, datasource, showExplain } = props; const { query, onChange, onRunQuery, datasource, showExplain, timeRange } = props;
const [state, dispatch] = useReducer(stateSlice.reducer, { const [state, dispatch] = useReducer(stateSlice.reducer, {
expr: query.expr, expr: query.expr,
// Use initial visual query only if query.expr is empty string // Use initial visual query only if query.expr is empty string
@@ -65,6 +68,7 @@ export function LokiQueryBuilderContainer(props: Props) {
onRunQuery={onRunQuery} onRunQuery={onRunQuery}
showExplain={showExplain} showExplain={showExplain}
data-testid={testIds.editor} data-testid={testIds.editor}
timeRange={timeRange}
/> />
{query.expr !== '' && <QueryPreview query={query.expr} />} {query.expr !== '' && <QueryPreview query={query.expr} />}
</> </>