parsing.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. import { SyntaxNode } from '@lezer/common';
  2. import { parser } from '@grafana/lezer-logql';
  3. import {
  4. ErrorName,
  5. getAllByType,
  6. getLeftMostChild,
  7. getString,
  8. makeBinOp,
  9. makeError,
  10. replaceVariables,
  11. } from '../../prometheus/querybuilder/shared/parsingUtils';
  12. import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../../prometheus/querybuilder/shared/types';
  13. import { binaryScalarDefs } from './binaryScalarOperations';
  14. import { LokiVisualQuery, LokiVisualQueryBinary } from './types';
  15. interface Context {
  16. query: LokiVisualQuery;
  17. errors: ParsingError[];
  18. }
  19. interface ParsingError {
  20. text: string;
  21. from?: number;
  22. to?: number;
  23. parentType?: string;
  24. }
  25. export function buildVisualQueryFromString(expr: string): Context {
  26. const replacedExpr = replaceVariables(expr);
  27. const tree = parser.parse(replacedExpr);
  28. const node = tree.topNode;
  29. // This will be modified in the handleExpression
  30. const visQuery: LokiVisualQuery = {
  31. labels: [],
  32. operations: [],
  33. };
  34. const context: Context = {
  35. query: visQuery,
  36. errors: [],
  37. };
  38. try {
  39. handleExpression(replacedExpr, node, context);
  40. } catch (err) {
  41. // Not ideal to log it here, but otherwise we would lose the stack trace.
  42. console.error(err);
  43. context.errors.push({
  44. text: err.message,
  45. });
  46. }
  47. // If we have empty query, we want to reset errors
  48. if (isEmptyQuery(context.query)) {
  49. context.errors = [];
  50. }
  51. return context;
  52. }
  53. export function handleExpression(expr: string, node: SyntaxNode, context: Context) {
  54. const visQuery = context.query;
  55. switch (node.name) {
  56. case 'Matcher': {
  57. visQuery.labels.push(getLabel(expr, node));
  58. const err = node.getChild(ErrorName);
  59. if (err) {
  60. context.errors.push(makeError(expr, err));
  61. }
  62. break;
  63. }
  64. case 'LineFilter': {
  65. const { operation, error } = getLineFilter(expr, node);
  66. if (operation) {
  67. visQuery.operations.push(operation);
  68. }
  69. // Show error for query patterns not supported in visual query builder
  70. if (error) {
  71. context.errors.push(createNotSupportedError(expr, node, error));
  72. }
  73. break;
  74. }
  75. case 'LabelParser': {
  76. visQuery.operations.push(getLabelParser(expr, node));
  77. break;
  78. }
  79. case 'LabelFilter': {
  80. const { operation, error } = getLabelFilter(expr, node);
  81. if (operation) {
  82. visQuery.operations.push(operation);
  83. }
  84. // Show error for query patterns not supported in visual query builder
  85. if (error) {
  86. context.errors.push(createNotSupportedError(expr, node, error));
  87. }
  88. break;
  89. }
  90. case 'JsonExpressionParser': {
  91. // JsonExpressionParser is not supported in query builder
  92. const error = 'JsonExpressionParser not supported in visual query builder';
  93. context.errors.push(createNotSupportedError(expr, node, error));
  94. }
  95. case 'LineFormatExpr': {
  96. visQuery.operations.push(getLineFormat(expr, node));
  97. break;
  98. }
  99. case 'LabelFormatMatcher': {
  100. visQuery.operations.push(getLabelFormat(expr, node));
  101. break;
  102. }
  103. case 'UnwrapExpr': {
  104. const { operation, error } = handleUnwrapExpr(expr, node, context);
  105. if (operation) {
  106. visQuery.operations.push(operation);
  107. }
  108. // Show error for query patterns not supported in visual query builder
  109. if (error) {
  110. context.errors.push(createNotSupportedError(expr, node, error));
  111. }
  112. break;
  113. }
  114. case 'RangeAggregationExpr': {
  115. visQuery.operations.push(handleRangeAggregation(expr, node, context));
  116. break;
  117. }
  118. case 'VectorAggregationExpr': {
  119. visQuery.operations.push(handleVectorAggregation(expr, node, context));
  120. break;
  121. }
  122. case 'BinOpExpr': {
  123. handleBinary(expr, node, context);
  124. break;
  125. }
  126. case ErrorName: {
  127. if (isIntervalVariableError(node)) {
  128. break;
  129. }
  130. context.errors.push(makeError(expr, node));
  131. break;
  132. }
  133. default: {
  134. // Any other nodes we just ignore and go to it's children. This should be fine as there are lot's of wrapper
  135. // nodes that can be skipped.
  136. // TODO: there are probably cases where we will just skip nodes we don't support and we should be able to
  137. // detect those and report back.
  138. let child = node.firstChild;
  139. while (child) {
  140. handleExpression(expr, child, context);
  141. child = child.nextSibling;
  142. }
  143. }
  144. }
  145. }
  146. function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter {
  147. const labelNode = node.getChild('Identifier');
  148. const label = getString(expr, labelNode);
  149. const op = getString(expr, labelNode!.nextSibling);
  150. const value = getString(expr, node.getChild('String')).replace(/"/g, '');
  151. return {
  152. label,
  153. op,
  154. value,
  155. };
  156. }
  157. function getLineFilter(expr: string, node: SyntaxNode): { operation?: QueryBuilderOperation; error?: string } {
  158. // Check for nodes not supported in visual builder and return error
  159. const ipLineFilter = getAllByType(expr, node, 'Ip');
  160. if (ipLineFilter.length > 0) {
  161. return {
  162. error: 'Matching ip addresses not supported in query builder',
  163. };
  164. }
  165. const mapFilter: any = {
  166. '|=': '__line_contains',
  167. '!=': '__line_contains_not',
  168. '|~': '__line_matches_regex',
  169. '!~': '"__line_matches_regex"_not',
  170. };
  171. const filter = getString(expr, node.getChild('Filter'));
  172. const filterExpr = handleQuotes(getString(expr, node.getChild('String')));
  173. return {
  174. operation: {
  175. id: mapFilter[filter],
  176. params: [filterExpr],
  177. },
  178. };
  179. }
  180. function getLabelParser(expr: string, node: SyntaxNode): QueryBuilderOperation {
  181. const parserNode = node.firstChild;
  182. const parser = getString(expr, parserNode);
  183. const string = handleQuotes(getString(expr, node.getChild('String')));
  184. const params = !!string ? [string] : [];
  185. return {
  186. id: parser,
  187. params,
  188. };
  189. }
  190. function getLabelFilter(expr: string, node: SyntaxNode): { operation?: QueryBuilderOperation; error?: string } {
  191. // Check for nodes not supported in visual builder and return error
  192. if (node.getChild('Or') || node.getChild('And') || node.getChild('Comma')) {
  193. return {
  194. error: 'Label filter with comma, "and", "or" not supported in query builder',
  195. };
  196. }
  197. if (node.firstChild!.name === 'IpLabelFilter') {
  198. return {
  199. error: 'IpLabelFilter not supported in query builder',
  200. };
  201. }
  202. const id = '__label_filter';
  203. if (node.firstChild!.name === 'UnitFilter') {
  204. const filter = node.firstChild!.firstChild;
  205. const label = filter!.firstChild;
  206. const op = label!.nextSibling;
  207. const value = op!.nextSibling;
  208. const valueString = handleQuotes(getString(expr, value));
  209. return {
  210. operation: {
  211. id,
  212. params: [getString(expr, label), getString(expr, op), valueString],
  213. },
  214. };
  215. }
  216. // In this case it is Matcher or NumberFilter
  217. const filter = node.firstChild;
  218. const label = filter!.firstChild;
  219. const op = label!.nextSibling;
  220. const value = op!.nextSibling;
  221. const params = [getString(expr, label), getString(expr, op), handleQuotes(getString(expr, value))];
  222. // Special case of pipe filtering - no errors
  223. if (params.join('') === `__error__=`) {
  224. return {
  225. operation: {
  226. id: '__label_filter_no_errors',
  227. params: [],
  228. },
  229. };
  230. }
  231. return {
  232. operation: {
  233. id,
  234. params,
  235. },
  236. };
  237. }
  238. function getLineFormat(expr: string, node: SyntaxNode): QueryBuilderOperation {
  239. const id = 'line_format';
  240. const string = handleQuotes(getString(expr, node.getChild('String')));
  241. return {
  242. id,
  243. params: [string],
  244. };
  245. }
  246. function getLabelFormat(expr: string, node: SyntaxNode): QueryBuilderOperation {
  247. const id = 'label_format';
  248. const identifier = node.getChild('Identifier');
  249. const op = identifier!.nextSibling;
  250. const value = op!.nextSibling;
  251. let valueString = handleQuotes(getString(expr, value));
  252. return {
  253. id,
  254. params: [getString(expr, identifier), valueString],
  255. };
  256. }
  257. function handleUnwrapExpr(
  258. expr: string,
  259. node: SyntaxNode,
  260. context: Context
  261. ): { operation?: QueryBuilderOperation; error?: string } {
  262. const unwrapExprChild = node.getChild('UnwrapExpr');
  263. const labelFilterChild = node.getChild('LabelFilter');
  264. const unwrapChild = node.getChild('Unwrap');
  265. if (unwrapExprChild) {
  266. handleExpression(expr, unwrapExprChild, context);
  267. }
  268. if (labelFilterChild) {
  269. handleExpression(expr, labelFilterChild, context);
  270. }
  271. if (unwrapChild) {
  272. if (unwrapChild?.nextSibling?.type.name === 'ConvOp') {
  273. return {
  274. error: 'Unwrap with conversion operator not supported in query builder',
  275. };
  276. }
  277. return {
  278. operation: {
  279. id: 'unwrap',
  280. params: [getString(expr, unwrapChild?.nextSibling)],
  281. },
  282. };
  283. }
  284. return {};
  285. }
  286. function handleRangeAggregation(expr: string, node: SyntaxNode, context: Context) {
  287. const nameNode = node.getChild('RangeOp');
  288. const funcName = getString(expr, nameNode);
  289. const number = node.getChild('Number');
  290. const logExpr = node.getChild('LogRangeExpr');
  291. const params = number !== null && number !== undefined ? [getString(expr, number)] : [];
  292. let match = getString(expr, node).match(/\[(.+)\]/);
  293. if (match?.[1]) {
  294. params.push(match[1]);
  295. }
  296. const op = {
  297. id: funcName,
  298. params,
  299. };
  300. if (logExpr) {
  301. handleExpression(expr, logExpr, context);
  302. }
  303. return op;
  304. }
  305. function handleVectorAggregation(expr: string, node: SyntaxNode, context: Context) {
  306. const nameNode = node.getChild('VectorOp');
  307. let funcName = getString(expr, nameNode);
  308. const grouping = node.getChild('Grouping');
  309. const params = [];
  310. const numberNode = node.getChild('Number');
  311. if (numberNode) {
  312. params.push(Number(getString(expr, numberNode)));
  313. }
  314. if (grouping) {
  315. const byModifier = grouping.getChild(`By`);
  316. if (byModifier && funcName) {
  317. funcName = `__${funcName}_by`;
  318. }
  319. const withoutModifier = grouping.getChild(`Without`);
  320. if (withoutModifier) {
  321. funcName = `__${funcName}_without`;
  322. }
  323. params.push(...getAllByType(expr, grouping, 'Identifier'));
  324. }
  325. const metricExpr = node.getChild('MetricExpr');
  326. const op: QueryBuilderOperation = { id: funcName, params };
  327. if (metricExpr) {
  328. handleExpression(expr, metricExpr, context);
  329. }
  330. return op;
  331. }
  332. const operatorToOpName = binaryScalarDefs.reduce((acc, def) => {
  333. acc[def.sign] = {
  334. id: def.id,
  335. comparison: def.comparison,
  336. };
  337. return acc;
  338. }, {} as Record<string, { id: string; comparison?: boolean }>);
  339. /**
  340. * Right now binary expressions can be represented in 2 way in visual query. As additional operation in case it is
  341. * just operation with scalar or it creates a binaryQuery when it's 2 queries.
  342. * @param expr
  343. * @param node
  344. * @param context
  345. */
  346. function handleBinary(expr: string, node: SyntaxNode, context: Context) {
  347. const visQuery = context.query;
  348. const left = node.firstChild!;
  349. const op = getString(expr, left.nextSibling);
  350. const binModifier = getBinaryModifier(expr, node.getChild('BinModifiers'));
  351. const right = node.lastChild!;
  352. const opDef = operatorToOpName[op];
  353. const leftNumber = getLastChildWithSelector(left, 'MetricExpr.LiteralExpr.Number');
  354. const rightNumber = getLastChildWithSelector(right, 'MetricExpr.LiteralExpr.Number');
  355. const rightBinary = right.getChild('BinOpExpr');
  356. if (leftNumber) {
  357. // TODO: this should be already handled in case parent is binary expression as it has to be added to parent
  358. // if query starts with a number that isn't handled now.
  359. } else {
  360. // If this is binary we don't really know if there is a query or just chained scalars. So
  361. // we have to traverse a bit deeper to know
  362. handleExpression(expr, left, context);
  363. }
  364. if (rightNumber) {
  365. visQuery.operations.push(makeBinOp(opDef, expr, right, !!binModifier?.isBool));
  366. } else if (rightBinary) {
  367. // Due to the way binary ops are parsed we can get a binary operation on the right that starts with a number which
  368. // is a factor for a current binary operation. So we have to add it as an operation now.
  369. const leftMostChild = getLeftMostChild(right);
  370. if (leftMostChild?.name === 'Number') {
  371. visQuery.operations.push(makeBinOp(opDef, expr, leftMostChild, !!binModifier?.isBool));
  372. }
  373. // If we added the first number literal as operation here we still can continue and handle the rest as the first
  374. // number will be just skipped.
  375. handleExpression(expr, right, context);
  376. } else {
  377. visQuery.binaryQueries = visQuery.binaryQueries || [];
  378. const binQuery: LokiVisualQueryBinary = {
  379. operator: op,
  380. query: {
  381. labels: [],
  382. operations: [],
  383. },
  384. };
  385. if (binModifier?.isMatcher) {
  386. binQuery.vectorMatchesType = binModifier.matchType;
  387. binQuery.vectorMatches = binModifier.matches;
  388. }
  389. visQuery.binaryQueries.push(binQuery);
  390. handleExpression(expr, right, {
  391. query: binQuery.query,
  392. errors: context.errors,
  393. });
  394. }
  395. }
  396. function getBinaryModifier(
  397. expr: string,
  398. node: SyntaxNode | null
  399. ):
  400. | { isBool: true; isMatcher: false }
  401. | { isBool: false; isMatcher: true; matches: string; matchType: 'ignoring' | 'on' }
  402. | undefined {
  403. if (!node) {
  404. return undefined;
  405. }
  406. if (node.getChild('Bool')) {
  407. return { isBool: true, isMatcher: false };
  408. } else {
  409. const matcher = node.getChild('OnOrIgnoring');
  410. if (!matcher) {
  411. // Not sure what this could be, maybe should be an error.
  412. return undefined;
  413. }
  414. const labels = getString(expr, matcher.getChild('GroupingLabels')?.getChild('GroupingLabelList'));
  415. return {
  416. isMatcher: true,
  417. isBool: false,
  418. matches: labels,
  419. matchType: matcher.getChild('On') ? 'on' : 'ignoring',
  420. };
  421. }
  422. }
  423. function isIntervalVariableError(node: SyntaxNode) {
  424. return node?.parent?.name === 'Range';
  425. }
  426. function handleQuotes(string: string) {
  427. if (string[0] === `"` && string[string.length - 1] === `"`) {
  428. return string.replace(/"/g, '').replace(/\\\\/g, '\\');
  429. }
  430. return string.replace(/`/g, '');
  431. }
  432. /**
  433. * Simple helper to traverse the syntax tree. Instead of node.getChild('foo')?.getChild('bar')?.getChild('baz') you
  434. * can write getChildWithSelector(node, 'foo.bar.baz')
  435. * @param node
  436. * @param selector
  437. */
  438. function getLastChildWithSelector(node: SyntaxNode, selector: string) {
  439. let child: SyntaxNode | null = node;
  440. const children = selector.split('.');
  441. for (const s of children) {
  442. child = child.getChild(s);
  443. if (!child) {
  444. return null;
  445. }
  446. }
  447. return child;
  448. }
  449. /**
  450. * Helper function to enrich error text with information that visual query builder doesn't support that logQL
  451. * @param expr
  452. * @param node
  453. * @param error
  454. */
  455. function createNotSupportedError(expr: string, node: SyntaxNode, error: string) {
  456. const err = makeError(expr, node);
  457. err.text = `${error}: ${err.text}`;
  458. return err;
  459. }
  460. function isEmptyQuery(query: LokiVisualQuery) {
  461. if (query.labels.length === 0 && query.operations.length === 0) {
  462. return true;
  463. }
  464. return false;
  465. }