mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add compatibility between prometheus and ruler identifiers (#92496)
* Unify Prom and Ruler rules hash creation * Add tests, refarct prom hash
This commit is contained in:
parent
372d0acec8
commit
e0950a1283
@ -31,6 +31,7 @@ import {
|
||||
isCloudRulesSource,
|
||||
isGrafanaRulesSource,
|
||||
} from '../utils/datasource';
|
||||
import { hashQuery } from '../utils/rule-id';
|
||||
import {
|
||||
isAlertingRule,
|
||||
isAlertingRulerRule,
|
||||
@ -452,18 +453,6 @@ function isCombinedRuleEqualToPromRule(combinedRule: CombinedRule, rule: Rule, c
|
||||
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.slice(1, -1);
|
||||
}
|
||||
// whitespace could be added or removed
|
||||
query = query.replace(/\s|\n/g, '');
|
||||
// labels matchers can be reordered, so sort the enitre string, esentially comparing just the character counts
|
||||
return query.split('').sort().join('');
|
||||
}
|
||||
|
||||
/*
|
||||
This hook returns combined Grafana rules. Optionally, it can filter rules by dashboard UID and panel ID.
|
||||
*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`hashRulerRule should hash alerting rule 1`] = `"1465866290"`;
|
||||
exports[`hashRulerRule should hash alerting rule 1`] = `"7317348"`;
|
||||
|
||||
exports[`hashRulerRule should hash recording rules 1`] = `"2044193757"`;
|
||||
exports[`hashRulerRule should hash recording rules 1`] = `"-447747460"`;
|
||||
|
@ -1,15 +1,50 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { AlertingRule, RecordingRule, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import {
|
||||
GrafanaAlertStateDecision,
|
||||
GrafanaRuleDefinition,
|
||||
PromAlertingRuleState,
|
||||
PromRuleType,
|
||||
RulerAlertingRuleDTO,
|
||||
RulerGrafanaRuleDTO,
|
||||
RulerRecordingRuleDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { hashRulerRule, parse, stringifyIdentifier, getRuleIdFromPathname } from './rule-id';
|
||||
import { hashRulerRule, parse, stringifyIdentifier, getRuleIdFromPathname, hashRule, equal } from './rule-id';
|
||||
|
||||
const alertingRule = {
|
||||
prom: {
|
||||
name: 'cpu-over-90',
|
||||
query: 'cpu_usage_seconds_total{job="integrations/node_exporter"} > 90',
|
||||
labels: { type: 'cpu' },
|
||||
annotations: { description: 'CPU usage too high' },
|
||||
state: PromAlertingRuleState.Firing,
|
||||
type: PromRuleType.Alerting,
|
||||
health: 'ok',
|
||||
} satisfies AlertingRule,
|
||||
ruler: {
|
||||
alert: 'cpu-over-90',
|
||||
expr: 'cpu_usage_seconds_total{job="integrations/node_exporter"} > 90',
|
||||
labels: { type: 'cpu' },
|
||||
annotations: { description: 'CPU usage too high' },
|
||||
} satisfies RulerAlertingRuleDTO,
|
||||
};
|
||||
|
||||
const recordingRule = {
|
||||
prom: {
|
||||
name: 'instance:node_num_cpu:sum',
|
||||
type: PromRuleType.Recording,
|
||||
health: 'ok',
|
||||
query: 'count without (mode) (node_cpu_seconds_total{job="integrations/node_exporter"})',
|
||||
labels: { type: 'cpu' },
|
||||
} satisfies RecordingRule,
|
||||
ruler: {
|
||||
record: 'instance:node_num_cpu:sum',
|
||||
expr: 'count without (mode) (node_cpu_seconds_total{job="integrations/node_exporter"})',
|
||||
labels: { type: 'cpu' },
|
||||
} satisfies RulerRecordingRuleDTO,
|
||||
};
|
||||
|
||||
describe('hashRulerRule', () => {
|
||||
it('should not hash unknown rule types', () => {
|
||||
@ -18,8 +53,9 @@ describe('hashRulerRule', () => {
|
||||
expect(() => {
|
||||
// @ts-ignore
|
||||
hashRulerRule(unknownRule);
|
||||
}).toThrowError();
|
||||
}).toThrow('Only recording and alerting ruler rules can be hashed');
|
||||
});
|
||||
|
||||
it('should hash recording rules', () => {
|
||||
const recordingRule: RulerRecordingRuleDTO = {
|
||||
record: 'instance:node_num_cpu:sum',
|
||||
@ -117,6 +153,48 @@ describe('hashRulerRule', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashRule', () => {
|
||||
it('should produce hashRulerRule compatible hashes for alerting rules', () => {
|
||||
const promHash = hashRule(alertingRule.prom);
|
||||
const rulerHash = hashRulerRule(alertingRule.ruler);
|
||||
|
||||
expect(promHash).toBe(rulerHash);
|
||||
});
|
||||
|
||||
it('should produce hashRulerRule compatible hashes for recording rules', () => {
|
||||
const promHash = hashRule(recordingRule.prom);
|
||||
const rulerHash = hashRulerRule(recordingRule.ruler);
|
||||
|
||||
expect(promHash).toBe(rulerHash);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equal', () => {
|
||||
it('should return true for Prom and cloud identifiers with the same name, type, query and labels', () => {
|
||||
const promIdentifier: RuleIdentifier = {
|
||||
ruleSourceName: 'mimir-cloud',
|
||||
namespace: 'cloud-alerts',
|
||||
groupName: 'cpu-usage',
|
||||
ruleName: alertingRule.prom.name,
|
||||
ruleHash: hashRule(alertingRule.prom),
|
||||
};
|
||||
|
||||
const cloudIdentifier: RuleIdentifier = {
|
||||
ruleSourceName: 'mimir-cloud',
|
||||
namespace: 'cloud-alerts',
|
||||
groupName: 'cpu-usage',
|
||||
ruleName: alertingRule.ruler.alert,
|
||||
ruleHash: hashRulerRule(alertingRule.ruler),
|
||||
};
|
||||
|
||||
const promToCloud = equal(promIdentifier, cloudIdentifier);
|
||||
const cloudToProm = equal(cloudIdentifier, promIdentifier);
|
||||
|
||||
expect(promToCloud).toBe(true);
|
||||
expect(cloudToProm).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRuleIdFromPathname', () => {
|
||||
it('should return undefined when there is no id in params', () => {
|
||||
const { result } = renderHook(() => {
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
RuleIdentifier,
|
||||
RuleWithLocation,
|
||||
} from 'app/types/unified-alerting';
|
||||
import { Annotations, Labels, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
import { Annotations, Labels, PromRuleType, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
||||
import {
|
||||
@ -104,6 +104,28 @@ export function equal(a: RuleIdentifier, b: RuleIdentifier) {
|
||||
);
|
||||
}
|
||||
|
||||
// It might happen to compare Cloud and Prometheus identifiers for datasources with available Ruler API
|
||||
// It happends when the Ruler API timeouts and the UI cannot create Cloud identifiers, so it creates a Prometheus identifier instead.
|
||||
if (isCloudRuleIdentifier(a) && isPrometheusRuleIdentifier(b)) {
|
||||
return (
|
||||
a.groupName === b.groupName &&
|
||||
a.namespace === b.namespace &&
|
||||
a.ruleName === b.ruleName &&
|
||||
a.rulerRuleHash === b.ruleHash &&
|
||||
a.ruleSourceName === b.ruleSourceName
|
||||
);
|
||||
}
|
||||
|
||||
if (isPrometheusRuleIdentifier(a) && isCloudRuleIdentifier(b)) {
|
||||
return (
|
||||
a.groupName === b.groupName &&
|
||||
a.namespace === b.namespace &&
|
||||
a.ruleName === b.ruleName &&
|
||||
a.ruleHash === b.rulerRuleHash &&
|
||||
a.ruleSourceName === b.ruleSourceName
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -219,41 +241,61 @@ function hash(value: string): number {
|
||||
|
||||
// this is used to identify rules, mimir / loki rules do not have a unique identifier
|
||||
export function hashRulerRule(rule: RulerRuleDTO): string {
|
||||
if (isRecordingRulerRule(rule)) {
|
||||
return hash(JSON.stringify([rule.record, rule.expr, hashLabelsOrAnnotations(rule.labels)])).toString();
|
||||
} else if (isAlertingRulerRule(rule)) {
|
||||
return hash(
|
||||
JSON.stringify([
|
||||
rule.alert,
|
||||
rule.expr,
|
||||
hashLabelsOrAnnotations(rule.annotations),
|
||||
hashLabelsOrAnnotations(rule.labels),
|
||||
])
|
||||
).toString();
|
||||
} else if (isGrafanaRulerRule(rule)) {
|
||||
if (isGrafanaRulerRule(rule)) {
|
||||
return rule.grafana_alert.uid;
|
||||
} else {
|
||||
throw new Error('only recording and alerting ruler rules can be hashed');
|
||||
}
|
||||
|
||||
const fingerprint = getRulerRuleFingerprint(rule);
|
||||
return hash(JSON.stringify(fingerprint)).toString();
|
||||
}
|
||||
|
||||
function getRulerRuleFingerprint(rule: RulerRuleDTO) {
|
||||
if (isRecordingRulerRule(rule)) {
|
||||
return [rule.record, PromRuleType.Recording, hashQuery(rule.expr), hashLabelsOrAnnotations(rule.labels)];
|
||||
}
|
||||
if (isAlertingRulerRule(rule)) {
|
||||
return [
|
||||
rule.alert,
|
||||
PromRuleType.Alerting,
|
||||
hashQuery(rule.expr),
|
||||
hashLabelsOrAnnotations(rule.annotations),
|
||||
hashLabelsOrAnnotations(rule.labels),
|
||||
];
|
||||
}
|
||||
throw new Error('Only recording and alerting ruler rules can be hashed');
|
||||
}
|
||||
|
||||
export function hashRule(rule: Rule): string {
|
||||
const fingerprint = getPromRuleFingerprint(rule);
|
||||
return hash(JSON.stringify(fingerprint)).toString();
|
||||
}
|
||||
|
||||
function getPromRuleFingerprint(rule: Rule) {
|
||||
if (isRecordingRule(rule)) {
|
||||
return hash(JSON.stringify([rule.type, rule.query, hashLabelsOrAnnotations(rule.labels)])).toString();
|
||||
return [rule.name, PromRuleType.Recording, hashQuery(rule.query), hashLabelsOrAnnotations(rule.labels)];
|
||||
}
|
||||
|
||||
if (isAlertingRule(rule)) {
|
||||
return hash(
|
||||
JSON.stringify([
|
||||
rule.type,
|
||||
rule.query,
|
||||
hashLabelsOrAnnotations(rule.annotations),
|
||||
hashLabelsOrAnnotations(rule.labels),
|
||||
])
|
||||
).toString();
|
||||
return [
|
||||
rule.name,
|
||||
PromRuleType.Alerting,
|
||||
hashQuery(rule.query),
|
||||
hashLabelsOrAnnotations(rule.annotations),
|
||||
hashLabelsOrAnnotations(rule.labels),
|
||||
];
|
||||
}
|
||||
throw new Error('Only recording and alerting rules can be hashed');
|
||||
}
|
||||
|
||||
throw new Error('only recording and alerting rules can be hashed');
|
||||
// there can be slight differences in how prom & ruler render a query, this will hash them accounting for the differences
|
||||
export function hashQuery(query: string) {
|
||||
// one of them might be wrapped in parens
|
||||
if (query.length > 1 && query[0] === '(' && query[query.length - 1] === ')') {
|
||||
query = query.slice(1, -1);
|
||||
}
|
||||
// whitespace could be added or removed
|
||||
query = query.replace(/\s|\n/g, '');
|
||||
// labels matchers can be reordered, so sort the enitre string, esentially comparing just the character counts
|
||||
return query.split('').sort().join('');
|
||||
}
|
||||
|
||||
export function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string {
|
||||
|
Loading…
Reference in New Issue
Block a user