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,
|
isCloudRulesSource,
|
||||||
isGrafanaRulesSource,
|
isGrafanaRulesSource,
|
||||||
} from '../utils/datasource';
|
} from '../utils/datasource';
|
||||||
|
import { hashQuery } from '../utils/rule-id';
|
||||||
import {
|
import {
|
||||||
isAlertingRule,
|
isAlertingRule,
|
||||||
isAlertingRulerRule,
|
isAlertingRulerRule,
|
||||||
@ -452,18 +453,6 @@ function isCombinedRuleEqualToPromRule(combinedRule: CombinedRule, rule: Rule, c
|
|||||||
return false;
|
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.
|
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
|
// 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 { renderHook } from '@testing-library/react-hooks';
|
||||||
|
|
||||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
import { AlertingRule, RecordingRule, RuleIdentifier } from 'app/types/unified-alerting';
|
||||||
import {
|
import {
|
||||||
GrafanaAlertStateDecision,
|
GrafanaAlertStateDecision,
|
||||||
GrafanaRuleDefinition,
|
GrafanaRuleDefinition,
|
||||||
|
PromAlertingRuleState,
|
||||||
|
PromRuleType,
|
||||||
RulerAlertingRuleDTO,
|
RulerAlertingRuleDTO,
|
||||||
RulerGrafanaRuleDTO,
|
RulerGrafanaRuleDTO,
|
||||||
RulerRecordingRuleDTO,
|
RulerRecordingRuleDTO,
|
||||||
} from 'app/types/unified-alerting-dto';
|
} 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', () => {
|
describe('hashRulerRule', () => {
|
||||||
it('should not hash unknown rule types', () => {
|
it('should not hash unknown rule types', () => {
|
||||||
@ -18,8 +53,9 @@ describe('hashRulerRule', () => {
|
|||||||
expect(() => {
|
expect(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
hashRulerRule(unknownRule);
|
hashRulerRule(unknownRule);
|
||||||
}).toThrowError();
|
}).toThrow('Only recording and alerting ruler rules can be hashed');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hash recording rules', () => {
|
it('should hash recording rules', () => {
|
||||||
const recordingRule: RulerRecordingRuleDTO = {
|
const recordingRule: RulerRecordingRuleDTO = {
|
||||||
record: 'instance:node_num_cpu:sum',
|
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', () => {
|
describe('useRuleIdFromPathname', () => {
|
||||||
it('should return undefined when there is no id in params', () => {
|
it('should return undefined when there is no id in params', () => {
|
||||||
const { result } = renderHook(() => {
|
const { result } = renderHook(() => {
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
RuleIdentifier,
|
RuleIdentifier,
|
||||||
RuleWithLocation,
|
RuleWithLocation,
|
||||||
} from 'app/types/unified-alerting';
|
} 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 { GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
||||||
import {
|
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;
|
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
|
// this is used to identify rules, mimir / loki rules do not have a unique identifier
|
||||||
export function hashRulerRule(rule: RulerRuleDTO): string {
|
export function hashRulerRule(rule: RulerRuleDTO): string {
|
||||||
if (isRecordingRulerRule(rule)) {
|
if (isGrafanaRulerRule(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)) {
|
|
||||||
return rule.grafana_alert.uid;
|
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 {
|
export function hashRule(rule: Rule): string {
|
||||||
|
const fingerprint = getPromRuleFingerprint(rule);
|
||||||
|
return hash(JSON.stringify(fingerprint)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPromRuleFingerprint(rule: Rule) {
|
||||||
if (isRecordingRule(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)) {
|
if (isAlertingRule(rule)) {
|
||||||
return hash(
|
return [
|
||||||
JSON.stringify([
|
rule.name,
|
||||||
rule.type,
|
PromRuleType.Alerting,
|
||||||
rule.query,
|
hashQuery(rule.query),
|
||||||
hashLabelsOrAnnotations(rule.annotations),
|
hashLabelsOrAnnotations(rule.annotations),
|
||||||
hashLabelsOrAnnotations(rule.labels),
|
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 {
|
export function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string {
|
||||||
|
Loading…
Reference in New Issue
Block a user