Loki: Implement keep and drop operations (#73636)

* Update lezer

* Add functionalities for code and builder

* Add comment
This commit is contained in:
Ivana Huckova 2023-08-23 14:52:19 +02:00 committed by GitHub
parent 356d8872bd
commit fc9b8f6be1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 341 additions and 6 deletions

View File

@ -240,7 +240,7 @@
"@grafana/faro-core": "1.1.2", "@grafana/faro-core": "1.1.2",
"@grafana/faro-web-sdk": "1.1.2", "@grafana/faro-web-sdk": "1.1.2",
"@grafana/google-sdk": "0.1.1", "@grafana/google-sdk": "0.1.1",
"@grafana/lezer-logql": "0.1.8", "@grafana/lezer-logql": "0.1.9",
"@grafana/lezer-traceql": "0.0.4", "@grafana/lezer-traceql": "0.0.4",
"@grafana/monaco-logql": "^0.0.7", "@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*", "@grafana/runtime": "workspace:*",

View File

@ -130,6 +130,18 @@ const afterSelectorCompletions = [
label: 'distinct', label: 'distinct',
type: 'PIPE_OPERATION', type: 'PIPE_OPERATION',
}, },
{
documentation: 'Operator docs',
insertText: '| drop',
label: 'drop',
type: 'PIPE_OPERATION',
},
{
documentation: 'Operator docs',
insertText: '| keep',
label: 'keep',
type: 'PIPE_OPERATION',
},
]; ];
function buildAfterSelectorCompletions( function buildAfterSelectorCompletions(
@ -413,6 +425,32 @@ describe('getCompletions', () => {
}, },
]); ]);
}); });
test('Returns completion options when the situation is AFTER_KEEP_AND_DROP', async () => {
const situation: Situation = { type: 'AFTER_KEEP_AND_DROP', logQuery: '{label="value"}' };
const completions = await getCompletions(situation, completionProvider);
expect(completions).toEqual([
{
insertText: 'extracted',
label: 'extracted',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
{
insertText: 'place',
label: 'place',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
{
insertText: 'source',
label: 'source',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
]);
});
}); });
describe('getAfterSelectorCompletions', () => { describe('getAfterSelectorCompletions', () => {

View File

@ -293,6 +293,20 @@ export async function getAfterSelectorCompletions(
documentation: explainOperator(LokiOperationId.Distinct), documentation: explainOperator(LokiOperationId.Distinct),
}); });
completions.push({
type: 'PIPE_OPERATION',
label: 'drop',
insertText: `${prefix}drop`,
documentation: explainOperator(LokiOperationId.Drop),
});
completions.push({
type: 'PIPE_OPERATION',
label: 'keep',
insertText: `${prefix}keep`,
documentation: explainOperator(LokiOperationId.Keep),
});
// Let's show label options only if query has parser // Let's show label options only if query has parser
if (hasQueryParser) { if (hasQueryParser) {
extractedLabelKeys.forEach((key) => { extractedLabelKeys.forEach((key) => {
@ -357,6 +371,18 @@ async function getAfterDistinctCompletions(logQuery: string, dataProvider: Compl
return [...labelCompletions]; return [...labelCompletions];
} }
async function getAfterKeepAndDropCompletions(logQuery: string, dataProvider: CompletionDataProvider) {
const { extractedLabelKeys } = await dataProvider.getParserAndLabelKeys(logQuery);
const labelCompletions: Completion[] = extractedLabelKeys.map((label) => ({
type: 'LABEL_NAME',
label,
insertText: label,
triggerOnInsert: false,
}));
return [...labelCompletions];
}
export async function getCompletions( export async function getCompletions(
situation: Situation, situation: Situation,
dataProvider: CompletionDataProvider dataProvider: CompletionDataProvider
@ -393,6 +419,8 @@ export async function getCompletions(
return [...FUNCTION_COMPLETIONS, ...AGGREGATION_COMPLETIONS]; return [...FUNCTION_COMPLETIONS, ...AGGREGATION_COMPLETIONS];
case 'AFTER_DISTINCT': case 'AFTER_DISTINCT':
return getAfterDistinctCompletions(situation.logQuery, dataProvider); return getAfterDistinctCompletions(situation.logQuery, dataProvider);
case 'AFTER_KEEP_AND_DROP':
return getAfterKeepAndDropCompletions(situation.logQuery, dataProvider);
default: default:
throw new NeverCaseError(situation); throw new NeverCaseError(situation);
} }

View File

@ -276,4 +276,36 @@ describe('situation', () => {
logQuery: '{label="value"} | logfmt ', logQuery: '{label="value"} | logfmt ',
}); });
}); });
it('identifies AFTER_KEEP_AND_DROP autocomplete situations', () => {
assertSituation('{label="value"} | logfmt | drop^', {
type: 'AFTER_KEEP_AND_DROP',
logQuery: '{label="value"} | logfmt ',
});
assertSituation('{label="value"} | logfmt | keep^', {
type: 'AFTER_KEEP_AND_DROP',
logQuery: '{label="value"} | logfmt ',
});
assertSituation('{label="value"} | logfmt | drop id,^', {
type: 'AFTER_KEEP_AND_DROP',
logQuery: '{label="value"} | logfmt ',
});
assertSituation('{label="value"} | logfmt | keep id,^', {
type: 'AFTER_KEEP_AND_DROP',
logQuery: '{label="value"} | logfmt ',
});
assertSituation('{label="value"} | logfmt | drop id, name="test",^', {
type: 'AFTER_KEEP_AND_DROP',
logQuery: '{label="value"} | logfmt ',
});
assertSituation('{label="value"} | logfmt | keep id, name="test",^', {
type: 'AFTER_KEEP_AND_DROP',
logQuery: '{label="value"} | logfmt ',
});
});
}); });

View File

@ -22,6 +22,10 @@ import {
UnwrapExpr, UnwrapExpr,
DistinctFilter, DistinctFilter,
DistinctLabel, DistinctLabel,
DropLabelsExpr,
KeepLabelsExpr,
DropLabels,
KeepLabels,
} from '@grafana/lezer-logql'; } from '@grafana/lezer-logql';
import { getLogQueryFromMetricsQuery } from '../../../queryUtils'; import { getLogQueryFromMetricsQuery } from '../../../queryUtils';
@ -131,6 +135,10 @@ export type Situation =
| { | {
type: 'AFTER_DISTINCT'; type: 'AFTER_DISTINCT';
logQuery: string; logQuery: string;
}
| {
type: 'AFTER_KEEP_AND_DROP';
logQuery: string;
}; };
type Resolver = { type Resolver = {
@ -205,6 +213,22 @@ const RESOLVERS: Resolver[] = [
path: [ERROR_NODE_ID, DistinctLabel], path: [ERROR_NODE_ID, DistinctLabel],
fun: resolveAfterDistinct, fun: resolveAfterDistinct,
}, },
{
path: [ERROR_NODE_ID, DropLabelsExpr],
fun: resolveAfterKeepAndDrop,
},
{
path: [ERROR_NODE_ID, DropLabels],
fun: resolveAfterKeepAndDrop,
},
{
path: [ERROR_NODE_ID, KeepLabelsExpr],
fun: resolveAfterKeepAndDrop,
},
{
path: [ERROR_NODE_ID, KeepLabels],
fun: resolveAfterKeepAndDrop,
},
]; ];
const LABEL_OP_MAP = new Map<string, LabelOperator>([ const LABEL_OP_MAP = new Map<string, LabelOperator>([
@ -532,6 +556,28 @@ function resolveAfterDistinct(node: SyntaxNode, text: string, pos: number): Situ
}; };
} }
function resolveAfterKeepAndDrop(node: SyntaxNode, text: string, pos: number): Situation | null {
let logQuery = getLogQueryFromMetricsQuery(text).trim();
let keepAndDropParent: SyntaxNode | null = null;
let parent = node.parent;
while (parent !== null) {
if (parent.type.id === PipelineStage) {
keepAndDropParent = parent;
break;
}
parent = parent.parent;
}
if (keepAndDropParent?.type.id === PipelineStage) {
logQuery = logQuery.slice(0, keepAndDropParent.from);
}
return {
type: 'AFTER_KEEP_AND_DROP',
logQuery,
};
}
// we find the first error-node in the tree that is at the cursor-position. // we find the first error-node in the tree that is at the cursor-position.
// NOTE: this might be too slow, might need to optimize it // NOTE: this might be too slow, might need to optimize it
// (ideas: we do not need to go into every subtree, based on from/to) // (ideas: we do not need to go into every subtree, based on from/to)

View File

@ -508,6 +508,55 @@ Example: \`\`error_level=\`level\` \`\`
explainHandler: () => explainHandler: () =>
'Allows filtering log lines using their original and extracted labels to filter out duplicate label values. The first line occurrence of a distinct value is returned, and the others are dropped.', 'Allows filtering log lines using their original and extracted labels to filter out duplicate label values. The first line occurrence of a distinct value is returned, and the others are dropped.',
}, },
{
id: LokiOperationId.Drop,
name: 'Drop',
params: [
// As drop can support both labels (e.g. job) and expressions (e.g. job="grafana"), we
// use input and not LabelParamEditor.
{
name: 'Label',
type: 'string',
restParam: true,
optional: true,
minWidth: 18,
placeholder: 'job="grafana"',
description: 'Specify labels or expressions to drop.',
},
],
defaultParams: [''],
alternativesKey: 'format',
category: LokiVisualQueryOperationCategory.Formats,
orderRank: LokiOperationOrder.PipeOperations,
renderer: (op, def, innerExpr) => `${innerExpr} | drop ${op.params.join(',')}`,
addOperationHandler: addLokiOperation,
explainHandler: () => 'The drop expression will drop the given labels in the pipeline.',
},
{
id: LokiOperationId.Keep,
name: 'Keep',
params: [
// As keep can support both labels (e.g. job) and expressions (e.g. job="grafana"), we
// use input and not LabelParamEditor.
{
name: 'Label',
type: 'string',
restParam: true,
optional: true,
minWidth: 18,
placeholder: 'job="grafana"',
description: 'Specify labels or expressions to keep.',
},
],
defaultParams: [''],
alternativesKey: 'format',
category: LokiVisualQueryOperationCategory.Formats,
orderRank: LokiOperationOrder.PipeOperations,
renderer: (op, def, innerExpr) => `${innerExpr} | keep ${op.params.join(',')}`,
addOperationHandler: addLokiOperation,
explainHandler: () =>
'The keep expression will keep only the specified labels in the pipeline and drop all the other labels.',
},
...binaryScalarOperations, ...binaryScalarOperations,
{ {
id: LokiOperationId.NestedQuery, id: LokiOperationId.NestedQuery,

View File

@ -776,6 +776,96 @@ describe('buildVisualQueryFromString', () => {
}) })
); );
}); });
it('parses a log query with drop and no labels', () => {
expect(buildVisualQueryFromString('{app="frontend"} | drop')).toEqual(
noErrors({
labels: [
{
op: '=',
value: 'frontend',
label: 'app',
},
],
operations: [{ id: LokiOperationId.Drop, params: [] }],
})
);
});
it('parses a log query with drop and labels', () => {
expect(buildVisualQueryFromString('{app="frontend"} | drop id, email')).toEqual(
noErrors({
labels: [
{
op: '=',
value: 'frontend',
label: 'app',
},
],
operations: [{ id: LokiOperationId.Drop, params: ['id', 'email'] }],
})
);
});
it('parses a log query with drop, labels and expressions', () => {
expect(buildVisualQueryFromString('{app="frontend"} | drop id, email, test="test1"')).toEqual(
noErrors({
labels: [
{
op: '=',
value: 'frontend',
label: 'app',
},
],
operations: [{ id: LokiOperationId.Drop, params: ['id', 'email', 'test="test1"'] }],
})
);
});
it('parses a log query with keep and no labels', () => {
expect(buildVisualQueryFromString('{app="frontend"} | keep')).toEqual(
noErrors({
labels: [
{
op: '=',
value: 'frontend',
label: 'app',
},
],
operations: [{ id: LokiOperationId.Keep, params: [] }],
})
);
});
it('parses a log query with keep and labels', () => {
expect(buildVisualQueryFromString('{app="frontend"} | keep id, email')).toEqual(
noErrors({
labels: [
{
op: '=',
value: 'frontend',
label: 'app',
},
],
operations: [{ id: LokiOperationId.Keep, params: ['id', 'email'] }],
})
);
});
it('parses a log query with keep, labels and expressions', () => {
expect(buildVisualQueryFromString('{app="frontend"} | keep id, email, test="test1"')).toEqual(
noErrors({
labels: [
{
op: '=',
value: 'frontend',
label: 'app',
},
],
operations: [{ id: LokiOperationId.Keep, params: ['id', 'email', 'test="test1"'] }],
})
);
});
}); });
function noErrors(query: LokiVisualQuery) { function noErrors(query: LokiVisualQuery) {

View File

@ -10,6 +10,9 @@ import {
Decolorize, Decolorize,
DistinctFilter, DistinctFilter,
DistinctLabel, DistinctLabel,
DropLabel,
DropLabels,
DropLabelsExpr,
Filter, Filter,
FilterOp, FilterOp,
Grouping, Grouping,
@ -21,6 +24,9 @@ import {
Json, Json,
JsonExpression, JsonExpression,
JsonExpressionParser, JsonExpressionParser,
KeepLabel,
KeepLabels,
KeepLabelsExpr,
LabelFilter, LabelFilter,
LabelFormatMatcher, LabelFormatMatcher,
LabelParser, LabelParser,
@ -212,6 +218,16 @@ export function handleExpression(expr: string, node: SyntaxNode, context: Contex
break; break;
} }
case DropLabelsExpr: {
visQuery.operations.push(handleDropFilter(expr, node, context));
break;
}
case KeepLabelsExpr: {
visQuery.operations.push(handleKeepFilter(expr, node, context));
break;
}
default: { default: {
// Any other nodes we just ignore and go to its children. This should be fine as there are lots of wrapper // Any other nodes we just ignore and go to its children. This should be fine as there are lots of wrapper
// nodes that can be skipped. // nodes that can be skipped.
@ -660,3 +676,37 @@ function handleDistinctFilter(expr: string, node: SyntaxNode, context: Context):
params: labels, params: labels,
}; };
} }
function handleDropFilter(expr: string, node: SyntaxNode, context: Context): QueryBuilderOperation {
const labels: string[] = [];
let exploringNode = node.getChild(DropLabels);
while (exploringNode) {
const label = getString(expr, exploringNode.getChild(DropLabel));
if (label) {
labels.push(label);
}
exploringNode = exploringNode?.getChild(DropLabels);
}
labels.reverse();
return {
id: LokiOperationId.Drop,
params: labels,
};
}
function handleKeepFilter(expr: string, node: SyntaxNode, context: Context): QueryBuilderOperation {
const labels: string[] = [];
let exploringNode = node.getChild(KeepLabels);
while (exploringNode) {
const label = getString(expr, exploringNode.getChild(KeepLabel));
if (label) {
labels.push(label);
}
exploringNode = exploringNode?.getChild(KeepLabels);
}
labels.reverse();
return {
id: LokiOperationId.Keep,
params: labels,
};
}

View File

@ -42,6 +42,8 @@ export enum LokiOperationId {
LineFormat = 'line_format', LineFormat = 'line_format',
LabelFormat = 'label_format', LabelFormat = 'label_format',
Decolorize = 'decolorize', Decolorize = 'decolorize',
Drop = 'drop',
Keep = 'keep',
Rate = 'rate', Rate = 'rate',
RateCounter = 'rate_counter', RateCounter = 'rate_counter',
CountOverTime = 'count_over_time', CountOverTime = 'count_over_time',

View File

@ -3857,14 +3857,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@grafana/lezer-logql@npm:0.1.8": "@grafana/lezer-logql@npm:0.1.9":
version: 0.1.8 version: 0.1.9
resolution: "@grafana/lezer-logql@npm:0.1.8" resolution: "@grafana/lezer-logql@npm:0.1.9"
dependencies: dependencies:
lodash: ^4.17.21 lodash: ^4.17.21
peerDependencies: peerDependencies:
"@lezer/lr": ^1.0.0 "@lezer/lr": ^1.0.0
checksum: f0f301b6d4fbd2d79563b5b4e34303257be0ea995b2b9fa1f012648654b4afaa9cea91642bc59eddb70e9fa24ec8804489c161f7065b41eef49db68d3a2ca561 checksum: 6cceb18586413864137ef2305bbe2fdce054796c61a3fde4864e3f2d30ea3dac853aa701728867bf609a4009334dbf92fddbc06f7291dbca9459a86c9dc29e67
languageName: node languageName: node
linkType: hard linkType: hard
@ -19244,7 +19244,7 @@ __metadata:
"@grafana/faro-core": 1.1.2 "@grafana/faro-core": 1.1.2
"@grafana/faro-web-sdk": 1.1.2 "@grafana/faro-web-sdk": 1.1.2
"@grafana/google-sdk": 0.1.1 "@grafana/google-sdk": 0.1.1
"@grafana/lezer-logql": 0.1.8 "@grafana/lezer-logql": 0.1.9
"@grafana/lezer-traceql": 0.0.4 "@grafana/lezer-traceql": 0.0.4
"@grafana/monaco-logql": ^0.0.7 "@grafana/monaco-logql": ^0.0.7
"@grafana/runtime": "workspace:*" "@grafana/runtime": "workspace:*"