operationUtils.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import { capitalize } from 'lodash';
  2. import pluralize from 'pluralize';
  3. import { SelectableValue } from '@grafana/data/src';
  4. import { LabelParamEditor } from '../components/LabelParamEditor';
  5. import { PromVisualQueryOperationCategory } from '../types';
  6. import {
  7. QueryBuilderOperation,
  8. QueryBuilderOperationDef,
  9. QueryBuilderOperationParamDef,
  10. QueryBuilderOperationParamValue,
  11. QueryWithOperations,
  12. } from './types';
  13. export function functionRendererLeft(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  14. const params = renderParams(model, def, innerExpr);
  15. const str = model.id + '(';
  16. if (innerExpr) {
  17. params.push(innerExpr);
  18. }
  19. return str + params.join(', ') + ')';
  20. }
  21. export function functionRendererRight(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  22. const params = renderParams(model, def, innerExpr);
  23. const str = model.id + '(';
  24. if (innerExpr) {
  25. params.unshift(innerExpr);
  26. }
  27. return str + params.join(', ') + ')';
  28. }
  29. function rangeRendererWithParams(
  30. model: QueryBuilderOperation,
  31. def: QueryBuilderOperationDef,
  32. innerExpr: string,
  33. renderLeft: boolean
  34. ) {
  35. if (def.params.length < 2) {
  36. throw `Cannot render a function with params of length [${def.params.length}]`;
  37. }
  38. let rangeVector = (model.params ?? [])[0] ?? '5m';
  39. // Next frame the remaining parameters, but get rid of the first one because it's used to move the
  40. // instant vector into a range vector.
  41. const params = renderParams(
  42. {
  43. ...model,
  44. params: model.params.slice(1),
  45. },
  46. {
  47. ...def,
  48. params: def.params.slice(1),
  49. defaultParams: def.defaultParams.slice(1),
  50. },
  51. innerExpr
  52. );
  53. const str = model.id + '(';
  54. // Depending on the renderLeft variable, render parameters to the left or right
  55. // renderLeft === true (renderLeft) => (param1, param2, rangeVector[...])
  56. // renderLeft === false (renderRight) => (rangeVector[...], param1, param2)
  57. if (innerExpr) {
  58. renderLeft ? params.push(`${innerExpr}[${rangeVector}]`) : params.unshift(`${innerExpr}[${rangeVector}]`);
  59. }
  60. // stick everything together
  61. return str + params.join(', ') + ')';
  62. }
  63. export function rangeRendererRightWithParams(
  64. model: QueryBuilderOperation,
  65. def: QueryBuilderOperationDef,
  66. innerExpr: string
  67. ) {
  68. return rangeRendererWithParams(model, def, innerExpr, false);
  69. }
  70. export function rangeRendererLeftWithParams(
  71. model: QueryBuilderOperation,
  72. def: QueryBuilderOperationDef,
  73. innerExpr: string
  74. ) {
  75. return rangeRendererWithParams(model, def, innerExpr, true);
  76. }
  77. function renderParams(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  78. return (model.params ?? []).map((value, index) => {
  79. const paramDef = def.params[index];
  80. if (paramDef.type === 'string') {
  81. return '"' + value + '"';
  82. }
  83. return value;
  84. });
  85. }
  86. export function defaultAddOperationHandler<T extends QueryWithOperations>(def: QueryBuilderOperationDef, query: T) {
  87. const newOperation: QueryBuilderOperation = {
  88. id: def.id,
  89. params: def.defaultParams,
  90. };
  91. return {
  92. ...query,
  93. operations: [...query.operations, newOperation],
  94. };
  95. }
  96. export function getPromAndLokiOperationDisplayName(funcName: string) {
  97. return capitalize(funcName.replace(/_/g, ' '));
  98. }
  99. export function getOperationParamId(operationIndex: number, paramIndex: number) {
  100. return `operations.${operationIndex}.param.${paramIndex}`;
  101. }
  102. export function getRangeVectorParamDef(withRateInterval = false): QueryBuilderOperationParamDef {
  103. const param: QueryBuilderOperationParamDef = {
  104. name: 'Range',
  105. type: 'string',
  106. options: [
  107. {
  108. label: '$__interval',
  109. value: '$__interval',
  110. // tooltip: 'Dynamic interval based on max data points, scrape and min interval',
  111. },
  112. { label: '1m', value: '1m' },
  113. { label: '5m', value: '5m' },
  114. { label: '10m', value: '10m' },
  115. { label: '1h', value: '1h' },
  116. { label: '24h', value: '24h' },
  117. ],
  118. };
  119. if (withRateInterval) {
  120. (param.options as Array<SelectableValue<string>>).unshift({
  121. label: '$__rate_interval',
  122. value: '$__rate_interval',
  123. // tooltip: 'Always above 4x scrape interval',
  124. });
  125. }
  126. return param;
  127. }
  128. /**
  129. * This function is shared between Prometheus and Loki variants
  130. */
  131. export function createAggregationOperation<T extends QueryWithOperations>(
  132. name: string,
  133. overrides: Partial<QueryBuilderOperationDef> = {}
  134. ): QueryBuilderOperationDef[] {
  135. const operations: QueryBuilderOperationDef[] = [
  136. {
  137. id: name,
  138. name: getPromAndLokiOperationDisplayName(name),
  139. params: [
  140. {
  141. name: 'By label',
  142. type: 'string',
  143. restParam: true,
  144. optional: true,
  145. },
  146. ],
  147. defaultParams: [],
  148. alternativesKey: 'plain aggregations',
  149. category: PromVisualQueryOperationCategory.Aggregations,
  150. renderer: functionRendererLeft,
  151. paramChangedHandler: getOnLabelAddedHandler(`__${name}_by`),
  152. explainHandler: getAggregationExplainer(name, ''),
  153. addOperationHandler: defaultAddOperationHandler,
  154. ...overrides,
  155. },
  156. {
  157. id: `__${name}_by`,
  158. name: `${getPromAndLokiOperationDisplayName(name)} by`,
  159. params: [
  160. {
  161. name: 'Label',
  162. type: 'string',
  163. restParam: true,
  164. optional: true,
  165. editor: LabelParamEditor,
  166. },
  167. ],
  168. defaultParams: [''],
  169. alternativesKey: 'aggregations by',
  170. category: PromVisualQueryOperationCategory.Aggregations,
  171. renderer: getAggregationByRenderer(name),
  172. paramChangedHandler: getLastLabelRemovedHandler(name),
  173. explainHandler: getAggregationExplainer(name, 'by'),
  174. addOperationHandler: defaultAddOperationHandler,
  175. hideFromList: true,
  176. ...overrides,
  177. },
  178. {
  179. id: `__${name}_without`,
  180. name: `${getPromAndLokiOperationDisplayName(name)} without`,
  181. params: [
  182. {
  183. name: 'Label',
  184. type: 'string',
  185. restParam: true,
  186. optional: true,
  187. editor: LabelParamEditor,
  188. },
  189. ],
  190. defaultParams: [''],
  191. alternativesKey: 'aggregations by',
  192. category: PromVisualQueryOperationCategory.Aggregations,
  193. renderer: getAggregationWithoutRenderer(name),
  194. paramChangedHandler: getLastLabelRemovedHandler(name),
  195. explainHandler: getAggregationExplainer(name, 'without'),
  196. addOperationHandler: defaultAddOperationHandler,
  197. hideFromList: true,
  198. ...overrides,
  199. },
  200. ];
  201. return operations;
  202. }
  203. export function createAggregationOperationWithParam(
  204. name: string,
  205. paramsDef: { params: QueryBuilderOperationParamDef[]; defaultParams: QueryBuilderOperationParamValue[] },
  206. overrides: Partial<QueryBuilderOperationDef> = {}
  207. ): QueryBuilderOperationDef[] {
  208. const operations = createAggregationOperation(name, overrides);
  209. operations[0].params.unshift(...paramsDef.params);
  210. operations[1].params.unshift(...paramsDef.params);
  211. operations[2].params.unshift(...paramsDef.params);
  212. operations[0].defaultParams = paramsDef.defaultParams;
  213. operations[1].defaultParams = [...paramsDef.defaultParams, ''];
  214. operations[2].defaultParams = [...paramsDef.defaultParams, ''];
  215. operations[1].renderer = getAggregationByRendererWithParameter(name);
  216. operations[2].renderer = getAggregationByRendererWithParameter(name);
  217. return operations;
  218. }
  219. function getAggregationByRenderer(aggregation: string) {
  220. return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  221. return `${aggregation} by(${model.params.join(', ')}) (${innerExpr})`;
  222. };
  223. }
  224. function getAggregationWithoutRenderer(aggregation: string) {
  225. return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  226. return `${aggregation} without(${model.params.join(', ')}) (${innerExpr})`;
  227. };
  228. }
  229. /**
  230. * Very simple poc implementation, needs to be modified to support all aggregation operators
  231. */
  232. function getAggregationExplainer(aggregationName: string, mode: 'by' | 'without' | '') {
  233. return function aggregationExplainer(model: QueryBuilderOperation) {
  234. const labels = model.params.map((label) => `\`${label}\``).join(' and ');
  235. const labelWord = pluralize('label', model.params.length);
  236. switch (mode) {
  237. case 'by':
  238. return `Calculates ${aggregationName} over dimensions while preserving ${labelWord} ${labels}.`;
  239. case 'without':
  240. return `Calculates ${aggregationName} over the dimensions ${labels}. All other labels are preserved.`;
  241. default:
  242. return `Calculates ${aggregationName} over the dimensions.`;
  243. }
  244. };
  245. }
  246. function getAggregationByRendererWithParameter(aggregation: string) {
  247. return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
  248. const restParamIndex = def.params.findIndex((param) => param.restParam);
  249. const params = model.params.slice(0, restParamIndex);
  250. const restParams = model.params.slice(restParamIndex);
  251. return `${aggregation} by(${restParams.join(', ')}) (${params
  252. .map((param, idx) => (def.params[idx].type === 'string' ? `\"${param}\"` : param))
  253. .join(', ')}, ${innerExpr})`;
  254. };
  255. }
  256. /**
  257. * This function will transform operations without labels to their plan aggregation operation
  258. */
  259. function getLastLabelRemovedHandler(changeToOperationId: string) {
  260. return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) {
  261. // If definition has more params then is defined there are no optional rest params anymore.
  262. // We then transform this operation into a different one
  263. if (op.params.length < def.params.length) {
  264. return {
  265. ...op,
  266. id: changeToOperationId,
  267. };
  268. }
  269. return op;
  270. };
  271. }
  272. function getOnLabelAddedHandler(changeToOperationId: string) {
  273. return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) {
  274. // Check if we actually have the label param. As it's optional the aggregation can have one less, which is the
  275. // case of just simple aggregation without label. When user adds the label it now has the same number of params
  276. // as it's definition, and now we can change it to it's `_by` variant.
  277. if (op.params.length === def.params.length) {
  278. return {
  279. ...op,
  280. id: changeToOperationId,
  281. };
  282. }
  283. return op;
  284. };
  285. }