language_utils.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import { invert } from 'lodash';
  2. import { Token } from 'prismjs';
  3. import { DataQuery, AbstractQuery, AbstractLabelOperator, AbstractLabelMatcher } from '@grafana/data';
  4. import { addLabelToQuery } from './add_label_to_query';
  5. import { SUGGESTIONS_LIMIT } from './language_provider';
  6. import { PromMetricsMetadata, PromMetricsMetadataItem } from './types';
  7. export const processHistogramMetrics = (metrics: string[]) => {
  8. const resultSet: Set<string> = new Set();
  9. const regexp = new RegExp('_bucket($|:)');
  10. for (let index = 0; index < metrics.length; index++) {
  11. const metric = metrics[index];
  12. const isHistogramValue = regexp.test(metric);
  13. if (isHistogramValue) {
  14. resultSet.add(metric);
  15. }
  16. }
  17. return [...resultSet];
  18. };
  19. export function processLabels(labels: Array<{ [key: string]: string }>, withName = false) {
  20. // For processing we are going to use sets as they have significantly better performance than arrays
  21. // After we process labels, we will convert sets to arrays and return object with label values in arrays
  22. const valueSet: { [key: string]: Set<string> } = {};
  23. labels.forEach((label) => {
  24. const { __name__, ...rest } = label;
  25. if (withName) {
  26. valueSet['__name__'] = valueSet['__name__'] || new Set();
  27. if (!valueSet['__name__'].has(__name__)) {
  28. valueSet['__name__'].add(__name__);
  29. }
  30. }
  31. Object.keys(rest).forEach((key) => {
  32. if (!valueSet[key]) {
  33. valueSet[key] = new Set();
  34. }
  35. if (!valueSet[key].has(rest[key])) {
  36. valueSet[key].add(rest[key]);
  37. }
  38. });
  39. });
  40. // valueArray that we are going to return in the object
  41. const valueArray: { [key: string]: string[] } = {};
  42. limitSuggestions(Object.keys(valueSet)).forEach((key) => {
  43. valueArray[key] = limitSuggestions(Array.from(valueSet[key]));
  44. });
  45. return { values: valueArray, keys: Object.keys(valueArray) };
  46. }
  47. // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
  48. export const selectorRegexp = /\{[^}]*?(\}|$)/;
  49. export const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
  50. export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
  51. if (!query.match(selectorRegexp)) {
  52. // Special matcher for metrics
  53. if (query.match(/^[A-Za-z:][\w:]*$/)) {
  54. return {
  55. selector: `{__name__="${query}"}`,
  56. labelKeys: ['__name__'],
  57. };
  58. }
  59. throw new Error('Query must contain a selector: ' + query);
  60. }
  61. // Check if inside a selector
  62. const prefix = query.slice(0, cursorOffset);
  63. const prefixOpen = prefix.lastIndexOf('{');
  64. const prefixClose = prefix.lastIndexOf('}');
  65. if (prefixOpen === -1) {
  66. throw new Error('Not inside selector, missing open brace: ' + prefix);
  67. }
  68. if (prefixClose > -1 && prefixClose > prefixOpen) {
  69. throw new Error('Not inside selector, previous selector already closed: ' + prefix);
  70. }
  71. const suffix = query.slice(cursorOffset);
  72. const suffixCloseIndex = suffix.indexOf('}');
  73. const suffixClose = suffixCloseIndex + cursorOffset;
  74. const suffixOpenIndex = suffix.indexOf('{');
  75. const suffixOpen = suffixOpenIndex + cursorOffset;
  76. if (suffixClose === -1) {
  77. throw new Error('Not inside selector, missing closing brace in suffix: ' + suffix);
  78. }
  79. if (suffixOpenIndex > -1 && suffixOpen < suffixClose) {
  80. throw new Error('Not inside selector, next selector opens before this one closed: ' + suffix);
  81. }
  82. // Extract clean labels to form clean selector, incomplete labels are dropped
  83. const selector = query.slice(prefixOpen, suffixClose);
  84. const labels: { [key: string]: { value: string; operator: string } } = {};
  85. selector.replace(labelRegexp, (label, key, operator, value) => {
  86. const labelOffset = query.indexOf(label);
  87. const valueStart = labelOffset + key.length + operator.length + 1;
  88. const valueEnd = labelOffset + key.length + operator.length + value.length - 1;
  89. // Skip label if cursor is in value
  90. if (cursorOffset < valueStart || cursorOffset > valueEnd) {
  91. labels[key] = { value, operator };
  92. }
  93. return '';
  94. });
  95. // Add metric if there is one before the selector
  96. const metricPrefix = query.slice(0, prefixOpen);
  97. const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/);
  98. if (metricMatch) {
  99. labels['__name__'] = { value: `"${metricMatch[0]}"`, operator: '=' };
  100. }
  101. // Build sorted selector
  102. const labelKeys = Object.keys(labels).sort();
  103. const cleanSelector = labelKeys.map((key) => `${key}${labels[key].operator}${labels[key].value}`).join(',');
  104. const selectorString = ['{', cleanSelector, '}'].join('');
  105. return { labelKeys, selector: selectorString };
  106. }
  107. export function expandRecordingRules(query: string, mapping: { [name: string]: string }): string {
  108. const ruleNames = Object.keys(mapping);
  109. const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\(|\\[|\\{)`, 'ig');
  110. const expandedQuery = query.replace(rulesRegex, (match, pre, name, post) => `${pre}${mapping[name]}${post}`);
  111. // Split query into array, so if query uses operators, we can correctly add labels to each individual part.
  112. const queryArray = expandedQuery.split(/(\+|\-|\*|\/|\%|\^)/);
  113. // Regex that matches occurrences of ){ or }{ or ]{ which is a sign of incorrecly added labels.
  114. const invalidLabelsRegex = /(\)\{|\}\{|\]\{)/;
  115. const correctlyExpandedQueryArray = queryArray.map((query) => {
  116. return addLabelsToExpression(query, invalidLabelsRegex);
  117. });
  118. return correctlyExpandedQueryArray.join('');
  119. }
  120. function addLabelsToExpression(expr: string, invalidLabelsRegexp: RegExp) {
  121. const match = expr.match(invalidLabelsRegexp);
  122. if (!match) {
  123. return expr;
  124. }
  125. // Split query into 2 parts - before the invalidLabelsRegex match and after.
  126. const indexOfRegexMatch = match.index ?? 0;
  127. const exprBeforeRegexMatch = expr.slice(0, indexOfRegexMatch + 1);
  128. const exprAfterRegexMatch = expr.slice(indexOfRegexMatch + 1);
  129. // Create arrayOfLabelObjects with label objects that have key, operator and value.
  130. const arrayOfLabelObjects: Array<{ key: string; operator: string; value: string }> = [];
  131. exprAfterRegexMatch.replace(labelRegexp, (label, key, operator, value) => {
  132. arrayOfLabelObjects.push({ key, operator, value });
  133. return '';
  134. });
  135. // Loop through all label objects and add them to query.
  136. // As a starting point we have valid query without the labels.
  137. let result = exprBeforeRegexMatch;
  138. arrayOfLabelObjects.filter(Boolean).forEach((obj) => {
  139. // Remove extra set of quotes from obj.value
  140. const value = obj.value.slice(1, -1);
  141. result = addLabelToQuery(result, obj.key, value, obj.operator);
  142. });
  143. return result;
  144. }
  145. /**
  146. * Adds metadata for synthetic metrics for which the API does not provide metadata.
  147. * See https://github.com/grafana/grafana/issues/22337 for details.
  148. *
  149. * @param metadata HELP and TYPE metadata from /api/v1/metadata
  150. */
  151. export function fixSummariesMetadata(metadata: { [metric: string]: PromMetricsMetadataItem[] }): PromMetricsMetadata {
  152. if (!metadata) {
  153. return metadata;
  154. }
  155. const baseMetadata: PromMetricsMetadata = {};
  156. const summaryMetadata: PromMetricsMetadata = {};
  157. for (const metric in metadata) {
  158. // NOTE: based on prometheus-documentation, we can receive
  159. // multiple metadata-entries for the given metric, it seems
  160. // it happens when the same metric is on multiple targets
  161. // and their help-text differs
  162. // (https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metric-metadata)
  163. // for now we just use the first entry.
  164. const item = metadata[metric][0];
  165. baseMetadata[metric] = item;
  166. if (item.type === 'histogram') {
  167. summaryMetadata[`${metric}_bucket`] = {
  168. type: 'counter',
  169. help: `Cumulative counters for the observation buckets (${item.help})`,
  170. };
  171. summaryMetadata[`${metric}_count`] = {
  172. type: 'counter',
  173. help: `Count of events that have been observed for the histogram metric (${item.help})`,
  174. };
  175. summaryMetadata[`${metric}_sum`] = {
  176. type: 'counter',
  177. help: `Total sum of all observed values for the histogram metric (${item.help})`,
  178. };
  179. }
  180. if (item.type === 'summary') {
  181. summaryMetadata[`${metric}_count`] = {
  182. type: 'counter',
  183. help: `Count of events that have been observed for the base metric (${item.help})`,
  184. };
  185. summaryMetadata[`${metric}_sum`] = {
  186. type: 'counter',
  187. help: `Total sum of all observed values for the base metric (${item.help})`,
  188. };
  189. }
  190. }
  191. // Synthetic series
  192. const syntheticMetadata: PromMetricsMetadata = {};
  193. syntheticMetadata['ALERTS'] = {
  194. type: 'counter',
  195. help: 'Time series showing pending and firing alerts. The sample value is set to 1 as long as the alert is in the indicated active (pending or firing) state.',
  196. };
  197. return { ...baseMetadata, ...summaryMetadata, ...syntheticMetadata };
  198. }
  199. export function roundMsToMin(milliseconds: number): number {
  200. return roundSecToMin(milliseconds / 1000);
  201. }
  202. export function roundSecToMin(seconds: number): number {
  203. return Math.floor(seconds / 60);
  204. }
  205. export function limitSuggestions(items: string[]) {
  206. return items.slice(0, SUGGESTIONS_LIMIT);
  207. }
  208. export function addLimitInfo(items: any[] | undefined): string {
  209. return items && items.length >= SUGGESTIONS_LIMIT ? `, limited to the first ${SUGGESTIONS_LIMIT} received items` : '';
  210. }
  211. // NOTE: the following 2 exported functions are very similar to the prometheus*Escape
  212. // functions in datasource.ts, but they are not exactly the same algorithm, and we found
  213. // no way to reuse one in the another or vice versa.
  214. // Prometheus regular-expressions use the RE2 syntax (https://github.com/google/re2/wiki/Syntax),
  215. // so every character that matches something in that list has to be escaped.
  216. // the list of metacharacters is: *+?()|\.[]{}^$
  217. // we make a javascript regular expression that matches those characters:
  218. const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g;
  219. function escapePrometheusRegexp(value: string): string {
  220. return value.replace(RE2_METACHARACTERS, '\\$&');
  221. }
  222. // based on the openmetrics-documentation, the 3 symbols we have to handle are:
  223. // - \n ... the newline character
  224. // - \ ... the backslash character
  225. // - " ... the double-quote character
  226. export function escapeLabelValueInExactSelector(labelValue: string): string {
  227. return labelValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
  228. }
  229. export function escapeLabelValueInRegexSelector(labelValue: string): string {
  230. return escapeLabelValueInExactSelector(escapePrometheusRegexp(labelValue));
  231. }
  232. const FromPromLikeMap: Record<string, AbstractLabelOperator> = {
  233. '=': AbstractLabelOperator.Equal,
  234. '!=': AbstractLabelOperator.NotEqual,
  235. '=~': AbstractLabelOperator.EqualRegEx,
  236. '!~': AbstractLabelOperator.NotEqualRegEx,
  237. };
  238. const ToPromLikeMap: Record<AbstractLabelOperator, string> = invert(FromPromLikeMap) as Record<
  239. AbstractLabelOperator,
  240. string
  241. >;
  242. export function toPromLikeExpr(labelBasedQuery: AbstractQuery): string {
  243. const expr = labelBasedQuery.labelMatchers
  244. .map((selector: AbstractLabelMatcher) => {
  245. const operator = ToPromLikeMap[selector.operator];
  246. if (operator) {
  247. return `${selector.name}${operator}"${selector.value}"`;
  248. } else {
  249. return '';
  250. }
  251. })
  252. .filter((e: string) => e !== '')
  253. .join(', ');
  254. return expr ? `{${expr}}` : '';
  255. }
  256. export function toPromLikeQuery(labelBasedQuery: AbstractQuery): PromLikeQuery {
  257. return {
  258. refId: labelBasedQuery.refId,
  259. expr: toPromLikeExpr(labelBasedQuery),
  260. range: true,
  261. };
  262. }
  263. export interface PromLikeQuery extends DataQuery {
  264. expr: string;
  265. range: boolean;
  266. }
  267. export function extractLabelMatchers(tokens: Array<string | Token>): AbstractLabelMatcher[] {
  268. const labelMatchers: AbstractLabelMatcher[] = [];
  269. for (let prop in tokens) {
  270. if (tokens[prop] instanceof Token) {
  271. let token: Token = tokens[prop] as Token;
  272. if (token.type === 'context-labels') {
  273. let labelKey = '';
  274. let labelValue = '';
  275. let labelOperator = '';
  276. let contentTokens: any[] = token.content as any[];
  277. for (let currentToken in contentTokens) {
  278. if (typeof contentTokens[currentToken] === 'string') {
  279. let currentStr: string;
  280. currentStr = contentTokens[currentToken] as string;
  281. if (currentStr === '=' || currentStr === '!=' || currentStr === '=~' || currentStr === '!~') {
  282. labelOperator = currentStr;
  283. }
  284. } else if (contentTokens[currentToken] instanceof Token) {
  285. switch (contentTokens[currentToken].type) {
  286. case 'label-key':
  287. labelKey = contentTokens[currentToken].content as string;
  288. break;
  289. case 'label-value':
  290. labelValue = contentTokens[currentToken].content as string;
  291. labelValue = labelValue.substring(1, labelValue.length - 1);
  292. const labelComparator = FromPromLikeMap[labelOperator];
  293. if (labelComparator) {
  294. labelMatchers.push({ name: labelKey, operator: labelComparator, value: labelValue });
  295. }
  296. break;
  297. }
  298. }
  299. }
  300. }
  301. }
  302. }
  303. return labelMatchers;
  304. }