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