parsing.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import { buildVisualQueryFromString } from './parsing';
  2. import { LokiVisualQuery } from './types';
  3. describe('buildVisualQueryFromString', () => {
  4. it('creates no errors for empty query', () => {
  5. expect(buildVisualQueryFromString('')).toEqual(
  6. noErrors({
  7. labels: [],
  8. operations: [],
  9. })
  10. );
  11. });
  12. it('parses simple query with label-values', () => {
  13. expect(buildVisualQueryFromString('{app="frontend"}')).toEqual(
  14. noErrors({
  15. labels: [
  16. {
  17. op: '=',
  18. value: 'frontend',
  19. label: 'app',
  20. },
  21. ],
  22. operations: [],
  23. })
  24. );
  25. });
  26. it('parses query with multiple label-values pairs', () => {
  27. expect(buildVisualQueryFromString('{app="frontend", instance!="1"}')).toEqual(
  28. noErrors({
  29. labels: [
  30. {
  31. op: '=',
  32. value: 'frontend',
  33. label: 'app',
  34. },
  35. {
  36. op: '!=',
  37. value: '1',
  38. label: 'instance',
  39. },
  40. ],
  41. operations: [],
  42. })
  43. );
  44. });
  45. it('parses query with line filter', () => {
  46. expect(buildVisualQueryFromString('{app="frontend"} |= "line"')).toEqual(
  47. noErrors({
  48. labels: [
  49. {
  50. op: '=',
  51. value: 'frontend',
  52. label: 'app',
  53. },
  54. ],
  55. operations: [{ id: '__line_contains', params: ['line'] }],
  56. })
  57. );
  58. });
  59. it('parses query with line filters and escaped characters', () => {
  60. expect(buildVisualQueryFromString('{app="frontend"} |= "\\\\line"')).toEqual(
  61. noErrors({
  62. labels: [
  63. {
  64. op: '=',
  65. value: 'frontend',
  66. label: 'app',
  67. },
  68. ],
  69. operations: [{ id: '__line_contains', params: ['\\line'] }],
  70. })
  71. );
  72. });
  73. it('returns error for query with ip matching line filter', () => {
  74. const context = buildVisualQueryFromString('{app="frontend"} |= ip("192.168.4.5/16")');
  75. expect(context.errors).toEqual([
  76. {
  77. text: 'Matching ip addresses not supported in query builder: |= ip("192.168.4.5/16")',
  78. from: 17,
  79. to: 40,
  80. parentType: 'LineFilters',
  81. },
  82. ]);
  83. });
  84. it('parses query with matcher label filter', () => {
  85. expect(buildVisualQueryFromString('{app="frontend"} | bar="baz"')).toEqual(
  86. noErrors({
  87. labels: [
  88. {
  89. op: '=',
  90. value: 'frontend',
  91. label: 'app',
  92. },
  93. ],
  94. operations: [{ id: '__label_filter', params: ['bar', '=', 'baz'] }],
  95. })
  96. );
  97. });
  98. it('parses query with number label filter', () => {
  99. expect(buildVisualQueryFromString('{app="frontend"} | bar >= 8')).toEqual(
  100. noErrors({
  101. labels: [
  102. {
  103. op: '=',
  104. value: 'frontend',
  105. label: 'app',
  106. },
  107. ],
  108. operations: [{ id: '__label_filter', params: ['bar', '>=', '8'] }],
  109. })
  110. );
  111. });
  112. it('parses query with no pipe errors filter', () => {
  113. expect(buildVisualQueryFromString('{app="frontend"} | __error__=""')).toEqual(
  114. noErrors({
  115. labels: [
  116. {
  117. op: '=',
  118. value: 'frontend',
  119. label: 'app',
  120. },
  121. ],
  122. operations: [{ id: '__label_filter_no_errors', params: [] }],
  123. })
  124. );
  125. });
  126. it('parses query with with unit label filter', () => {
  127. expect(buildVisualQueryFromString('{app="frontend"} | bar < 8mb')).toEqual(
  128. noErrors({
  129. labels: [
  130. {
  131. op: '=',
  132. value: 'frontend',
  133. label: 'app',
  134. },
  135. ],
  136. operations: [{ id: '__label_filter', params: ['bar', '<', '8mb'] }],
  137. })
  138. );
  139. });
  140. it('returns error for query with "and", "or", "comma" in label filter', () => {
  141. const context = buildVisualQueryFromString('{app="frontend"} | logfmt | level="error" and job="grafana"');
  142. expect(context.errors).toEqual([
  143. {
  144. text: 'Label filter with comma, "and", "or" not supported in query builder: level="error" and job="grafana"',
  145. from: 28,
  146. to: 59,
  147. parentType: 'PipelineStage',
  148. },
  149. ]);
  150. });
  151. it('returns error for query with ip label filter', () => {
  152. const context = buildVisualQueryFromString('{app="frontend"} | logfmt | address=ip("192.168.4.5/16")');
  153. expect(context.errors).toEqual([
  154. {
  155. text: 'IpLabelFilter not supported in query builder: address=ip("192.168.4.5/16")',
  156. from: 28,
  157. to: 56,
  158. parentType: 'PipelineStage',
  159. },
  160. ]);
  161. });
  162. it('parses query with with parser', () => {
  163. expect(buildVisualQueryFromString('{app="frontend"} | json')).toEqual(
  164. noErrors({
  165. labels: [
  166. {
  167. op: '=',
  168. value: 'frontend',
  169. label: 'app',
  170. },
  171. ],
  172. operations: [{ id: 'json', params: [] }],
  173. })
  174. );
  175. });
  176. it('returns error for query with JSON expression parser', () => {
  177. const context = buildVisualQueryFromString('{app="frontend"} | json label="value" ');
  178. expect(context.errors).toEqual([
  179. {
  180. text: 'JsonExpressionParser not supported in visual query builder: json label="value"',
  181. from: 19,
  182. to: 37,
  183. parentType: 'PipelineStage',
  184. },
  185. ]);
  186. });
  187. it('parses query with with simple unwrap', () => {
  188. expect(
  189. buildVisualQueryFromString('sum_over_time({app="frontend"} | logfmt | unwrap bytes_processed [1m])')
  190. ).toEqual(
  191. noErrors({
  192. labels: [
  193. {
  194. op: '=',
  195. value: 'frontend',
  196. label: 'app',
  197. },
  198. ],
  199. operations: [
  200. { id: 'logfmt', params: [] },
  201. { id: 'unwrap', params: ['bytes_processed'] },
  202. { id: 'sum_over_time', params: ['1m'] },
  203. ],
  204. })
  205. );
  206. });
  207. it('parses query with with unwrap and error filter', () => {
  208. expect(
  209. buildVisualQueryFromString('sum_over_time({app="frontend"} | logfmt | unwrap duration | __error__="" [1m])')
  210. ).toEqual(
  211. noErrors({
  212. labels: [
  213. {
  214. op: '=',
  215. value: 'frontend',
  216. label: 'app',
  217. },
  218. ],
  219. operations: [
  220. { id: 'logfmt', params: [] },
  221. { id: 'unwrap', params: ['duration'] },
  222. { id: '__label_filter_no_errors', params: [] },
  223. { id: 'sum_over_time', params: ['1m'] },
  224. ],
  225. })
  226. );
  227. });
  228. it('parses query with with unwrap and label filter', () => {
  229. expect(
  230. buildVisualQueryFromString('sum_over_time({app="frontend"} | logfmt | unwrap duration | label="value" [1m])')
  231. ).toEqual(
  232. noErrors({
  233. labels: [
  234. {
  235. op: '=',
  236. value: 'frontend',
  237. label: 'app',
  238. },
  239. ],
  240. operations: [
  241. { id: 'logfmt', params: [] },
  242. { id: 'unwrap', params: ['duration'] },
  243. { id: '__label_filter', params: ['label', '=', 'value'] },
  244. { id: 'sum_over_time', params: ['1m'] },
  245. ],
  246. })
  247. );
  248. });
  249. it('returns error for query with unwrap and conversion operation', () => {
  250. const context = buildVisualQueryFromString(
  251. 'sum_over_time({app="frontend"} | logfmt | unwrap duration(label) [5m])'
  252. );
  253. expect(context.errors).toEqual([
  254. {
  255. text: 'Unwrap with conversion operator not supported in query builder: | unwrap duration(label)',
  256. from: 40,
  257. to: 64,
  258. parentType: 'LogRangeExpr',
  259. },
  260. ]);
  261. });
  262. it('parses metrics query with function', () => {
  263. expect(buildVisualQueryFromString('rate({app="frontend"} | json [5m])')).toEqual(
  264. noErrors({
  265. labels: [
  266. {
  267. op: '=',
  268. value: 'frontend',
  269. label: 'app',
  270. },
  271. ],
  272. operations: [
  273. { id: 'json', params: [] },
  274. { id: 'rate', params: ['5m'] },
  275. ],
  276. })
  277. );
  278. });
  279. it('parses metrics query with function and aggregation', () => {
  280. expect(buildVisualQueryFromString('sum(rate({app="frontend"} | json [5m]))')).toEqual(
  281. noErrors({
  282. labels: [
  283. {
  284. op: '=',
  285. value: 'frontend',
  286. label: 'app',
  287. },
  288. ],
  289. operations: [
  290. { id: 'json', params: [] },
  291. { id: 'rate', params: ['5m'] },
  292. { id: 'sum', params: [] },
  293. ],
  294. })
  295. );
  296. });
  297. it('parses metrics query with function and aggregation with grouping', () => {
  298. expect(buildVisualQueryFromString('sum by (job,name) (rate({app="frontend"} | json [5m]))')).toEqual(
  299. noErrors({
  300. labels: [
  301. {
  302. op: '=',
  303. value: 'frontend',
  304. label: 'app',
  305. },
  306. ],
  307. operations: [
  308. { id: 'json', params: [] },
  309. { id: 'rate', params: ['5m'] },
  310. { id: '__sum_by', params: ['job', 'name'] },
  311. ],
  312. })
  313. );
  314. });
  315. it('parses metrics query with function and aggregation with grouping at the end', () => {
  316. expect(buildVisualQueryFromString('sum(rate({app="frontend"} | json [5m])) without(job,name)')).toEqual(
  317. noErrors({
  318. labels: [
  319. {
  320. op: '=',
  321. value: 'frontend',
  322. label: 'app',
  323. },
  324. ],
  325. operations: [
  326. { id: 'json', params: [] },
  327. { id: 'rate', params: ['5m'] },
  328. { id: '__sum_without', params: ['job', 'name'] },
  329. ],
  330. })
  331. );
  332. });
  333. it('parses metrics query with function and aggregation and filters', () => {
  334. expect(buildVisualQueryFromString('sum(rate({app="frontend"} |~ `abc` | json | bar="baz" [5m]))')).toEqual(
  335. noErrors({
  336. labels: [
  337. {
  338. op: '=',
  339. value: 'frontend',
  340. label: 'app',
  341. },
  342. ],
  343. operations: [
  344. { id: '__line_matches_regex', params: ['abc'] },
  345. { id: 'json', params: [] },
  346. { id: '__label_filter', params: ['bar', '=', 'baz'] },
  347. { id: 'rate', params: ['5m'] },
  348. { id: 'sum', params: [] },
  349. ],
  350. })
  351. );
  352. });
  353. it('parses metrics query with vector aggregation with number', () => {
  354. expect(
  355. buildVisualQueryFromString('topk(10, sum(count_over_time({app="frontend"} | logfmt | __error__=`` [5m])))')
  356. ).toEqual(
  357. noErrors({
  358. labels: [
  359. {
  360. op: '=',
  361. value: 'frontend',
  362. label: 'app',
  363. },
  364. ],
  365. operations: [
  366. { id: 'logfmt', params: [] },
  367. { id: '__label_filter_no_errors', params: [] },
  368. { id: 'count_over_time', params: ['5m'] },
  369. { id: 'sum', params: [] },
  370. { id: 'topk', params: [10] },
  371. ],
  372. })
  373. );
  374. });
  375. it('parses template variables in strings', () => {
  376. expect(buildVisualQueryFromString('{instance="$label_variable"}')).toEqual(
  377. noErrors({
  378. labels: [{ label: 'instance', op: '=', value: '$label_variable' }],
  379. operations: [],
  380. })
  381. );
  382. });
  383. it('parses metrics query with interval variables', () => {
  384. expect(buildVisualQueryFromString('rate({app="frontend"} [$__interval])')).toEqual(
  385. noErrors({
  386. labels: [
  387. {
  388. op: '=',
  389. value: 'frontend',
  390. label: 'app',
  391. },
  392. ],
  393. operations: [{ id: 'rate', params: ['$__interval'] }],
  394. })
  395. );
  396. });
  397. it('parses quantile queries', () => {
  398. expect(buildVisualQueryFromString(`quantile_over_time(0.99, {app="frontend"} [1m])`)).toEqual(
  399. noErrors({
  400. labels: [
  401. {
  402. op: '=',
  403. value: 'frontend',
  404. label: 'app',
  405. },
  406. ],
  407. operations: [{ id: 'quantile_over_time', params: ['0.99', '1m'] }],
  408. })
  409. );
  410. });
  411. it('parses query with line format', () => {
  412. expect(buildVisualQueryFromString('{app="frontend"} | line_format "abc"')).toEqual(
  413. noErrors({
  414. labels: [
  415. {
  416. op: '=',
  417. value: 'frontend',
  418. label: 'app',
  419. },
  420. ],
  421. operations: [{ id: 'line_format', params: ['abc'] }],
  422. })
  423. );
  424. });
  425. it('parses query with label format', () => {
  426. expect(buildVisualQueryFromString('{app="frontend"} | label_format newLabel=oldLabel')).toEqual(
  427. noErrors({
  428. labels: [
  429. {
  430. op: '=',
  431. value: 'frontend',
  432. label: 'app',
  433. },
  434. ],
  435. operations: [{ id: 'label_format', params: ['newLabel', 'oldLabel'] }],
  436. })
  437. );
  438. });
  439. it('parses query with multiple label format', () => {
  440. expect(buildVisualQueryFromString('{app="frontend"} | label_format newLabel=oldLabel, bar="baz"')).toEqual(
  441. noErrors({
  442. labels: [
  443. {
  444. op: '=',
  445. value: 'frontend',
  446. label: 'app',
  447. },
  448. ],
  449. operations: [
  450. { id: 'label_format', params: ['newLabel', 'oldLabel'] },
  451. { id: 'label_format', params: ['bar', 'baz'] },
  452. ],
  453. })
  454. );
  455. });
  456. it('parses binary query', () => {
  457. expect(buildVisualQueryFromString('rate({project="bar"}[5m]) / rate({project="foo"}[5m])')).toEqual(
  458. noErrors({
  459. labels: [{ op: '=', value: 'bar', label: 'project' }],
  460. operations: [{ id: 'rate', params: ['5m'] }],
  461. binaryQueries: [
  462. {
  463. operator: '/',
  464. query: {
  465. labels: [{ op: '=', value: 'foo', label: 'project' }],
  466. operations: [{ id: 'rate', params: ['5m'] }],
  467. },
  468. },
  469. ],
  470. })
  471. );
  472. });
  473. it('parses binary scalar query', () => {
  474. expect(buildVisualQueryFromString('rate({project="bar"}[5m]) / 2')).toEqual(
  475. noErrors({
  476. labels: [{ op: '=', value: 'bar', label: 'project' }],
  477. operations: [
  478. { id: 'rate', params: ['5m'] },
  479. { id: '__divide_by', params: [2] },
  480. ],
  481. })
  482. );
  483. });
  484. it('parses chained binary query', () => {
  485. expect(
  486. buildVisualQueryFromString('rate({project="bar"}[5m]) * 2 / rate({project="foo"}[5m]) + rate({app="test"}[1m])')
  487. ).toEqual(
  488. noErrors({
  489. labels: [{ op: '=', value: 'bar', label: 'project' }],
  490. operations: [
  491. { id: 'rate', params: ['5m'] },
  492. { id: '__multiply_by', params: [2] },
  493. ],
  494. binaryQueries: [
  495. {
  496. operator: '/',
  497. query: {
  498. labels: [{ op: '=', value: 'foo', label: 'project' }],
  499. operations: [{ id: 'rate', params: ['5m'] }],
  500. binaryQueries: [
  501. {
  502. operator: '+',
  503. query: {
  504. labels: [{ op: '=', value: 'test', label: 'app' }],
  505. operations: [{ id: 'rate', params: ['1m'] }],
  506. },
  507. },
  508. ],
  509. },
  510. },
  511. ],
  512. })
  513. );
  514. });
  515. });
  516. function noErrors(query: LokiVisualQuery) {
  517. return {
  518. errors: [],
  519. query,
  520. };
  521. }