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 <sven.grossmann@grafana.com>

* Update public/app/plugins/datasource/loki/LogContextProvider.ts

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>

* Update

* Rename variable

---------

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>
dashboard-previews-drop-table-migration
Ivana Huckova 2 years ago committed by GitHub
parent a438576a6d
commit a31104b107
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .betterer.results
  2. 161
      public/app/plugins/datasource/loki/LogContextProvider.test.ts
  3. 215
      public/app/plugins/datasource/loki/LogContextProvider.ts
  4. 48
      public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx
  5. 26
      public/app/plugins/datasource/loki/components/LokiContextUi.tsx
  6. 75
      public/app/plugins/datasource/loki/datasource.ts
  7. 14
      public/app/plugins/datasource/loki/queryUtils.ts

@ -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"]
],

@ -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);
describe('LogContextProvider', () => {
let logContextProvider: LogContextProvider;
beforeEach(() => {
logContextProvider = new LogContextProvider(defaultDatasourceMock);
logContextProvider.getInitContextFiltersFromLabels = jest.fn(() =>
Promise.resolve([{ value: 'bar', enabled: true, fromParser: false, label: 'bar' }])
);
});
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);
});
expect(result).toEqual('{foo="uniqueParsedLabel"}');
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);
});
});
it('returns empty expression for parsed labels', async () => {
const languageProviderMock = {
...defaultLanguageProviderMock,
getLabelKeys: jest.fn(() => []),
} as unknown as LokiLanguageProvider;
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('{}');
});
const lcp = new LogContextProvider(languageProviderMock);
const result = await lcp.prepareContextExpr(defaultLogRow);
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(result).toEqual('{}');
});
});
expect(contextQuery.query.expr).toEqual('{bar="baz",xyz="abc"}');
});
});
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('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).toContain('uniqueParsedLabel');
expect(contextQuery.query.expr).not.toContain('baz');
});
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
);
it('should call languageProvider.start to fetch labels', async () => {
await lcp.prepareLogRowContextQueryTarget(defaultLogRow, 10, LogRowContextQueryDirection.Backward);
expect(lcp.languageProvider.start).toBeCalled();
expect(contextQuery.query.expr).toEqual(`{bar="baz"}`);
});
});
});

@ -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();
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;
};
this.appliedContextFilters = contextFilters;
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<string> {
return await this.prepareContextExprWithoutParsedLabels(row, origQuery);
}
private async prepareContextExprWithoutParsedLabels(row: LogRowModel, origQuery?: DataQuery): Promise<string> {
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;
};
}

@ -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(<LokiContextUi {...props} />);
await waitFor(() => {
expect(props.languageProvider.start).toHaveBeenCalled();
expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled();
});
});
@ -96,7 +76,7 @@ describe('LokiContextUi', () => {
const props = setupProps();
render(<LokiContextUi {...props} />);
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(<LokiContextUi {...props} />);
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(<LokiContextUi {...props} />);
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');

@ -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<st
));
export function LokiContextUi(props: LokiContextUiProps) {
const { row, languageProvider, updateFilter, onClose } = props;
const { row, logContextProvider, updateFilter, onClose } = props;
const styles = useStyles2(getStyles);
const [contextFilters, setContextFilters] = useState<ContextFilter[]>([]);
@ -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(() => {

@ -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 {

@ -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;
};

Loading…
Cancel
Save