mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
a438576a6d
commit
a31104b107
@ -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);
|
||||
|
||||
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"}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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<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…
Reference in New Issue
Block a user