rule-id.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import { CombinedRule, Rule, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting';
  2. import { Annotations, Labels, RulerRuleDTO } from 'app/types/unified-alerting-dto';
  3. import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
  4. import {
  5. isAlertingRule,
  6. isAlertingRulerRule,
  7. isCloudRuleIdentifier,
  8. isGrafanaRuleIdentifier,
  9. isGrafanaRulerRule,
  10. isPrometheusRuleIdentifier,
  11. isRecordingRule,
  12. isRecordingRulerRule,
  13. } from './rules';
  14. export function fromRulerRule(
  15. ruleSourceName: string,
  16. namespace: string,
  17. groupName: string,
  18. rule: RulerRuleDTO
  19. ): RuleIdentifier {
  20. if (isGrafanaRulerRule(rule)) {
  21. return { uid: rule.grafana_alert.uid!, ruleSourceName: 'grafana' };
  22. }
  23. return {
  24. ruleSourceName,
  25. namespace,
  26. groupName,
  27. rulerRuleHash: hashRulerRule(rule),
  28. };
  29. }
  30. export function fromRule(ruleSourceName: string, namespace: string, groupName: string, rule: Rule): RuleIdentifier {
  31. return {
  32. ruleSourceName,
  33. namespace,
  34. groupName,
  35. ruleHash: hashRule(rule),
  36. };
  37. }
  38. export function fromCombinedRule(ruleSourceName: string, rule: CombinedRule): RuleIdentifier {
  39. const namespaceName = rule.namespace.name;
  40. const groupName = rule.group.name;
  41. if (rule.rulerRule) {
  42. return fromRulerRule(ruleSourceName, namespaceName, groupName, rule.rulerRule);
  43. }
  44. if (rule.promRule) {
  45. return fromRule(ruleSourceName, namespaceName, groupName, rule.promRule);
  46. }
  47. throw new Error('Could not create an id for a rule that is missing both `rulerRule` and `promRule`.');
  48. }
  49. export function fromRuleWithLocation(rule: RuleWithLocation): RuleIdentifier {
  50. return fromRulerRule(rule.ruleSourceName, rule.namespace, rule.group.name, rule.rule);
  51. }
  52. export function equal(a: RuleIdentifier, b: RuleIdentifier) {
  53. if (isGrafanaRuleIdentifier(a) && isGrafanaRuleIdentifier(b)) {
  54. return a.uid === b.uid;
  55. }
  56. if (isCloudRuleIdentifier(a) && isCloudRuleIdentifier(b)) {
  57. return (
  58. a.groupName === b.groupName &&
  59. a.namespace === b.namespace &&
  60. a.rulerRuleHash === b.rulerRuleHash &&
  61. a.ruleSourceName === b.ruleSourceName
  62. );
  63. }
  64. if (isPrometheusRuleIdentifier(a) && isPrometheusRuleIdentifier(b)) {
  65. return (
  66. a.groupName === b.groupName &&
  67. a.namespace === b.namespace &&
  68. a.ruleHash === b.ruleHash &&
  69. a.ruleSourceName === b.ruleSourceName
  70. );
  71. }
  72. return false;
  73. }
  74. const cloudRuleIdentifierPrefix = 'cri';
  75. const prometheusRuleIdentifierPrefix = 'pri';
  76. function escapeDollars(value: string): string {
  77. return value.replace(/\$/g, '_DOLLAR_');
  78. }
  79. function unesacapeDollars(value: string): string {
  80. return value.replace(/\_DOLLAR\_/g, '$');
  81. }
  82. export function parse(value: string, decodeFromUri = false): RuleIdentifier {
  83. const source = decodeFromUri ? decodeURIComponent(value) : value;
  84. const parts = source.split('$');
  85. if (parts.length === 1) {
  86. return { uid: value, ruleSourceName: 'grafana' };
  87. }
  88. if (parts.length === 5) {
  89. const [prefix, ruleSourceName, namespace, groupName, hash] = parts.map(unesacapeDollars);
  90. if (prefix === cloudRuleIdentifierPrefix) {
  91. return { ruleSourceName, namespace, groupName, rulerRuleHash: Number(hash) };
  92. }
  93. if (prefix === prometheusRuleIdentifierPrefix) {
  94. return { ruleSourceName, namespace, groupName, ruleHash: Number(hash) };
  95. }
  96. }
  97. throw new Error(`Failed to parse rule location: ${value}`);
  98. }
  99. export function tryParse(value: string | undefined, decodeFromUri = false): RuleIdentifier | undefined {
  100. if (!value) {
  101. return;
  102. }
  103. try {
  104. return parse(value, decodeFromUri);
  105. } catch (error) {
  106. return;
  107. }
  108. }
  109. export function stringifyIdentifier(identifier: RuleIdentifier): string {
  110. if (isGrafanaRuleIdentifier(identifier)) {
  111. return identifier.uid;
  112. }
  113. if (isCloudRuleIdentifier(identifier)) {
  114. return [
  115. cloudRuleIdentifierPrefix,
  116. identifier.ruleSourceName,
  117. identifier.namespace,
  118. identifier.groupName,
  119. identifier.rulerRuleHash,
  120. ]
  121. .map(String)
  122. .map(escapeDollars)
  123. .join('$');
  124. }
  125. return [
  126. prometheusRuleIdentifierPrefix,
  127. identifier.ruleSourceName,
  128. identifier.namespace,
  129. identifier.groupName,
  130. identifier.ruleHash,
  131. ]
  132. .map(String)
  133. .map(escapeDollars)
  134. .join('$');
  135. }
  136. function hash(value: string): number {
  137. let hash = 0;
  138. if (value.length === 0) {
  139. return hash;
  140. }
  141. for (var i = 0; i < value.length; i++) {
  142. var char = value.charCodeAt(i);
  143. hash = (hash << 5) - hash + char;
  144. hash = hash & hash; // Convert to 32bit integer
  145. }
  146. return hash;
  147. }
  148. // this is used to identify lotex rules, as they do not have a unique identifier
  149. function hashRulerRule(rule: RulerRuleDTO): number {
  150. if (isRecordingRulerRule(rule)) {
  151. return hash(JSON.stringify([rule.record, rule.expr, hashLabelsOrAnnotations(rule.labels)]));
  152. } else if (isAlertingRulerRule(rule)) {
  153. return hash(
  154. JSON.stringify([
  155. rule.alert,
  156. rule.expr,
  157. hashLabelsOrAnnotations(rule.annotations),
  158. hashLabelsOrAnnotations(rule.labels),
  159. ])
  160. );
  161. } else {
  162. throw new Error('only recording and alerting ruler rules can be hashed');
  163. }
  164. }
  165. function hashRule(rule: Rule): number {
  166. if (isRecordingRule(rule)) {
  167. return hash(JSON.stringify([rule.type, rule.query, hashLabelsOrAnnotations(rule.labels)]));
  168. }
  169. if (isAlertingRule(rule)) {
  170. return hash(
  171. JSON.stringify([
  172. rule.type,
  173. rule.query,
  174. hashLabelsOrAnnotations(rule.annotations),
  175. hashLabelsOrAnnotations(rule.labels),
  176. ])
  177. );
  178. }
  179. throw new Error('only recording and alerting rules can be hashed');
  180. }
  181. function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string {
  182. return JSON.stringify(Object.entries(item || {}).sort((a, b) => a[0].localeCompare(b[0])));
  183. }
  184. export function ruleIdentifierToRuleSourceName(identifier: RuleIdentifier): string {
  185. return isGrafanaRuleIdentifier(identifier) ? GRAFANA_RULES_SOURCE_NAME : identifier.ruleSourceName;
  186. }