parsing.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import { SyntaxNode } from '@lezer/common';
  2. import { parser } from 'lezer-promql';
  3. import { binaryScalarOperatorToOperatorName } from './binaryScalarOperations';
  4. import {
  5. ErrorName,
  6. getAllByType,
  7. getLeftMostChild,
  8. getString,
  9. makeBinOp,
  10. makeError,
  11. replaceVariables,
  12. } from './shared/parsingUtils';
  13. import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types';
  14. import { PromVisualQuery, PromVisualQueryBinary } from './types';
  15. /**
  16. * Parses a PromQL query into a visual query model.
  17. *
  18. * It traverses the tree and uses sort of state machine to update the query model. The query model is modified
  19. * during the traversal and sent to each handler as context.
  20. *
  21. * @param expr
  22. */
  23. export function buildVisualQueryFromString(expr: string): Context {
  24. const replacedExpr = replaceVariables(expr);
  25. const tree = parser.parse(replacedExpr);
  26. const node = tree.topNode;
  27. // This will be modified in the handlers.
  28. const visQuery: PromVisualQuery = {
  29. metric: '',
  30. labels: [],
  31. operations: [],
  32. };
  33. const context: Context = {
  34. query: visQuery,
  35. errors: [],
  36. };
  37. try {
  38. handleExpression(replacedExpr, node, context);
  39. } catch (err) {
  40. // Not ideal to log it here, but otherwise we would lose the stack trace.
  41. console.error(err);
  42. context.errors.push({
  43. text: err.message,
  44. });
  45. }
  46. // If we have empty query, we want to reset errors
  47. if (isEmptyQuery(context.query)) {
  48. context.errors = [];
  49. }
  50. return context;
  51. }
  52. interface ParsingError {
  53. text: string;
  54. from?: number;
  55. to?: number;
  56. parentType?: string;
  57. }
  58. interface Context {
  59. query: PromVisualQuery;
  60. errors: ParsingError[];
  61. }
  62. /**
  63. * Handler for default state. It will traverse the tree and call the appropriate handler for each node. The node
  64. * handled here does not necessarily need to be of type == Expr.
  65. * @param expr
  66. * @param node
  67. * @param context
  68. */
  69. export function handleExpression(expr: string, node: SyntaxNode, context: Context) {
  70. const visQuery = context.query;
  71. switch (node.name) {
  72. case 'MetricIdentifier': {
  73. // Expectation is that there is only one of those per query.
  74. visQuery.metric = getString(expr, node);
  75. break;
  76. }
  77. case 'LabelMatcher': {
  78. // Same as MetricIdentifier should be just one per query.
  79. visQuery.labels.push(getLabel(expr, node));
  80. const err = node.getChild(ErrorName);
  81. if (err) {
  82. context.errors.push(makeError(expr, err));
  83. }
  84. break;
  85. }
  86. case 'FunctionCall': {
  87. handleFunction(expr, node, context);
  88. break;
  89. }
  90. case 'AggregateExpr': {
  91. handleAggregation(expr, node, context);
  92. break;
  93. }
  94. case 'BinaryExpr': {
  95. handleBinary(expr, node, context);
  96. break;
  97. }
  98. case ErrorName: {
  99. if (isIntervalVariableError(node)) {
  100. break;
  101. }
  102. context.errors.push(makeError(expr, node));
  103. break;
  104. }
  105. default: {
  106. if (node.name === 'ParenExpr') {
  107. // We don't support parenthesis in the query to group expressions. We just report error but go on with the
  108. // parsing.
  109. context.errors.push(makeError(expr, node));
  110. }
  111. // Any other nodes we just ignore and go to it's children. This should be fine as there are lot's of wrapper
  112. // nodes that can be skipped.
  113. // TODO: there are probably cases where we will just skip nodes we don't support and we should be able to
  114. // detect those and report back.
  115. let child = node.firstChild;
  116. while (child) {
  117. handleExpression(expr, child, context);
  118. child = child.nextSibling;
  119. }
  120. }
  121. }
  122. }
  123. function isIntervalVariableError(node: SyntaxNode) {
  124. return node.prevSibling?.name === 'Expr' && node.prevSibling?.firstChild?.name === 'VectorSelector';
  125. }
  126. function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter {
  127. const label = getString(expr, node.getChild('LabelName'));
  128. const op = getString(expr, node.getChild('MatchOp'));
  129. const value = getString(expr, node.getChild('StringLiteral')).replace(/"/g, '');
  130. return {
  131. label,
  132. op,
  133. value,
  134. };
  135. }
  136. const rangeFunctions = ['changes', 'rate', 'irate', 'increase', 'delta'];
  137. /**
  138. * Handle function call which is usually and identifier and its body > arguments.
  139. * @param expr
  140. * @param node
  141. * @param context
  142. */
  143. function handleFunction(expr: string, node: SyntaxNode, context: Context) {
  144. const visQuery = context.query;
  145. const nameNode = node.getChild('FunctionIdentifier');
  146. const funcName = getString(expr, nameNode);
  147. const body = node.getChild('FunctionCallBody');
  148. const callArgs = body!.getChild('FunctionCallArgs');
  149. const params = [];
  150. let interval = '';
  151. // This is a bit of a shortcut to get the interval argument. Reasons are
  152. // - interval is not part of the function args per promQL grammar but we model it as argument for the function in
  153. // the query model.
  154. // - it is easier to handle template variables this way as template variable is an error for the parser
  155. if (rangeFunctions.includes(funcName) || funcName.endsWith('_over_time')) {
  156. let match = getString(expr, node).match(/\[(.+)\]/);
  157. if (match?.[1]) {
  158. interval = match[1];
  159. params.push(match[1]);
  160. }
  161. }
  162. const op = { id: funcName, params };
  163. // We unshift operations to keep the more natural order that we want to have in the visual query editor.
  164. visQuery.operations.unshift(op);
  165. if (callArgs) {
  166. if (getString(expr, callArgs) === interval + ']') {
  167. // This is a special case where we have a function with a single argument and it is the interval.
  168. // This happens when you start adding operations in query builder and did not set a metric yet.
  169. return;
  170. }
  171. updateFunctionArgs(expr, callArgs, context, op);
  172. }
  173. }
  174. /**
  175. * Handle aggregation as they are distinct type from other functions.
  176. * @param expr
  177. * @param node
  178. * @param context
  179. */
  180. function handleAggregation(expr: string, node: SyntaxNode, context: Context) {
  181. const visQuery = context.query;
  182. const nameNode = node.getChild('AggregateOp');
  183. let funcName = getString(expr, nameNode);
  184. const modifier = node.getChild('AggregateModifier');
  185. const labels = [];
  186. if (modifier) {
  187. const byModifier = modifier.getChild(`By`);
  188. if (byModifier && funcName) {
  189. funcName = `__${funcName}_by`;
  190. }
  191. const withoutModifier = modifier.getChild(`Without`);
  192. if (withoutModifier) {
  193. funcName = `__${funcName}_without`;
  194. }
  195. labels.push(...getAllByType(expr, modifier, 'GroupingLabel'));
  196. }
  197. const body = node.getChild('FunctionCallBody');
  198. const callArgs = body!.getChild('FunctionCallArgs');
  199. const op: QueryBuilderOperation = { id: funcName, params: [] };
  200. visQuery.operations.unshift(op);
  201. updateFunctionArgs(expr, callArgs, context, op);
  202. // We add labels after params in the visual query editor.
  203. op.params.push(...labels);
  204. }
  205. /**
  206. * Handle (probably) all types of arguments that function or aggregation can have.
  207. *
  208. * FunctionCallArgs are nested bit weirdly basically its [firstArg, ...rest] where rest is again FunctionCallArgs so
  209. * we cannot just get all the children and iterate them as arguments we have to again recursively traverse through
  210. * them.
  211. *
  212. * @param expr
  213. * @param node
  214. * @param context
  215. * @param op - We need the operation to add the params to as an additional context.
  216. */
  217. function updateFunctionArgs(expr: string, node: SyntaxNode | null, context: Context, op: QueryBuilderOperation) {
  218. if (!node) {
  219. return;
  220. }
  221. switch (node.name) {
  222. // 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.
  223. case 'Expr':
  224. // FunctionCallArgs are nested bit weirdly as mentioned so we have to go one deeper in this case.
  225. case 'FunctionCallArgs': {
  226. let child = node.firstChild;
  227. while (child) {
  228. updateFunctionArgs(expr, child, context, op);
  229. child = child.nextSibling;
  230. }
  231. break;
  232. }
  233. case 'NumberLiteral': {
  234. op.params.push(parseFloat(getString(expr, node)));
  235. break;
  236. }
  237. case 'StringLiteral': {
  238. op.params.push(getString(expr, node).replace(/"/g, ''));
  239. break;
  240. }
  241. default: {
  242. // Means we get to something that does not seem like simple function arg and is probably nested query so jump
  243. // back to main context
  244. handleExpression(expr, node, context);
  245. }
  246. }
  247. }
  248. /**
  249. * Right now binary expressions can be represented in 2 way in visual query. As additional operation in case it is
  250. * just operation with scalar or it creates a binaryQuery when it's 2 queries.
  251. * @param expr
  252. * @param node
  253. * @param context
  254. */
  255. function handleBinary(expr: string, node: SyntaxNode, context: Context) {
  256. const visQuery = context.query;
  257. const left = node.firstChild!;
  258. const op = getString(expr, left.nextSibling);
  259. const binModifier = getBinaryModifier(expr, node.getChild('BinModifiers'));
  260. const right = node.lastChild!;
  261. const opDef = binaryScalarOperatorToOperatorName[op];
  262. const leftNumber = left.getChild('NumberLiteral');
  263. const rightNumber = right.getChild('NumberLiteral');
  264. const rightBinary = right.getChild('BinaryExpr');
  265. if (leftNumber) {
  266. // TODO: this should be already handled in case parent is binary expression as it has to be added to parent
  267. // if query starts with a number that isn't handled now.
  268. } else {
  269. // If this is binary we don't really know if there is a query or just chained scalars. So
  270. // we have to traverse a bit deeper to know
  271. handleExpression(expr, left, context);
  272. }
  273. if (rightNumber) {
  274. visQuery.operations.push(makeBinOp(opDef, expr, right, !!binModifier?.isBool));
  275. } else if (rightBinary) {
  276. // Due to the way binary ops are parsed we can get a binary operation on the right that starts with a number which
  277. // is a factor for a current binary operation. So we have to add it as an operation now.
  278. const leftMostChild = getLeftMostChild(right);
  279. if (leftMostChild?.name === 'NumberLiteral') {
  280. visQuery.operations.push(makeBinOp(opDef, expr, leftMostChild, !!binModifier?.isBool));
  281. }
  282. // If we added the first number literal as operation here we still can continue and handle the rest as the first
  283. // number will be just skipped.
  284. handleExpression(expr, right, context);
  285. } else {
  286. visQuery.binaryQueries = visQuery.binaryQueries || [];
  287. const binQuery: PromVisualQueryBinary = {
  288. operator: op,
  289. query: {
  290. metric: '',
  291. labels: [],
  292. operations: [],
  293. },
  294. };
  295. if (binModifier?.isMatcher) {
  296. binQuery.vectorMatchesType = binModifier.matchType;
  297. binQuery.vectorMatches = binModifier.matches;
  298. }
  299. visQuery.binaryQueries.push(binQuery);
  300. handleExpression(expr, right, {
  301. query: binQuery.query,
  302. errors: context.errors,
  303. });
  304. }
  305. }
  306. function getBinaryModifier(
  307. expr: string,
  308. node: SyntaxNode | null
  309. ):
  310. | { isBool: true; isMatcher: false }
  311. | { isBool: false; isMatcher: true; matches: string; matchType: 'ignoring' | 'on' }
  312. | undefined {
  313. if (!node) {
  314. return undefined;
  315. }
  316. if (node.getChild('Bool')) {
  317. return { isBool: true, isMatcher: false };
  318. } else {
  319. const matcher = node.getChild('OnOrIgnoring');
  320. if (!matcher) {
  321. // Not sure what this could be, maybe should be an error.
  322. return undefined;
  323. }
  324. const labels = getString(expr, matcher.getChild('GroupingLabels')?.getChild('GroupingLabelList'));
  325. return {
  326. isMatcher: true,
  327. isBool: false,
  328. matches: labels,
  329. matchType: matcher.getChild('On') ? 'on' : 'ignoring',
  330. };
  331. }
  332. }
  333. function isEmptyQuery(query: PromVisualQuery) {
  334. if (query.labels.length === 0 && query.operations.length === 0 && !query.metric) {
  335. return true;
  336. }
  337. return false;
  338. }