123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- import { SyntaxNode } from '@lezer/common';
- import { parser } from 'lezer-promql';
- import { binaryScalarOperatorToOperatorName } from './binaryScalarOperations';
- import {
- ErrorName,
- getAllByType,
- getLeftMostChild,
- getString,
- makeBinOp,
- makeError,
- replaceVariables,
- } from './shared/parsingUtils';
- import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types';
- import { PromVisualQuery, PromVisualQueryBinary } from './types';
- /**
- * Parses a PromQL query into a visual query model.
- *
- * It traverses the tree and uses sort of state machine to update the query model. The query model is modified
- * during the traversal and sent to each handler as context.
- *
- * @param expr
- */
- export function buildVisualQueryFromString(expr: string): Context {
- const replacedExpr = replaceVariables(expr);
- const tree = parser.parse(replacedExpr);
- const node = tree.topNode;
- // This will be modified in the handlers.
- const visQuery: PromVisualQuery = {
- metric: '',
- labels: [],
- operations: [],
- };
- const context: Context = {
- query: visQuery,
- errors: [],
- };
- try {
- handleExpression(replacedExpr, node, context);
- } catch (err) {
- // Not ideal to log it here, but otherwise we would lose the stack trace.
- console.error(err);
- context.errors.push({
- text: err.message,
- });
- }
- // If we have empty query, we want to reset errors
- if (isEmptyQuery(context.query)) {
- context.errors = [];
- }
- return context;
- }
- interface ParsingError {
- text: string;
- from?: number;
- to?: number;
- parentType?: string;
- }
- interface Context {
- query: PromVisualQuery;
- errors: ParsingError[];
- }
- /**
- * Handler for default state. It will traverse the tree and call the appropriate handler for each node. The node
- * handled here does not necessarily need to be of type == Expr.
- * @param expr
- * @param node
- * @param context
- */
- export function handleExpression(expr: string, node: SyntaxNode, context: Context) {
- const visQuery = context.query;
- switch (node.name) {
- case 'MetricIdentifier': {
- // Expectation is that there is only one of those per query.
- visQuery.metric = getString(expr, node);
- break;
- }
- case 'LabelMatcher': {
- // Same as MetricIdentifier should be just one per query.
- visQuery.labels.push(getLabel(expr, node));
- const err = node.getChild(ErrorName);
- if (err) {
- context.errors.push(makeError(expr, err));
- }
- break;
- }
- case 'FunctionCall': {
- handleFunction(expr, node, context);
- break;
- }
- case 'AggregateExpr': {
- handleAggregation(expr, node, context);
- break;
- }
- case 'BinaryExpr': {
- handleBinary(expr, node, context);
- break;
- }
- case ErrorName: {
- if (isIntervalVariableError(node)) {
- break;
- }
- context.errors.push(makeError(expr, node));
- break;
- }
- default: {
- if (node.name === 'ParenExpr') {
- // We don't support parenthesis in the query to group expressions. We just report error but go on with the
- // parsing.
- context.errors.push(makeError(expr, node));
- }
- // Any other nodes we just ignore and go to it's children. This should be fine as there are lot's of wrapper
- // nodes that can be skipped.
- // TODO: there are probably cases where we will just skip nodes we don't support and we should be able to
- // detect those and report back.
- let child = node.firstChild;
- while (child) {
- handleExpression(expr, child, context);
- child = child.nextSibling;
- }
- }
- }
- }
- function isIntervalVariableError(node: SyntaxNode) {
- return node.prevSibling?.name === 'Expr' && node.prevSibling?.firstChild?.name === 'VectorSelector';
- }
- function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter {
- const label = getString(expr, node.getChild('LabelName'));
- const op = getString(expr, node.getChild('MatchOp'));
- const value = getString(expr, node.getChild('StringLiteral')).replace(/"/g, '');
- return {
- label,
- op,
- value,
- };
- }
- const rangeFunctions = ['changes', 'rate', 'irate', 'increase', 'delta'];
- /**
- * Handle function call which is usually and identifier and its body > arguments.
- * @param expr
- * @param node
- * @param context
- */
- function handleFunction(expr: string, node: SyntaxNode, context: Context) {
- const visQuery = context.query;
- const nameNode = node.getChild('FunctionIdentifier');
- const funcName = getString(expr, nameNode);
- const body = node.getChild('FunctionCallBody');
- const callArgs = body!.getChild('FunctionCallArgs');
- const params = [];
- let interval = '';
- // This is a bit of a shortcut to get the interval argument. Reasons are
- // - interval is not part of the function args per promQL grammar but we model it as argument for the function in
- // the query model.
- // - it is easier to handle template variables this way as template variable is an error for the parser
- if (rangeFunctions.includes(funcName) || funcName.endsWith('_over_time')) {
- let match = getString(expr, node).match(/\[(.+)\]/);
- if (match?.[1]) {
- interval = match[1];
- params.push(match[1]);
- }
- }
- const op = { id: funcName, params };
- // We unshift operations to keep the more natural order that we want to have in the visual query editor.
- visQuery.operations.unshift(op);
- if (callArgs) {
- if (getString(expr, callArgs) === interval + ']') {
- // This is a special case where we have a function with a single argument and it is the interval.
- // This happens when you start adding operations in query builder and did not set a metric yet.
- return;
- }
- updateFunctionArgs(expr, callArgs, context, op);
- }
- }
- /**
- * Handle aggregation as they are distinct type from other functions.
- * @param expr
- * @param node
- * @param context
- */
- function handleAggregation(expr: string, node: SyntaxNode, context: Context) {
- const visQuery = context.query;
- const nameNode = node.getChild('AggregateOp');
- let funcName = getString(expr, nameNode);
- const modifier = node.getChild('AggregateModifier');
- const labels = [];
- if (modifier) {
- const byModifier = modifier.getChild(`By`);
- if (byModifier && funcName) {
- funcName = `__${funcName}_by`;
- }
- const withoutModifier = modifier.getChild(`Without`);
- if (withoutModifier) {
- funcName = `__${funcName}_without`;
- }
- labels.push(...getAllByType(expr, modifier, 'GroupingLabel'));
- }
- const body = node.getChild('FunctionCallBody');
- const callArgs = body!.getChild('FunctionCallArgs');
- const op: QueryBuilderOperation = { id: funcName, params: [] };
- visQuery.operations.unshift(op);
- updateFunctionArgs(expr, callArgs, context, op);
- // We add labels after params in the visual query editor.
- op.params.push(...labels);
- }
- /**
- * Handle (probably) all types of arguments that function or aggregation can have.
- *
- * FunctionCallArgs are nested bit weirdly basically its [firstArg, ...rest] where rest is again FunctionCallArgs so
- * we cannot just get all the children and iterate them as arguments we have to again recursively traverse through
- * them.
- *
- * @param expr
- * @param node
- * @param context
- * @param op - We need the operation to add the params to as an additional context.
- */
- function updateFunctionArgs(expr: string, node: SyntaxNode | null, context: Context, op: QueryBuilderOperation) {
- if (!node) {
- return;
- }
- switch (node.name) {
- // In case we have an expression we don't know what kind so we have to look at the child as it can be anything.
- case 'Expr':
- // FunctionCallArgs are nested bit weirdly as mentioned so we have to go one deeper in this case.
- case 'FunctionCallArgs': {
- let child = node.firstChild;
- while (child) {
- updateFunctionArgs(expr, child, context, op);
- child = child.nextSibling;
- }
- break;
- }
- case 'NumberLiteral': {
- op.params.push(parseFloat(getString(expr, node)));
- break;
- }
- case 'StringLiteral': {
- op.params.push(getString(expr, node).replace(/"/g, ''));
- break;
- }
- default: {
- // Means we get to something that does not seem like simple function arg and is probably nested query so jump
- // back to main context
- handleExpression(expr, node, context);
- }
- }
- }
- /**
- * Right now binary expressions can be represented in 2 way in visual query. As additional operation in case it is
- * just operation with scalar or it creates a binaryQuery when it's 2 queries.
- * @param expr
- * @param node
- * @param context
- */
- function handleBinary(expr: string, node: SyntaxNode, context: Context) {
- const visQuery = context.query;
- const left = node.firstChild!;
- const op = getString(expr, left.nextSibling);
- const binModifier = getBinaryModifier(expr, node.getChild('BinModifiers'));
- const right = node.lastChild!;
- const opDef = binaryScalarOperatorToOperatorName[op];
- const leftNumber = left.getChild('NumberLiteral');
- const rightNumber = right.getChild('NumberLiteral');
- const rightBinary = right.getChild('BinaryExpr');
- if (leftNumber) {
- // TODO: this should be already handled in case parent is binary expression as it has to be added to parent
- // if query starts with a number that isn't handled now.
- } else {
- // If this is binary we don't really know if there is a query or just chained scalars. So
- // we have to traverse a bit deeper to know
- handleExpression(expr, left, context);
- }
- if (rightNumber) {
- visQuery.operations.push(makeBinOp(opDef, expr, right, !!binModifier?.isBool));
- } else if (rightBinary) {
- // Due to the way binary ops are parsed we can get a binary operation on the right that starts with a number which
- // is a factor for a current binary operation. So we have to add it as an operation now.
- const leftMostChild = getLeftMostChild(right);
- if (leftMostChild?.name === 'NumberLiteral') {
- visQuery.operations.push(makeBinOp(opDef, expr, leftMostChild, !!binModifier?.isBool));
- }
- // If we added the first number literal as operation here we still can continue and handle the rest as the first
- // number will be just skipped.
- handleExpression(expr, right, context);
- } else {
- visQuery.binaryQueries = visQuery.binaryQueries || [];
- const binQuery: PromVisualQueryBinary = {
- operator: op,
- query: {
- metric: '',
- labels: [],
- operations: [],
- },
- };
- if (binModifier?.isMatcher) {
- binQuery.vectorMatchesType = binModifier.matchType;
- binQuery.vectorMatches = binModifier.matches;
- }
- visQuery.binaryQueries.push(binQuery);
- handleExpression(expr, right, {
- query: binQuery.query,
- errors: context.errors,
- });
- }
- }
- function getBinaryModifier(
- expr: string,
- node: SyntaxNode | null
- ):
- | { isBool: true; isMatcher: false }
- | { isBool: false; isMatcher: true; matches: string; matchType: 'ignoring' | 'on' }
- | undefined {
- if (!node) {
- return undefined;
- }
- if (node.getChild('Bool')) {
- return { isBool: true, isMatcher: false };
- } else {
- const matcher = node.getChild('OnOrIgnoring');
- if (!matcher) {
- // Not sure what this could be, maybe should be an error.
- return undefined;
- }
- const labels = getString(expr, matcher.getChild('GroupingLabels')?.getChild('GroupingLabelList'));
- return {
- isMatcher: true,
- isBool: false,
- matches: labels,
- matchType: matcher.getChild('On') ? 'on' : 'ignoring',
- };
- }
- }
- function isEmptyQuery(query: PromVisualQuery) {
- if (query.labels.length === 0 && query.operations.length === 0 && !query.metric) {
- return true;
- }
- return false;
- }
|