From a31104b107a6a0ebceaec8786ab1a0d8d02eb563 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 14 Apr 2023 13:44:02 +0200 Subject: [PATCH] Loki: Refactor logic for using context filters (#66382) * Loki: Change logic for using context filters * Dont add parser and parsed labels if multiple * Update public/app/plugins/datasource/loki/LogContextProvider.ts Co-authored-by: Sven Grossmann * Update public/app/plugins/datasource/loki/LogContextProvider.ts Co-authored-by: Sven Grossmann * Update * Rename variable --------- Co-authored-by: Sven Grossmann --- .betterer.results | 6 +- .../loki/LogContextProvider.test.ts | 171 +++++++++++--- .../datasource/loki/LogContextProvider.ts | 213 +++++++++++++----- .../loki/components/LokiContextUi.test.tsx | 48 ++-- .../loki/components/LokiContextUi.tsx | 26 +-- .../app/plugins/datasource/loki/datasource.ts | 75 +----- .../app/plugins/datasource/loki/queryUtils.ts | 14 +- 7 files changed, 327 insertions(+), 226 deletions(-) diff --git a/.betterer.results b/.betterer.results index 77c376299b1..ab21280defc 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4580,9 +4580,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/loki/LiveStreams.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/loki/LogContextProvider.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] @@ -4616,6 +4613,9 @@ exports[`better eslint`] = { "public/app/plugins/datasource/loki/getDerivedFields.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/plugins/datasource/loki/queryUtils.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/plugins/datasource/loki/querybuilder/binaryScalarOperations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/plugins/datasource/loki/LogContextProvider.test.ts b/public/app/plugins/datasource/loki/LogContextProvider.test.ts index 6a494ef8b79..7a4feeb7fd5 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.test.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.test.ts @@ -1,13 +1,27 @@ -import { FieldType, LogRowContextQueryDirection, LogRowModel, MutableDataFrame } from '@grafana/data'; +import { of } from 'rxjs'; + +import { + DataQueryResponse, + FieldType, + LogRowContextQueryDirection, + LogRowModel, + MutableDataFrame, +} from '@grafana/data'; import LokiLanguageProvider from './LanguageProvider'; import { LogContextProvider } from './LogContextProvider'; +import { createLokiDatasource } from './mocks'; +import { LokiQuery } from './types'; const defaultLanguageProviderMock = { start: jest.fn(), - getLabelKeys: jest.fn(() => ['foo']), + getLabelKeys: jest.fn(() => ['bar', 'xyz']), } as unknown as LokiLanguageProvider; +const defaultDatasourceMock = createLokiDatasource(); +defaultDatasourceMock.query = jest.fn(() => of({ data: [] } as DataQueryResponse)); +defaultDatasourceMock.languageProvider = defaultLanguageProviderMock; + const defaultLogRow = { rowIndex: 0, dataFrame: new MutableDataFrame({ @@ -19,46 +33,129 @@ const defaultLogRow = { }, ], }), - labels: { bar: 'baz', foo: 'uniqueParsedLabel' }, + labels: { bar: 'baz', foo: 'uniqueParsedLabel', xyz: 'abc' }, uid: '1', } as unknown as LogRowModel; -describe('new context ui', () => { - it('returns expression with 1 label', async () => { - const lcp = new LogContextProvider(defaultLanguageProviderMock); - const result = await lcp.prepareContextExpr(defaultLogRow); - - expect(result).toEqual('{foo="uniqueParsedLabel"}'); - }); - - it('returns empty expression for parsed labels', async () => { - const languageProviderMock = { - ...defaultLanguageProviderMock, - getLabelKeys: jest.fn(() => []), - } as unknown as LokiLanguageProvider; - - const lcp = new LogContextProvider(languageProviderMock); - const result = await lcp.prepareContextExpr(defaultLogRow); - - expect(result).toEqual('{}'); - }); -}); - -describe('prepareLogRowContextQueryTarget', () => { - const lcp = new LogContextProvider(defaultLanguageProviderMock); - it('creates query with only labels from /labels API', async () => { - const contextQuery = await lcp.prepareLogRowContextQueryTarget( - defaultLogRow, - 10, - LogRowContextQueryDirection.Backward +describe('LogContextProvider', () => { + let logContextProvider: LogContextProvider; + beforeEach(() => { + logContextProvider = new LogContextProvider(defaultDatasourceMock); + logContextProvider.getInitContextFiltersFromLabels = jest.fn(() => + Promise.resolve([{ value: 'bar', enabled: true, fromParser: false, label: 'bar' }]) ); - - expect(contextQuery.query.expr).toContain('uniqueParsedLabel'); - expect(contextQuery.query.expr).not.toContain('baz'); }); - it('should call languageProvider.start to fetch labels', async () => { - await lcp.prepareLogRowContextQueryTarget(defaultLogRow, 10, LogRowContextQueryDirection.Backward); - expect(lcp.languageProvider.start).toBeCalled(); + describe('getLogRowContext', () => { + it('should call getInitContextFilters if no appliedContextFilters', async () => { + expect(logContextProvider.appliedContextFilters).toHaveLength(0); + await logContextProvider.getLogRowContext(defaultLogRow, { + limit: 10, + direction: LogRowContextQueryDirection.Backward, + }); + expect(logContextProvider.getInitContextFiltersFromLabels).toBeCalled(); + expect(logContextProvider.appliedContextFilters).toHaveLength(1); + }); + + it('should not call getInitContextFilters if appliedContextFilters', async () => { + logContextProvider.appliedContextFilters = [ + { value: 'bar', enabled: true, fromParser: false, label: 'bar' }, + { value: 'xyz', enabled: true, fromParser: false, label: 'xyz' }, + ]; + await logContextProvider.getLogRowContext(defaultLogRow, { + limit: 10, + direction: LogRowContextQueryDirection.Backward, + }); + expect(logContextProvider.getInitContextFiltersFromLabels).not.toBeCalled(); + expect(logContextProvider.appliedContextFilters).toHaveLength(2); + }); + }); + + describe('prepareLogRowContextQueryTarget', () => { + describe('query with no parser', () => { + const query = { + expr: '{bar="baz"}', + } as LokiQuery; + it('returns empty expression if no appliedContextFilters', async () => { + logContextProvider.appliedContextFilters = []; + const result = await logContextProvider.prepareLogRowContextQueryTarget( + defaultLogRow, + 10, + LogRowContextQueryDirection.Backward, + query + ); + expect(result.query.expr).toEqual('{}'); + }); + + it('should not apply parsed labels', async () => { + logContextProvider.appliedContextFilters = [ + { value: 'bar', enabled: true, fromParser: false, label: 'bar' }, + { value: 'xyz', enabled: true, fromParser: false, label: 'xyz' }, + { value: 'foo', enabled: true, fromParser: true, label: 'foo' }, + ]; + const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget( + defaultLogRow, + 10, + LogRowContextQueryDirection.Backward, + query + ); + + expect(contextQuery.query.expr).toEqual('{bar="baz",xyz="abc"}'); + }); + }); + + describe('query with parser', () => { + it('should apply parser', async () => { + logContextProvider.appliedContextFilters = [ + { value: 'bar', enabled: true, fromParser: false, label: 'bar' }, + { value: 'xyz', enabled: true, fromParser: false, label: 'xyz' }, + ]; + const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget( + defaultLogRow, + 10, + LogRowContextQueryDirection.Backward, + { + expr: '{bar="baz"} | logfmt', + } as LokiQuery + ); + + expect(contextQuery.query.expr).toEqual('{bar="baz",xyz="abc"} | logfmt'); + }); + + it('should apply parser and parsed labels', async () => { + logContextProvider.appliedContextFilters = [ + { value: 'bar', enabled: true, fromParser: false, label: 'bar' }, + { value: 'xyz', enabled: true, fromParser: false, label: 'xyz' }, + { value: 'foo', enabled: true, fromParser: true, label: 'foo' }, + ]; + const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget( + defaultLogRow, + 10, + LogRowContextQueryDirection.Backward, + { + expr: '{bar="baz"} | logfmt', + } as LokiQuery + ); + + expect(contextQuery.query.expr).toEqual('{bar="baz",xyz="abc"} | logfmt | foo=`uniqueParsedLabel`'); + }); + }); + + it('should not apply parser and parsed labels if more parsers in original query', async () => { + logContextProvider.appliedContextFilters = [ + { value: 'bar', enabled: true, fromParser: false, label: 'bar' }, + { value: 'foo', enabled: true, fromParser: true, label: 'foo' }, + ]; + const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget( + defaultLogRow, + 10, + LogRowContextQueryDirection.Backward, + { + expr: '{bar="baz"} | logfmt | json', + } as unknown as LokiQuery + ); + + expect(contextQuery.query.expr).toEqual(`{bar="baz"}`); + }); }); }); diff --git a/public/app/plugins/datasource/loki/LogContextProvider.ts b/public/app/plugins/datasource/loki/LogContextProvider.ts index f8dedc90c3b..087563445c3 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.ts @@ -1,30 +1,128 @@ -import { FieldCache, FieldType, LogRowContextQueryDirection, LogRowModel, TimeRange, toUtc } from '@grafana/data'; -import { DataQuery } from '@grafana/schema'; +import { catchError, lastValueFrom, of, switchMap } from 'rxjs'; + +import { + CoreApp, + DataFrame, + DataQueryError, + DataQueryResponse, + FieldCache, + FieldType, + LogRowModel, + TimeRange, + toUtc, + LogRowContextQueryDirection, + LogRowContextOptions, +} from '@grafana/data'; +import { DataQuery, Labels } from '@grafana/schema'; -import LokiLanguageProvider from './LanguageProvider'; import { LokiContextUi } from './components/LokiContextUi'; -import { REF_ID_STARTER_LOG_ROW_CONTEXT } from './datasource'; +import { LokiDatasource, makeRequest, REF_ID_STARTER_LOG_ROW_CONTEXT } from './datasource'; import { escapeLabelValueInExactSelector } from './languageUtils'; import { addLabelToQuery, addParserToQuery } from './modifyQuery'; -import { getParserFromQuery } from './queryUtils'; +import { getParserFromQuery, isLokiQuery, isQueryWithParser } from './queryUtils'; +import { sortDataFrameByTime, SortDirection } from './sortDataFrame'; import { ContextFilter, LokiQuery, LokiQueryDirection, LokiQueryType } from './types'; export class LogContextProvider { - languageProvider: LokiLanguageProvider; + datasource: LokiDatasource; + appliedContextFilters: ContextFilter[]; onContextClose: (() => void) | undefined; - constructor(languageProvider: LokiLanguageProvider) { - this.languageProvider = languageProvider; + constructor(datasource: LokiDatasource) { + this.datasource = datasource; + this.appliedContextFilters = []; } + getLogRowContext = async ( + row: LogRowModel, + options?: LogRowContextOptions, + origQuery?: DataQuery + ): Promise<{ data: DataFrame[] }> => { + const direction = (options && options.direction) || LogRowContextQueryDirection.Backward; + const limit = (options && options.limit) || 10; + + // This happens only on initial load, when user haven't applied any filters yet + // We need to get the initial filters from the row labels + if (this.appliedContextFilters.length === 0) { + const filters = (await this.getInitContextFiltersFromLabels(row.labels)).filter((filter) => filter.enabled); + this.appliedContextFilters = filters; + } + + const { query, range } = await this.prepareLogRowContextQueryTarget(row, limit, direction, origQuery); + + const processDataFrame = (frame: DataFrame): DataFrame => { + // log-row-context requires specific field-names to work, so we set them here: "ts", "line", "id" + const cache = new FieldCache(frame); + const timestampField = cache.getFirstFieldOfType(FieldType.time); + const lineField = cache.getFirstFieldOfType(FieldType.string); + const idField = cache.getFieldByName('id'); + + if (timestampField === undefined || lineField === undefined || idField === undefined) { + // this should never really happen, but i want to keep typescript happy + return { ...frame, fields: [] }; + } + + return { + ...frame, + fields: [ + { + ...timestampField, + name: 'ts', + }, + { + ...lineField, + name: 'line', + }, + { + ...idField, + name: 'id', + }, + ], + }; + }; + + const processResults = (result: DataQueryResponse): DataQueryResponse => { + const frames: DataFrame[] = result.data; + const processedFrames = frames + .map((frame) => sortDataFrameByTime(frame, SortDirection.Descending)) + .map((frame) => processDataFrame(frame)); // rename fields if needed + + return { + ...result, + data: processedFrames, + }; + }; + + // this can only be called from explore currently + const app = CoreApp.Explore; + + return lastValueFrom( + this.datasource.query(makeRequest(query, range, app, `${REF_ID_STARTER_LOG_ROW_CONTEXT}${direction}`)).pipe( + catchError((err) => { + const error: DataQueryError = { + message: 'Error during context query. Please check JS console logs.', + status: err.status, + statusText: err.statusText, + }; + throw error; + }), + switchMap((res) => of(processResults(res))) + ) + ); + }; + async prepareLogRowContextQueryTarget( row: LogRowModel, limit: number, direction: LogRowContextQueryDirection, origQuery?: DataQuery ): Promise<{ query: LokiQuery; range: TimeRange }> { - let expr = await this.prepareContextExpr(row, origQuery); - + let originalLokiQuery: LokiQuery | undefined = undefined; + // Type guard for LokiQuery + if (origQuery && isLokiQuery(origQuery)) { + originalLokiQuery = origQuery; + } + const expr = this.processContextFiltersToExpr(row, this.appliedContextFilters, originalLokiQuery); const contextTimeBuffer = 2 * 60 * 60 * 1000; // 2h buffer const queryDirection = @@ -74,42 +172,8 @@ export class LogContextProvider { getLogRowContextUi(row: LogRowModel, runContextQuery: () => void): React.ReactNode { const updateFilter = (contextFilters: ContextFilter[]) => { - this.prepareContextExpr = async (row: LogRowModel, origQuery?: DataQuery) => { - await this.languageProvider.start(); - const labels = this.languageProvider.getLabelKeys(); + this.appliedContextFilters = contextFilters; - let expr = contextFilters - .map((filter) => { - const label = filter.value; - if (filter && !filter.fromParser && filter.enabled && labels.includes(label)) { - // escape backslashes in label as users can't escape them by themselves - return `${label}="${escapeLabelValueInExactSelector(row.labels[label])}"`; - } - return ''; - }) - // Filter empty strings - .filter((label) => !!label) - .join(','); - - expr = `{${expr}}`; - - const parserContextFilters = contextFilters.filter((filter) => filter.fromParser && filter.enabled); - if (parserContextFilters.length) { - // we should also filter for labels from parsers, let's find the right parser - if (origQuery) { - const parser = getParserFromQuery((origQuery as LokiQuery).expr); - if (parser) { - expr = addParserToQuery(expr, parser); - } - } - for (const filter of parserContextFilters) { - if (filter.enabled) { - expr = addLabelToQuery(expr, filter.label, '=', row.labels[filter.label]); - } - } - } - return expr; - }; if (runContextQuery) { runContextQuery(); } @@ -119,35 +183,66 @@ export class LogContextProvider { this.onContextClose = this.onContextClose ?? (() => { - this.prepareContextExpr = this.prepareContextExprWithoutParsedLabels; + this.appliedContextFilters = []; }); return LokiContextUi({ row, updateFilter, - languageProvider: this.languageProvider, onClose: this.onContextClose, + logContextProvider: this, }); } - async prepareContextExpr(row: LogRowModel, origQuery?: DataQuery): Promise { - return await this.prepareContextExprWithoutParsedLabels(row, origQuery); - } - - private async prepareContextExprWithoutParsedLabels(row: LogRowModel, origQuery?: DataQuery): Promise { - await this.languageProvider.start(); - const labels = this.languageProvider.getLabelKeys(); - const expr = Object.keys(row.labels) - .map((label: string) => { - if (labels.includes(label)) { + processContextFiltersToExpr = (row: LogRowModel, contextFilters: ContextFilter[], query: LokiQuery | undefined) => { + const labelFilters = contextFilters + .map((filter) => { + const label = filter.value; + if (!filter.fromParser && filter.enabled) { // escape backslashes in label as users can't escape them by themselves return `${label}="${escapeLabelValueInExactSelector(row.labels[label])}"`; } return ''; }) + // Filter empty strings .filter((label) => !!label) .join(','); - return `{${expr}}`; - } + let expr = `{${labelFilters}}`; + + // We need to have original query to get parser and include parsed labels + // We only add parser and parsed labels if there is only one parser in query + if (query && isQueryWithParser(query.expr).parserCount === 1) { + const parser = getParserFromQuery(query.expr); + if (parser) { + expr = addParserToQuery(expr, parser); + const parsedLabels = contextFilters.filter((filter) => filter.fromParser && filter.enabled); + for (const parsedLabel of parsedLabels) { + if (parsedLabel.enabled) { + expr = addLabelToQuery(expr, parsedLabel.label, '=', row.labels[parsedLabel.label]); + } + } + } + } + + return expr; + }; + + getInitContextFiltersFromLabels = async (labels: Labels) => { + await this.datasource.languageProvider.start(); + const allLabels = this.datasource.languageProvider.getLabelKeys(); + const contextFilters: ContextFilter[] = []; + Object.entries(labels).forEach(([label, value]) => { + const filter: ContextFilter = { + label, + value: label, // this looks weird in the first place, but we need to set the label as value here + enabled: allLabels.includes(label), + fromParser: !allLabels.includes(label), + description: value, + }; + contextFilters.push(filter); + }); + + return contextFilters; + }; } diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx index dc324fdc784..fce625d2f5d 100644 --- a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx @@ -4,7 +4,7 @@ import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; import { LogRowModel } from '@grafana/data'; -import LokiLanguageProvider from '../LanguageProvider'; +import { LogContextProvider } from '../LogContextProvider'; import { LokiContextUi, LokiContextUiProps } from './LokiContextUi'; @@ -30,37 +30,17 @@ describe('LokiContextUi', () => { global = savedGlobal; }); const setupProps = (): LokiContextUiProps => { - const mockLanguageProvider = { - start: jest.fn().mockImplementation(() => Promise.resolve()), - getLabelValues: (name: string) => { - switch (name) { - case 'label1': - return ['value1-1', 'value1-2']; - case 'label2': - return ['value2-1', 'value2-2']; - case 'label3': - return ['value3-1', 'value3-2']; - } - return []; - }, - fetchSeriesLabels: (selector: string) => { - switch (selector) { - case '{label1="value1-1"}': - return { label1: ['value1-1'], label2: ['value2-1'], label3: ['value3-1'] }; - case '{label1=~"value1-1|value1-2"}': - return { label1: ['value1-1', 'value1-2'], label2: ['value2-1'], label3: ['value3-1', 'value3-2'] }; - } - // Allow full set by default - return { - label1: ['value1-1', 'value1-2'], - label2: ['value2-1', 'value2-2'], - }; - }, - getLabelKeys: () => ['label1', 'label2'], + const mockLogContextProvider = { + getInitContextFiltersFromLabels: jest.fn().mockImplementation(() => + Promise.resolve([ + { value: 'label1', enabled: true, fromParser: false, label: 'label1' }, + { value: 'label3', enabled: false, fromParser: true, label: 'label3' }, + ]) + ), }; const defaults: LokiContextUiProps = { - languageProvider: mockLanguageProvider as unknown as LokiLanguageProvider, + logContextProvider: mockLogContextProvider as unknown as LogContextProvider, updateFilter: jest.fn(), row: { entry: 'WARN test 1.23 on [xxx]', @@ -83,12 +63,12 @@ describe('LokiContextUi', () => { expect(await screen.findByText(/Select labels to be included in the context query/)).toBeInTheDocument(); }); - it('starts the languageProvider', async () => { + it('initialize context filters', async () => { const props = setupProps(); render(); await waitFor(() => { - expect(props.languageProvider.start).toHaveBeenCalled(); + expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled(); }); }); @@ -96,7 +76,7 @@ describe('LokiContextUi', () => { const props = setupProps(); render(); await waitFor(() => { - expect(props.languageProvider.start).toHaveBeenCalled(); + expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled(); }); const select = await screen.findAllByRole('combobox'); await selectOptionInTest(select[0], 'label1'); @@ -106,7 +86,7 @@ describe('LokiContextUi', () => { const props = setupProps(); render(); await waitFor(() => { - expect(props.languageProvider.start).toHaveBeenCalled(); + expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled(); }); const select = await screen.findAllByRole('combobox'); await selectOptionInTest(select[1], 'label3'); @@ -117,7 +97,7 @@ describe('LokiContextUi', () => { const props = setupProps(); render(); await waitFor(() => { - expect(props.languageProvider.start).toHaveBeenCalled(); + expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled(); expect(screen.getAllByRole('combobox')).toHaveLength(2); }); await selectOptionInTest(screen.getAllByRole('combobox')[1], 'label3'); diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx index 859a715845d..4532dce3b5d 100644 --- a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx @@ -7,11 +7,11 @@ import { GrafanaTheme2, LogRowModel, SelectableValue } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { LoadingPlaceholder, MultiSelect, Tag, Tooltip, useStyles2 } from '@grafana/ui'; -import LokiLanguageProvider from '../LanguageProvider'; +import { LogContextProvider } from '../LogContextProvider'; import { ContextFilter } from '../types'; export interface LokiContextUiProps { - languageProvider: LokiLanguageProvider; + logContextProvider: LogContextProvider; row: LogRowModel; updateFilter: (value: ContextFilter[]) => void; onClose: () => void; @@ -58,7 +58,7 @@ const formatOptionLabel = memoizeOne(({ label, description }: SelectableValue([]); @@ -91,7 +91,7 @@ export function LokiContextUi(props: LokiContextUiProps) { } setLoading(true); timerHandle.current = window.setTimeout(() => { - updateFilter(contextFilters); + updateFilter(contextFilters.filter(({ enabled }) => enabled)); setLoading(false); }, 1500); @@ -109,23 +109,11 @@ export function LokiContextUi(props: LokiContextUiProps) { }, [onClose]); useAsync(async () => { - await languageProvider.start(); - const allLabels = languageProvider.getLabelKeys(); - const contextFilters: ContextFilter[] = []; - - Object.entries(row.labels).forEach(([label, value]) => { - const filter: ContextFilter = { - label, - value: label, // this looks weird in the first place, but we need to set the label as value here - enabled: allLabels.includes(label), - fromParser: !allLabels.includes(label), - description: value, - }; - contextFilters.push(filter); - }); - + setLoading(true); + const contextFilters = await logContextProvider.getInitContextFiltersFromLabels(row.labels); setContextFilters(contextFilters); setInitialized(true); + setLoading(false); }); useEffect(() => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index f07f7b1c52d..384a298e81b 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -32,7 +32,6 @@ import { ScopedVars, TimeRange, LogRowContextOptions, - LogRowContextQueryDirection, } from '@grafana/data'; import { BackendSrvRequest, config, DataSourceWithBackend, FetchError } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; @@ -76,7 +75,6 @@ import { isValidQuery, requestSupportsSplitting, } from './queryUtils'; -import { sortDataFrameByTime, SortDirection } from './sortDataFrame'; import { doLokiChannelStream } from './streaming'; import { trackQuery } from './tracking'; import { @@ -100,7 +98,7 @@ export const REF_ID_STARTER_LOG_VOLUME = 'log-volume-'; export const REF_ID_STARTER_LOG_SAMPLE = 'log-sample-'; const NS_IN_MS = 1000000; -function makeRequest( +export function makeRequest( query: LokiQuery, range: TimeRange, app: CoreApp, @@ -149,7 +147,7 @@ export class LokiDatasource QueryEditor: LokiAnnotationsQueryEditor, }; this.variables = new LokiVariableSupport(this); - this.logContextProvider = new LogContextProvider(this.languageProvider); + this.logContextProvider = new LogContextProvider(this); } getDataProvider( @@ -650,74 +648,7 @@ export class LokiDatasource options?: LogRowContextOptions, origQuery?: DataQuery ): Promise<{ data: DataFrame[] }> => { - const direction = (options && options.direction) || LogRowContextQueryDirection.Backward; - const limit = (options && options.limit) || 10; - const { query, range } = await this.logContextProvider.prepareLogRowContextQueryTarget( - row, - limit, - direction, - origQuery - ); - - const processDataFrame = (frame: DataFrame): DataFrame => { - // log-row-context requires specific field-names to work, so we set them here: "ts", "line", "id" - const cache = new FieldCache(frame); - const timestampField = cache.getFirstFieldOfType(FieldType.time); - const lineField = cache.getFirstFieldOfType(FieldType.string); - const idField = cache.getFieldByName('id'); - - if (timestampField === undefined || lineField === undefined || idField === undefined) { - // this should never really happen, but i want to keep typescript happy - return { ...frame, fields: [] }; - } - - return { - ...frame, - fields: [ - { - ...timestampField, - name: 'ts', - }, - { - ...lineField, - name: 'line', - }, - { - ...idField, - name: 'id', - }, - ], - }; - }; - - const processResults = (result: DataQueryResponse): DataQueryResponse => { - const frames: DataFrame[] = result.data; - const processedFrames = frames - .map((frame) => sortDataFrameByTime(frame, SortDirection.Descending)) - .map((frame) => processDataFrame(frame)); // rename fields if needed - - return { - ...result, - data: processedFrames, - }; - }; - - // this can only be called from explore currently - const app = CoreApp.Explore; - - return lastValueFrom( - this.query(makeRequest(query, range, app, `${REF_ID_STARTER_LOG_ROW_CONTEXT}${direction}`)).pipe( - catchError((err) => { - const error: DataQueryError = { - message: 'Error during context query. Please check JS console logs.', - status: err.status, - statusText: err.statusText, - }; - throw error; - }), - switchMap((res) => of(processResults(res))) - ) - ); + return await this.logContextProvider.getLogRowContext(row, options, origQuery); }; getLogRowContextUi(row: LogRowModel, runContextQuery: () => void): React.ReactNode { diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts index 236e02b0616..9ad5b5a2c7b 100644 --- a/public/app/plugins/datasource/loki/queryUtils.ts +++ b/public/app/plugins/datasource/loki/queryUtils.ts @@ -18,6 +18,7 @@ import { Matcher, Identifier, } from '@grafana/lezer-logql'; +import { DataQuery } from '@grafana/schema'; import { ErrorId } from '../prometheus/querybuilder/shared/parsingUtils'; @@ -175,9 +176,9 @@ export function isQueryWithParser(query: string): { queryWithParser: boolean; pa return { queryWithParser: parserCount > 0, parserCount }; } -export function getParserFromQuery(query: string) { +export function getParserFromQuery(query: string): string | undefined { const tree = parser.parse(query); - let logParser; + let logParser: string | undefined = undefined; tree.iterate({ enter: (node: SyntaxNode): false | void => { if (node.type.id === LabelParser || node.type.id === JsonExpressionParser) { @@ -304,3 +305,12 @@ export function requestSupportsSplitting(allQueries: LokiQuery[]) { return queries.length > 0; } + +export const isLokiQuery = (query: DataQuery): query is LokiQuery => { + if (!query) { + return false; + } + + const lokiQuery = query as LokiQuery; + return lokiQuery.expr !== undefined; +};