2021-04-16 03:08:26 -05:00
|
|
|
import {
|
|
|
|
CombinedRule,
|
|
|
|
CombinedRuleGroup,
|
|
|
|
CombinedRuleNamespace,
|
|
|
|
Rule,
|
2021-04-16 04:08:08 -05:00
|
|
|
RuleGroup,
|
2021-04-16 03:08:26 -05:00
|
|
|
RuleNamespace,
|
|
|
|
RulesSource,
|
|
|
|
} from 'app/types/unified-alerting';
|
2021-04-16 04:08:08 -05:00
|
|
|
import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
2021-04-07 00:42:43 -05:00
|
|
|
import { useMemo, useRef } from 'react';
|
2021-05-17 02:39:42 -05:00
|
|
|
import {
|
|
|
|
getAllRulesSources,
|
|
|
|
getRulesSourceByName,
|
|
|
|
isCloudRulesSource,
|
|
|
|
isGrafanaRulesSource,
|
|
|
|
} from '../utils/datasource';
|
2021-04-14 07:57:36 -05:00
|
|
|
import { isAlertingRule, isAlertingRulerRule, isRecordingRulerRule } from '../utils/rules';
|
2021-04-07 00:42:43 -05:00
|
|
|
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
|
|
|
|
|
|
|
interface CacheValue {
|
|
|
|
promRules?: RuleNamespace[];
|
|
|
|
rulerRules?: RulerRulesConfigDTO | null;
|
|
|
|
result: CombinedRuleNamespace[];
|
|
|
|
}
|
|
|
|
|
2021-07-01 05:02:41 -05:00
|
|
|
// this little monster combines prometheus rules and ruler rules to produce a unified data structure
|
2021-05-17 02:39:42 -05:00
|
|
|
// can limit to a single rules source
|
|
|
|
export function useCombinedRuleNamespaces(rulesSourceName?: string): CombinedRuleNamespace[] {
|
2021-04-07 00:42:43 -05:00
|
|
|
const promRulesResponses = useUnifiedAlertingSelector((state) => state.promRules);
|
|
|
|
const rulerRulesResponses = useUnifiedAlertingSelector((state) => state.rulerRules);
|
|
|
|
|
|
|
|
// cache results per rules source, so we only recalculate those for which results have actually changed
|
|
|
|
const cache = useRef<Record<string, CacheValue>>({});
|
|
|
|
|
2021-05-17 02:39:42 -05:00
|
|
|
const rulesSources = useMemo((): RulesSource[] => {
|
|
|
|
if (rulesSourceName) {
|
|
|
|
const rulesSource = getRulesSourceByName(rulesSourceName);
|
|
|
|
if (!rulesSource) {
|
|
|
|
throw new Error(`Unknown rules source: ${rulesSourceName}`);
|
|
|
|
}
|
|
|
|
return [rulesSource];
|
|
|
|
}
|
|
|
|
return getAllRulesSources();
|
|
|
|
}, [rulesSourceName]);
|
|
|
|
|
2021-04-16 04:08:08 -05:00
|
|
|
return useMemo(
|
|
|
|
() =>
|
2021-05-17 02:39:42 -05:00
|
|
|
rulesSources
|
2021-04-16 04:08:08 -05:00
|
|
|
.map((rulesSource): CombinedRuleNamespace[] => {
|
|
|
|
const rulesSourceName = isCloudRulesSource(rulesSource) ? rulesSource.name : rulesSource;
|
|
|
|
const promRules = promRulesResponses[rulesSourceName]?.result;
|
|
|
|
const rulerRules = rulerRulesResponses[rulesSourceName]?.result;
|
2021-04-07 00:42:43 -05:00
|
|
|
|
2021-04-16 04:08:08 -05:00
|
|
|
const cached = cache.current[rulesSourceName];
|
|
|
|
if (cached && cached.promRules === promRules && cached.rulerRules === rulerRules) {
|
|
|
|
return cached.result;
|
|
|
|
}
|
|
|
|
const namespaces: Record<string, CombinedRuleNamespace> = {};
|
2021-04-07 00:42:43 -05:00
|
|
|
|
2021-04-16 04:08:08 -05:00
|
|
|
// first get all the ruler rules in
|
|
|
|
Object.entries(rulerRules || {}).forEach(([namespaceName, groups]) => {
|
|
|
|
const namespace: CombinedRuleNamespace = {
|
|
|
|
rulesSource,
|
|
|
|
name: namespaceName,
|
|
|
|
groups: [],
|
|
|
|
};
|
|
|
|
namespaces[namespaceName] = namespace;
|
|
|
|
addRulerGroupsToCombinedNamespace(namespace, groups);
|
2021-04-07 00:42:43 -05:00
|
|
|
});
|
|
|
|
|
2021-04-16 04:08:08 -05:00
|
|
|
// then correlate with prometheus rules
|
|
|
|
promRules?.forEach(({ name: namespaceName, groups }) => {
|
|
|
|
const ns = (namespaces[namespaceName] = namespaces[namespaceName] || {
|
|
|
|
rulesSource,
|
|
|
|
name: namespaceName,
|
|
|
|
groups: [],
|
2021-04-07 00:42:43 -05:00
|
|
|
});
|
|
|
|
|
2021-04-16 04:08:08 -05:00
|
|
|
addPromGroupsToCombinedNamespace(ns, groups);
|
2021-04-14 07:57:36 -05:00
|
|
|
});
|
2021-04-16 04:08:08 -05:00
|
|
|
|
|
|
|
const result = Object.values(namespaces);
|
|
|
|
if (isGrafanaRulesSource(rulesSource)) {
|
|
|
|
// merge all groups in case of grafana managed, essentially treating namespaces (folders) as gorups
|
|
|
|
result.forEach((namespace) => {
|
|
|
|
namespace.groups = [
|
|
|
|
{
|
|
|
|
name: 'default',
|
|
|
|
rules: namespace.groups.flatMap((g) => g.rules).sort((a, b) => a.name.localeCompare(b.name)),
|
|
|
|
},
|
|
|
|
];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
cache.current[rulesSourceName] = { promRules, rulerRules, result };
|
|
|
|
return result;
|
|
|
|
})
|
|
|
|
.flat(),
|
2021-05-17 02:39:42 -05:00
|
|
|
[promRulesResponses, rulerRulesResponses, rulesSources]
|
2021-04-16 04:08:08 -05:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function addRulerGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, groups: RulerRuleGroupDTO[]): void {
|
|
|
|
namespace.groups = groups.map((group) => {
|
|
|
|
const combinedGroup: CombinedRuleGroup = {
|
|
|
|
name: group.name,
|
2021-08-26 08:40:27 -05:00
|
|
|
interval: group.interval,
|
2021-04-16 04:08:08 -05:00
|
|
|
rules: [],
|
|
|
|
};
|
|
|
|
combinedGroup.rules = group.rules.map((rule) => rulerRuleToCombinedRule(rule, namespace, combinedGroup));
|
|
|
|
return combinedGroup;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function addPromGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, groups: RuleGroup[]): void {
|
|
|
|
groups.forEach((group) => {
|
|
|
|
let combinedGroup = namespace.groups.find((g) => g.name === group.name);
|
|
|
|
if (!combinedGroup) {
|
|
|
|
combinedGroup = {
|
|
|
|
name: group.name,
|
|
|
|
rules: [],
|
|
|
|
};
|
|
|
|
namespace.groups.push(combinedGroup);
|
|
|
|
}
|
|
|
|
|
|
|
|
(group.rules ?? []).forEach((rule) => {
|
|
|
|
const existingRule = getExistingRuleInGroup(rule, combinedGroup!, namespace.rulesSource);
|
|
|
|
if (existingRule) {
|
|
|
|
existingRule.promRule = rule;
|
|
|
|
} else {
|
|
|
|
combinedGroup!.rules.push(promRuleToCombinedRule(rule, namespace, combinedGroup!));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function promRuleToCombinedRule(rule: Rule, namespace: CombinedRuleNamespace, group: CombinedRuleGroup): CombinedRule {
|
|
|
|
return {
|
|
|
|
name: rule.name,
|
|
|
|
query: rule.query,
|
|
|
|
labels: rule.labels || {},
|
|
|
|
annotations: isAlertingRule(rule) ? rule.annotations || {} : {},
|
|
|
|
promRule: rule,
|
|
|
|
namespace: namespace,
|
|
|
|
group,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function rulerRuleToCombinedRule(
|
|
|
|
rule: RulerRuleDTO,
|
|
|
|
namespace: CombinedRuleNamespace,
|
|
|
|
group: CombinedRuleGroup
|
|
|
|
): CombinedRule {
|
|
|
|
return isAlertingRulerRule(rule)
|
|
|
|
? {
|
|
|
|
name: rule.alert,
|
|
|
|
query: rule.expr,
|
|
|
|
labels: rule.labels || {},
|
|
|
|
annotations: rule.annotations || {},
|
|
|
|
rulerRule: rule,
|
|
|
|
namespace,
|
|
|
|
group,
|
|
|
|
}
|
|
|
|
: isRecordingRulerRule(rule)
|
|
|
|
? {
|
|
|
|
name: rule.record,
|
|
|
|
query: rule.expr,
|
|
|
|
labels: rule.labels || {},
|
|
|
|
annotations: {},
|
|
|
|
rulerRule: rule,
|
|
|
|
namespace,
|
|
|
|
group,
|
|
|
|
}
|
|
|
|
: {
|
|
|
|
name: rule.grafana_alert.title,
|
|
|
|
query: '',
|
2021-04-19 04:53:02 -05:00
|
|
|
labels: rule.labels || {},
|
|
|
|
annotations: rule.annotations || {},
|
2021-04-16 04:08:08 -05:00
|
|
|
rulerRule: rule,
|
|
|
|
namespace,
|
|
|
|
group,
|
|
|
|
};
|
2021-04-07 00:42:43 -05:00
|
|
|
}
|
|
|
|
|
2021-05-14 05:41:13 -05:00
|
|
|
// find existing rule in group that matches the given prom rule
|
2021-04-16 03:08:26 -05:00
|
|
|
function getExistingRuleInGroup(
|
|
|
|
rule: Rule,
|
|
|
|
group: CombinedRuleGroup,
|
|
|
|
rulesSource: RulesSource
|
|
|
|
): CombinedRule | undefined {
|
2021-05-14 05:41:13 -05:00
|
|
|
if (isGrafanaRulesSource(rulesSource)) {
|
|
|
|
// assume grafana groups have only the one rule. check name anyway because paranoid
|
|
|
|
return group!.rules.find((existingRule) => existingRule.name === rule.name);
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
// try finding a rule that matches name, labels, annotations and query
|
|
|
|
group!.rules.find(
|
|
|
|
(existingRule) => !existingRule.promRule && isCombinedRuleEqualToPromRule(existingRule, rule, true)
|
|
|
|
) ??
|
|
|
|
// if that fails, try finding a rule that only matches name, labels and annotations.
|
|
|
|
// loki & prom can sometimes modify the query so it doesnt match, eg `2 > 1` becomes `1`
|
|
|
|
group!.rules.find(
|
|
|
|
(existingRule) => !existingRule.promRule && isCombinedRuleEqualToPromRule(existingRule, rule, false)
|
|
|
|
)
|
|
|
|
);
|
2021-04-16 03:08:26 -05:00
|
|
|
}
|
|
|
|
|
2021-05-14 05:41:13 -05:00
|
|
|
function isCombinedRuleEqualToPromRule(combinedRule: CombinedRule, rule: Rule, checkQuery = true): boolean {
|
2021-04-07 00:42:43 -05:00
|
|
|
if (combinedRule.name === rule.name) {
|
|
|
|
return (
|
2021-05-14 05:41:13 -05:00
|
|
|
JSON.stringify([
|
|
|
|
checkQuery ? hashQuery(combinedRule.query) : '',
|
|
|
|
combinedRule.labels,
|
|
|
|
combinedRule.annotations,
|
|
|
|
]) ===
|
|
|
|
JSON.stringify([
|
|
|
|
checkQuery ? hashQuery(rule.query) : '',
|
|
|
|
rule.labels || {},
|
|
|
|
isAlertingRule(rule) ? rule.annotations || {} : {},
|
|
|
|
])
|
2021-04-07 00:42:43 -05:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// there can be slight differences in how prom & ruler render a query, this will hash them accounting for the differences
|
|
|
|
function hashQuery(query: string) {
|
|
|
|
// one of them might be wrapped in parens
|
|
|
|
if (query.length > 1 && query[0] === '(' && query[query.length - 1] === ')') {
|
|
|
|
query = query.substr(1, query.length - 2);
|
|
|
|
}
|
|
|
|
// whitespace could be added or removed
|
|
|
|
query = query.replace(/\s|\n/g, '');
|
2021-05-14 05:41:13 -05:00
|
|
|
// labels matchers can be reordered, so sort the enitre string, esentially comparing just the character counts
|
2021-04-07 00:42:43 -05:00
|
|
|
return query.split('').sort().join('');
|
|
|
|
}
|