Alerting: Add compatibility between prometheus and ruler identifiers (#92496)

* Unify Prom and Ruler rules hash creation

* Add tests, refarct prom hash
pull/92733/head
Konrad Lalik 10 months ago committed by GitHub
parent 372d0acec8
commit e0950a1283
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts
  2. 4
      public/app/features/alerting/unified/utils/__snapshots__/rule-id.test.ts.snap
  3. 84
      public/app/features/alerting/unified/utils/rule-id.test.ts
  4. 84
      public/app/features/alerting/unified/utils/rule-id.ts

@ -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 (isGrafanaRulerRule(rule)) {
return rule.grafana_alert.uid;
}
const fingerprint = getRulerRuleFingerprint(rule);
return hash(JSON.stringify(fingerprint)).toString();
}
function getRulerRuleFingerprint(rule: RulerRuleDTO) {
if (isRecordingRulerRule(rule)) {
return hash(JSON.stringify([rule.record, rule.expr, hashLabelsOrAnnotations(rule.labels)])).toString();
} else if (isAlertingRulerRule(rule)) {
return hash(
JSON.stringify([
return [rule.record, PromRuleType.Recording, hashQuery(rule.expr), hashLabelsOrAnnotations(rule.labels)];
}
if (isAlertingRulerRule(rule)) {
return [
rule.alert,
rule.expr,
PromRuleType.Alerting,
hashQuery(rule.expr),
hashLabelsOrAnnotations(rule.annotations),
hashLabelsOrAnnotations(rule.labels),
])
).toString();
} else if (isGrafanaRulerRule(rule)) {
return rule.grafana_alert.uid;
} else {
throw new Error('only recording and alerting ruler rules can be hashed');
];
}
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,
return [
rule.name,
PromRuleType.Alerting,
hashQuery(rule.query),
hashLabelsOrAnnotations(rule.annotations),
hashLabelsOrAnnotations(rule.labels),
])
).toString();
];
}
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…
Cancel
Save