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