add_label_to_query.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import _ from 'lodash';
  2. const keywords = 'by|without|on|ignoring|group_left|group_right|bool|or|and|unless';
  3. // Duplicate from mode-prometheus.js, which can't be used in tests due to global ace not being loaded.
  4. const builtInWords = [
  5. keywords,
  6. 'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile',
  7. 'true|false|null|__name__|job',
  8. 'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv',
  9. 'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2',
  10. 'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time',
  11. 'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time',
  12. ]
  13. .join('|')
  14. .split('|');
  15. const metricNameRegexp = /([A-Za-z:][\w:]*)\b(?![\(\]{=!",])/g;
  16. const selectorRegexp = /{([^{]*)}/g;
  17. // addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}'
  18. export function addLabelToQuery(query: string, key: string, value: string, operator?: string): string {
  19. if (!key || !value) {
  20. throw new Error('Need label to add to query.');
  21. }
  22. // Add empty selectors to bare metric names
  23. let previousWord;
  24. query = query.replace(metricNameRegexp, (match, word, offset) => {
  25. const insideSelector = isPositionInsideChars(query, offset, '{', '}');
  26. // Handle "sum by (key) (metric)"
  27. const previousWordIsKeyWord = previousWord && keywords.split('|').indexOf(previousWord) > -1;
  28. previousWord = word;
  29. if (!insideSelector && !previousWordIsKeyWord && builtInWords.indexOf(word) === -1) {
  30. return `${word}{}`;
  31. }
  32. return word;
  33. });
  34. // Adding label to existing selectors
  35. let match = selectorRegexp.exec(query);
  36. const parts = [];
  37. let lastIndex = 0;
  38. let suffix = '';
  39. while (match) {
  40. const prefix = query.slice(lastIndex, match.index);
  41. const selector = match[1];
  42. const selectorWithLabel = addLabelToSelector(selector, key, value, operator);
  43. lastIndex = match.index + match[1].length + 2;
  44. suffix = query.slice(match.index + match[0].length);
  45. parts.push(prefix, selectorWithLabel);
  46. match = selectorRegexp.exec(query);
  47. }
  48. parts.push(suffix);
  49. return parts.join('');
  50. }
  51. const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g;
  52. export function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) {
  53. const parsedLabels = [];
  54. // Split selector into labels
  55. if (selector) {
  56. let match = labelRegexp.exec(selector);
  57. while (match) {
  58. parsedLabels.push({ key: match[1], operator: match[2], value: match[3] });
  59. match = labelRegexp.exec(selector);
  60. }
  61. }
  62. // Add new label
  63. const operatorForLabelKey = labelOperator || '=';
  64. parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` });
  65. // Sort labels by key and put them together
  66. const formatted = _.chain(parsedLabels)
  67. .uniqWith(_.isEqual)
  68. .compact()
  69. .sortBy('key')
  70. .map(({ key, operator, value }) => `${key}${operator}${value}`)
  71. .value()
  72. .join(',');
  73. return `{${formatted}}`;
  74. }
  75. function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) {
  76. const nextSelectorStart = text.slice(position).indexOf(openChar);
  77. const nextSelectorEnd = text.slice(position).indexOf(closeChar);
  78. return nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd);
  79. }
  80. export default addLabelToQuery;