operations.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import {
  2. createAggregationOperation,
  3. createAggregationOperationWithParam,
  4. getPromAndLokiOperationDisplayName,
  5. } from '../../prometheus/querybuilder/shared/operationUtils';
  6. import {
  7. QueryBuilderOperation,
  8. QueryBuilderOperationDef,
  9. QueryBuilderOperationParamDef,
  10. VisualQueryModeller,
  11. } from '../../prometheus/querybuilder/shared/types';
  12. import { FUNCTIONS } from '../syntax';
  13. import { binaryScalarOperations } from './binaryScalarOperations';
  14. import { LokiOperationId, LokiOperationOrder, LokiVisualQuery, LokiVisualQueryOperationCategory } from './types';
  15. export function getOperationDefinitions(): QueryBuilderOperationDef[] {
  16. const aggregations = [
  17. LokiOperationId.Sum,
  18. LokiOperationId.Min,
  19. LokiOperationId.Max,
  20. LokiOperationId.Avg,
  21. LokiOperationId.Stddev,
  22. LokiOperationId.Stdvar,
  23. LokiOperationId.Count,
  24. ].flatMap((opId) =>
  25. createAggregationOperation(opId, {
  26. addOperationHandler: addLokiOperation,
  27. orderRank: LokiOperationOrder.Last,
  28. })
  29. );
  30. const aggregationsWithParam = [LokiOperationId.TopK, LokiOperationId.BottomK].flatMap((opId) => {
  31. return createAggregationOperationWithParam(
  32. opId,
  33. {
  34. params: [{ name: 'K-value', type: 'number' }],
  35. defaultParams: [5],
  36. },
  37. {
  38. addOperationHandler: addLokiOperation,
  39. orderRank: LokiOperationOrder.Last,
  40. }
  41. );
  42. });
  43. const list: QueryBuilderOperationDef[] = [
  44. createRangeOperation(LokiOperationId.Rate),
  45. createRangeOperation(LokiOperationId.CountOverTime),
  46. createRangeOperation(LokiOperationId.SumOverTime),
  47. createRangeOperation(LokiOperationId.BytesRate),
  48. createRangeOperation(LokiOperationId.BytesOverTime),
  49. createRangeOperation(LokiOperationId.AbsentOverTime),
  50. createRangeOperation(LokiOperationId.AvgOverTime),
  51. createRangeOperation(LokiOperationId.MaxOverTime),
  52. createRangeOperation(LokiOperationId.MinOverTime),
  53. createRangeOperation(LokiOperationId.FirstOverTime),
  54. createRangeOperation(LokiOperationId.LastOverTime),
  55. createRangeOperation(LokiOperationId.StdvarOverTime),
  56. createRangeOperation(LokiOperationId.StddevOverTime),
  57. createRangeOperation(LokiOperationId.QuantileOverTime),
  58. ...aggregations,
  59. ...aggregationsWithParam,
  60. {
  61. id: LokiOperationId.Json,
  62. name: 'Json',
  63. params: [],
  64. defaultParams: [],
  65. alternativesKey: 'format',
  66. category: LokiVisualQueryOperationCategory.Formats,
  67. orderRank: LokiOperationOrder.LineFormats,
  68. renderer: pipelineRenderer,
  69. addOperationHandler: addLokiOperation,
  70. },
  71. {
  72. id: LokiOperationId.Logfmt,
  73. name: 'Logfmt',
  74. params: [],
  75. defaultParams: [],
  76. alternativesKey: 'format',
  77. category: LokiVisualQueryOperationCategory.Formats,
  78. orderRank: LokiOperationOrder.LineFormats,
  79. renderer: pipelineRenderer,
  80. addOperationHandler: addLokiOperation,
  81. explainHandler: () =>
  82. `This will extract all keys and values from a [logfmt](https://grafana.com/docs/loki/latest/logql/log_queries/#logfmt) formatted log line as labels. The extracted labels can be used in label filter expressions and used as values for a range aggregation via the unwrap operation.`,
  83. },
  84. {
  85. id: LokiOperationId.Regexp,
  86. name: 'Regexp',
  87. params: [
  88. {
  89. name: 'String',
  90. type: 'string',
  91. hideName: true,
  92. placeholder: '<re>',
  93. description: 'The regexp expression that matches the structure of a log line.',
  94. minWidth: 20,
  95. },
  96. ],
  97. defaultParams: [''],
  98. alternativesKey: 'format',
  99. category: LokiVisualQueryOperationCategory.Formats,
  100. orderRank: LokiOperationOrder.LineFormats,
  101. renderer: (model, def, innerExpr) => `${innerExpr} | regexp \`${model.params[0]}\``,
  102. addOperationHandler: addLokiOperation,
  103. explainHandler: () =>
  104. `The [regexp parser](https://grafana.com/docs/loki/latest/logql/log_queries/#regular-expression) takes a single parameter | regexp "<re>" which is the regular expression using the Golang RE2 syntax. The regular expression must contain a least one named sub-match (e.g (?P<name>re)), each sub-match will extract a different label. The expression matches the structure of a log line. The extracted labels can be used in label filter expressions and used as values for a range aggregation via the unwrap operation.`,
  105. },
  106. {
  107. id: LokiOperationId.Pattern,
  108. name: 'Pattern',
  109. params: [
  110. {
  111. name: 'String',
  112. type: 'string',
  113. hideName: true,
  114. placeholder: '<pattern-expression>',
  115. description: 'The expression that matches the structure of a log line.',
  116. minWidth: 20,
  117. },
  118. ],
  119. defaultParams: [''],
  120. alternativesKey: 'format',
  121. category: LokiVisualQueryOperationCategory.Formats,
  122. orderRank: LokiOperationOrder.LineFormats,
  123. renderer: (model, def, innerExpr) => `${innerExpr} | pattern \`${model.params[0]}\``,
  124. addOperationHandler: addLokiOperation,
  125. explainHandler: () =>
  126. `The [pattern parser](https://grafana.com/docs/loki/latest/logql/log_queries/#pattern) allows the explicit extraction of fields from log lines by defining a pattern expression (| pattern \`<pattern-expression>\`). The expression matches the structure of a log line. The extracted labels can be used in label filter expressions and used as values for a range aggregation via the unwrap operation.`,
  127. },
  128. {
  129. id: LokiOperationId.Unpack,
  130. name: 'Unpack',
  131. params: [],
  132. defaultParams: [],
  133. alternativesKey: 'format',
  134. category: LokiVisualQueryOperationCategory.Formats,
  135. orderRank: LokiOperationOrder.LineFormats,
  136. renderer: pipelineRenderer,
  137. addOperationHandler: addLokiOperation,
  138. explainHandler: () =>
  139. `This will extract all keys and values from a JSON log line, [unpacking](https://grafana.com/docs/loki/latest/logql/log_queries/#unpack) all embedded labels in the pack stage. The extracted labels can be used in label filter expressions and used as values for a range aggregation via the unwrap operation.`,
  140. },
  141. {
  142. id: LokiOperationId.LineFormat,
  143. name: 'Line format',
  144. params: [
  145. {
  146. name: 'String',
  147. type: 'string',
  148. hideName: true,
  149. placeholder: '{{.status_code}}',
  150. description: 'A line template that can refer to stream labels and extracted labels.',
  151. minWidth: 20,
  152. },
  153. ],
  154. defaultParams: [''],
  155. alternativesKey: 'format',
  156. category: LokiVisualQueryOperationCategory.Formats,
  157. orderRank: LokiOperationOrder.LineFormats,
  158. renderer: (model, def, innerExpr) => `${innerExpr} | line_format \`${model.params[0]}\``,
  159. addOperationHandler: addLokiOperation,
  160. explainHandler: () =>
  161. `This will replace log line using a specified template. The template can refer to stream labels and extracted labels.
  162. Example: \`{{.status_code}} - {{.message}}\`
  163. [Read the docs](https://grafana.com/docs/loki/latest/logql/log_queries/#line-format-expression) for more.
  164. `,
  165. },
  166. {
  167. id: LokiOperationId.LabelFormat,
  168. name: 'Label format',
  169. params: [
  170. { name: 'Label', type: 'string' },
  171. { name: 'Rename', type: 'string' },
  172. ],
  173. defaultParams: ['', ''],
  174. alternativesKey: 'format',
  175. category: LokiVisualQueryOperationCategory.Formats,
  176. orderRank: LokiOperationOrder.LineFormats,
  177. renderer: (model, def, innerExpr) => `${innerExpr} | label_format ${model.params[1]}=\`${model.params[0]}\``,
  178. addOperationHandler: addLokiOperation,
  179. explainHandler: () =>
  180. `This will change name of label to desired new label. In the example below, label "error_level" will be renamed to "level".
  181. Example: error_level=\`level\`
  182. [Read the docs](https://grafana.com/docs/loki/latest/logql/log_queries/#labels-format-expression) for more.
  183. `,
  184. },
  185. {
  186. id: LokiOperationId.LineContains,
  187. name: 'Line contains',
  188. params: [
  189. {
  190. name: 'String',
  191. type: 'string',
  192. hideName: true,
  193. placeholder: 'Text to find',
  194. description: 'Find log lines that contains this text',
  195. minWidth: 20,
  196. runQueryOnEnter: true,
  197. },
  198. ],
  199. defaultParams: [''],
  200. alternativesKey: 'line filter',
  201. category: LokiVisualQueryOperationCategory.LineFilters,
  202. orderRank: LokiOperationOrder.LineFilters,
  203. renderer: getLineFilterRenderer('|='),
  204. addOperationHandler: addLokiOperation,
  205. explainHandler: (op) => `Return log lines that contain string \`${op.params[0]}\`.`,
  206. },
  207. {
  208. id: LokiOperationId.LineContainsNot,
  209. name: 'Line does not contain',
  210. params: [
  211. {
  212. name: 'String',
  213. type: 'string',
  214. hideName: true,
  215. placeholder: 'Text to exclude',
  216. description: 'Find log lines that does not contain this text',
  217. minWidth: 26,
  218. runQueryOnEnter: true,
  219. },
  220. ],
  221. defaultParams: [''],
  222. alternativesKey: 'line filter',
  223. category: LokiVisualQueryOperationCategory.LineFilters,
  224. orderRank: LokiOperationOrder.LineFilters,
  225. renderer: getLineFilterRenderer('!='),
  226. addOperationHandler: addLokiOperation,
  227. explainHandler: (op) => `Return log lines that does not contain string \`${op.params[0]}\`.`,
  228. },
  229. {
  230. id: LokiOperationId.LineMatchesRegex,
  231. name: 'Line contains regex match',
  232. params: [
  233. {
  234. name: 'Regex',
  235. type: 'string',
  236. hideName: true,
  237. placeholder: 'Pattern to match',
  238. description: 'Find log lines that match this regex pattern',
  239. minWidth: 30,
  240. runQueryOnEnter: true,
  241. },
  242. ],
  243. defaultParams: [''],
  244. alternativesKey: 'line filter',
  245. category: LokiVisualQueryOperationCategory.LineFilters,
  246. orderRank: LokiOperationOrder.LineFilters,
  247. renderer: getLineFilterRenderer('|~'),
  248. addOperationHandler: addLokiOperation,
  249. explainHandler: (op) => `Return log lines that match regex \`${op.params[0]}\`.`,
  250. },
  251. {
  252. id: LokiOperationId.LineMatchesRegexNot,
  253. name: 'Line does not match regex',
  254. params: [
  255. {
  256. name: 'Regex',
  257. type: 'string',
  258. hideName: true,
  259. placeholder: 'Pattern to exclude',
  260. description: 'Find log lines that does not match this regex pattern',
  261. minWidth: 30,
  262. runQueryOnEnter: true,
  263. },
  264. ],
  265. defaultParams: [''],
  266. alternativesKey: 'line filter',
  267. category: LokiVisualQueryOperationCategory.LineFilters,
  268. orderRank: LokiOperationOrder.LineFilters,
  269. renderer: getLineFilterRenderer('!~'),
  270. addOperationHandler: addLokiOperation,
  271. explainHandler: (op) => `Return log lines that does not match regex \`${op.params[0]}\`.`,
  272. },
  273. {
  274. id: LokiOperationId.LabelFilter,
  275. name: 'Label filter expression',
  276. params: [
  277. { name: 'Label', type: 'string' },
  278. { name: 'Operator', type: 'string', options: ['=', '!=', ' =~', '!~', '>', '<', '>=', '<='] },
  279. { name: 'Value', type: 'string' },
  280. ],
  281. defaultParams: ['', '=', ''],
  282. alternativesKey: 'label filter',
  283. category: LokiVisualQueryOperationCategory.LabelFilters,
  284. orderRank: LokiOperationOrder.LabelFilters,
  285. renderer: labelFilterRenderer,
  286. addOperationHandler: addLokiOperation,
  287. explainHandler: () => `Label expression filter allows filtering using original and extracted labels.`,
  288. },
  289. {
  290. id: LokiOperationId.LabelFilterNoErrors,
  291. name: 'No pipeline errors',
  292. params: [],
  293. defaultParams: [],
  294. alternativesKey: 'label filter',
  295. category: LokiVisualQueryOperationCategory.LabelFilters,
  296. orderRank: LokiOperationOrder.NoErrors,
  297. renderer: (model, def, innerExpr) => `${innerExpr} | __error__=\`\``,
  298. addOperationHandler: addLokiOperation,
  299. explainHandler: () => `Filter out all formatting and parsing errors.`,
  300. },
  301. {
  302. id: LokiOperationId.Unwrap,
  303. name: 'Unwrap',
  304. params: [{ name: 'Identifier', type: 'string', hideName: true, minWidth: 16, placeholder: 'Label key' }],
  305. defaultParams: [''],
  306. alternativesKey: 'format',
  307. category: LokiVisualQueryOperationCategory.Formats,
  308. orderRank: LokiOperationOrder.Unwrap,
  309. renderer: (op, def, innerExpr) => `${innerExpr} | unwrap ${op.params[0]}`,
  310. addOperationHandler: addLokiOperation,
  311. explainHandler: (op) => {
  312. let label = String(op.params[0]).length > 0 ? op.params[0] : '<label>';
  313. return `Use the extracted label \`${label}\` as sample values instead of log lines for the subsequent range aggregation.`;
  314. },
  315. },
  316. ...binaryScalarOperations,
  317. {
  318. id: LokiOperationId.NestedQuery,
  319. name: 'Binary operation with query',
  320. params: [],
  321. defaultParams: [],
  322. category: LokiVisualQueryOperationCategory.BinaryOps,
  323. renderer: (model, def, innerExpr) => innerExpr,
  324. addOperationHandler: addNestedQueryHandler,
  325. },
  326. ];
  327. return list;
  328. }
  329. function createRangeOperation(name: string): QueryBuilderOperationDef {
  330. const params = [getRangeVectorParamDef()];
  331. const defaultParams = ['$__interval'];
  332. let renderer = operationWithRangeVectorRenderer;
  333. if (name === LokiOperationId.QuantileOverTime) {
  334. defaultParams.push('0.95');
  335. params.push({
  336. name: 'Quantile',
  337. type: 'number',
  338. });
  339. renderer = operationWithRangeVectorRendererAndParam;
  340. }
  341. return {
  342. id: name,
  343. name: getPromAndLokiOperationDisplayName(name),
  344. params,
  345. defaultParams,
  346. alternativesKey: 'range function',
  347. category: LokiVisualQueryOperationCategory.RangeFunctions,
  348. orderRank: LokiOperationOrder.RangeVectorFunction,
  349. renderer,
  350. addOperationHandler: addLokiOperation,
  351. explainHandler: (op, def) => {
  352. let opDocs = FUNCTIONS.find((x) => x.insertText === op.id)?.documentation ?? '';
  353. if (op.params[0] === '$__interval') {
  354. return `${opDocs} \`$__interval\` is variable that will be replaced with a calculated interval based on **Max data points**, **Min interval** and query time range. You find these options you find under **Query options** at the right of the data source select dropdown.`;
  355. } else {
  356. return `${opDocs} The [range vector](https://grafana.com/docs/loki/latest/logql/metric_queries/#range-vector-aggregation) is set to \`${op.params[0]}\`.`;
  357. }
  358. },
  359. };
  360. }
  361. function getRangeVectorParamDef(): QueryBuilderOperationParamDef {
  362. return {
  363. name: 'Range',
  364. type: 'string',
  365. options: ['$__interval', '$__range', '1m', '5m', '10m', '1h', '24h'],
  366. };
  367. }
  368. function operationWithRangeVectorRenderer(
  369. model: QueryBuilderOperation,
  370. def: QueryBuilderOperationDef,
  371. innerExpr: string
  372. ) {
  373. let rangeVector = (model.params ?? [])[0] ?? '$__interval';
  374. return `${def.id}(${innerExpr} [${rangeVector}])`;
  375. }
  376. function operationWithRangeVectorRendererAndParam(
  377. model: QueryBuilderOperation,
  378. def: QueryBuilderOperationDef,
  379. innerExpr: string
  380. ) {
  381. const params = model.params ?? [];
  382. const rangeVector = params[0] ?? '$__interval';
  383. const param = params[1];
  384. return `${def.id}(${param}, ${innerExpr} [${rangeVector}])`;
  385. }
  386. function getLineFilterRenderer(operation: string) {
  387. return function lineFilterRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  388. return `${innerExpr} ${operation} \`${model.params[0]}\``;
  389. };
  390. }
  391. function labelFilterRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  392. if (model.params[0] === '') {
  393. return innerExpr;
  394. }
  395. if (model.params[1] === '<' || model.params[1] === '>') {
  396. return `${innerExpr} | ${model.params[0]} ${model.params[1]} ${model.params[2]}`;
  397. }
  398. return `${innerExpr} | ${model.params[0]}${model.params[1]}\`${model.params[2]}\``;
  399. }
  400. function pipelineRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  401. return `${innerExpr} | ${model.id}`;
  402. }
  403. function isRangeVectorFunction(def: QueryBuilderOperationDef) {
  404. return def.category === LokiVisualQueryOperationCategory.RangeFunctions;
  405. }
  406. function getIndexOfOrLast(
  407. operations: QueryBuilderOperation[],
  408. queryModeller: VisualQueryModeller,
  409. condition: (def: QueryBuilderOperationDef) => boolean
  410. ) {
  411. const index = operations.findIndex((x) => {
  412. const opDef = queryModeller.getOperationDef(x.id);
  413. if (!opDef) {
  414. return false;
  415. }
  416. return condition(opDef);
  417. });
  418. return index === -1 ? operations.length : index;
  419. }
  420. export function addLokiOperation(
  421. def: QueryBuilderOperationDef,
  422. query: LokiVisualQuery,
  423. modeller: VisualQueryModeller
  424. ): LokiVisualQuery {
  425. const newOperation: QueryBuilderOperation = {
  426. id: def.id,
  427. params: def.defaultParams,
  428. };
  429. const operations = [...query.operations];
  430. const existingRangeVectorFunction = operations.find((x) => {
  431. const opDef = modeller.getOperationDef(x.id);
  432. if (!opDef) {
  433. return false;
  434. }
  435. return isRangeVectorFunction(opDef);
  436. });
  437. switch (def.category) {
  438. case LokiVisualQueryOperationCategory.Aggregations:
  439. case LokiVisualQueryOperationCategory.Functions:
  440. // If we are adding a function but we have not range vector function yet add one
  441. if (!existingRangeVectorFunction) {
  442. const placeToInsert = getIndexOfOrLast(
  443. operations,
  444. modeller,
  445. (def) => def.category === LokiVisualQueryOperationCategory.Functions
  446. );
  447. operations.splice(placeToInsert, 0, { id: LokiOperationId.Rate, params: ['$__interval'] });
  448. }
  449. operations.push(newOperation);
  450. break;
  451. case LokiVisualQueryOperationCategory.RangeFunctions:
  452. // If adding a range function and range function is already added replace it
  453. if (existingRangeVectorFunction) {
  454. const index = operations.indexOf(existingRangeVectorFunction);
  455. operations[index] = newOperation;
  456. break;
  457. }
  458. // Add range functions after any formats, line filters and label filters
  459. default:
  460. const placeToInsert = getIndexOfOrLast(
  461. operations,
  462. modeller,
  463. (x) => (def.orderRank ?? 100) < (x.orderRank ?? 100)
  464. );
  465. operations.splice(placeToInsert, 0, newOperation);
  466. break;
  467. }
  468. return {
  469. ...query,
  470. operations,
  471. };
  472. }
  473. function addNestedQueryHandler(def: QueryBuilderOperationDef, query: LokiVisualQuery): LokiVisualQuery {
  474. return {
  475. ...query,
  476. binaryQueries: [
  477. ...(query.binaryQueries ?? []),
  478. {
  479. operator: '/',
  480. query,
  481. },
  482. ],
  483. };
  484. }