Skip to content

Commit ed8c141

Browse files
committed
Add score details, add tests
1 parent 0fc73c2 commit ed8c141

File tree

4 files changed

+128
-26
lines changed

4 files changed

+128
-26
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { AlertStates } from '@openshift-console/dynamic-plugin-sdk';
2+
import { AlertWithRuleName, ByResource, computeAlertScore, computeScore } from '../helper';
3+
4+
const mockAlert = (
5+
name: string,
6+
severity: string,
7+
state: string,
8+
threshold: number,
9+
value: number
10+
): AlertWithRuleName => {
11+
return {
12+
ruleName: name,
13+
labels: { alertname: name, severity: severity },
14+
state: state as AlertStates,
15+
annotations: {},
16+
ruleID: '',
17+
metadata: { thresholdF: threshold, threshold: '', unit: '%' },
18+
value: value
19+
};
20+
};
21+
22+
describe('health helpers', () => {
23+
it('should compute unweighted alert min score', () => {
24+
const alert = mockAlert('test', 'critical', 'firing', 10, 10);
25+
const score = computeAlertScore(alert);
26+
expect(score.rawScore).toBeCloseTo(9.47, 2);
27+
expect(score.weight).toEqual(1);
28+
});
29+
30+
it('should compute unweighted alert max score', () => {
31+
const alert = mockAlert('test', 'critical', 'firing', 10, 100);
32+
const score = computeAlertScore(alert);
33+
expect(score.rawScore).toEqual(0);
34+
expect(score.weight).toEqual(1);
35+
});
36+
37+
it('should compute weighted alert score', () => {
38+
const alert = mockAlert('test', 'info', 'pending', 10, 10);
39+
const score = computeAlertScore(alert);
40+
expect(score.rawScore).toBeCloseTo(9.47, 2);
41+
expect(score.weight).toEqual(0.12);
42+
});
43+
44+
it('should compute full score', () => {
45+
// Start with an empty one => max score
46+
const r: ByResource = {
47+
name: 'test',
48+
critical: { firing: [], pending: [], silenced: [], inactive: [] },
49+
warning: { firing: [], pending: [], silenced: [], inactive: [] },
50+
other: { firing: [], pending: [], silenced: [], inactive: [] },
51+
score: { total: 0, details: [] }
52+
};
53+
expect(computeScore(r).total).toEqual(10);
54+
55+
// Add 3 inactive alerts => still max score
56+
r.critical.inactive.push('test-critical');
57+
r.warning.inactive.push('test-warning');
58+
r.other.inactive.push('test-info');
59+
expect(computeScore(r).total).toEqual(10);
60+
61+
// Turn the inactive info into pending => slightly decreasing score
62+
r.other.inactive = [];
63+
r.other.pending = [mockAlert('test-info', 'info', 'pending', 10, 20)];
64+
expect(computeScore(r).total).toBeCloseTo(9.9, 1);
65+
66+
// Turn the inactive warning into firing => more decreasing score
67+
r.warning.inactive = [];
68+
r.warning.firing = [mockAlert('test-warning', 'warning', 'firing', 10, 40)];
69+
expect(computeScore(r).total).toBeCloseTo(8.5, 1);
70+
71+
// Turn the inactive critical into firing => more decrease
72+
r.critical.inactive = [];
73+
r.critical.firing = [mockAlert('test-critical', 'critical', 'firing', 10, 40)];
74+
expect(computeScore(r).total).toBeCloseTo(6.5, 1);
75+
});
76+
});

web/src/components/health/health-card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const HealthCard: React.FC<HealthCardProps> = ({ stats, kind, isDark, isS
9696
</FlexItem>
9797
<FlexItem>
9898
<TextContent>
99-
<Text component={TextVariants.h1}>{valueFormat(stats.score)}</Text>
99+
<Text component={TextVariants.h1}>{valueFormat(stats.score.total)}</Text>
100100
</TextContent>
101101
</FlexItem>
102102
</Flex>

web/src/components/health/health-heatmap.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as React from 'react';
55
import { useTranslation } from 'react-i18next';
66
import { valueFormat } from '../../utils/format';
77
import { AlertDetails, AlertDetailsValue } from './alert-details';
8-
import { AlertWithRuleName, ByResource, computeAlertScore, getAllAlerts } from './helper';
8+
import { AlertWithRuleName, ByResource, computeExcessRatioStatusWeighted, getAllAlerts } from './helper';
99

1010
import './heatmap.css';
1111

@@ -132,7 +132,7 @@ export const HealthHeatmap: React.FC<HealthHeatmapProps> = ({ info, interactive
132132
: a.labels.severity === 'warning'
133133
? warningColorMap
134134
: infoColorMap;
135-
const score = computeAlertScore(a, true);
135+
const score = computeExcessRatioStatusWeighted(a);
136136
return {
137137
score,
138138
colorMap: colorMap,

web/src/components/health/helper.ts

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type ByResource = {
1313
critical: SeverityStats;
1414
warning: SeverityStats;
1515
other: SeverityStats;
16-
score: number;
16+
score: Score;
1717
};
1818

1919
type SeverityStats = {
@@ -41,6 +41,17 @@ type HealthMetadata = {
4141
namespaceLabels?: string[];
4242
};
4343

44+
type Score = {
45+
total: number;
46+
details: ScoreDetail[];
47+
};
48+
49+
type ScoreDetail = {
50+
alertName: string;
51+
rawScore: number;
52+
weight: number;
53+
};
54+
4455
export const getHealthMetadata = (annotations: PrometheusLabels): HealthMetadata | undefined => {
4556
if ('netobserv_io_network_health' in annotations) {
4657
const md = (JSON.parse(annotations['netobserv_io_network_health']) as HealthMetadata) || undefined;
@@ -141,7 +152,7 @@ const statsFromGrouped = (name: string, grouped: AlertWithRuleName[]): ByResourc
141152
critical: { firing: [], pending: [], silenced: [], inactive: [] },
142153
warning: { firing: [], pending: [], silenced: [], inactive: [] },
143154
other: { firing: [], pending: [], silenced: [], inactive: [] },
144-
score: 0
155+
score: { total: 0, details: [] }
145156
};
146157
_.uniqWith(grouped, (a, b) => {
147158
return a.ruleName === b.ruleName && _.isEqual(a.labels, b.labels);
@@ -241,37 +252,52 @@ const getSeverityWeight = (a: AlertWithRuleName) => {
241252
}
242253
};
243254

255+
const getStateWeight = (a: AlertWithRuleName) => {
256+
switch (a.state) {
257+
case 'pending':
258+
return pendingWeight;
259+
case 'silenced':
260+
return silencedWeight;
261+
}
262+
return 1;
263+
};
264+
244265
// Score [0,10]; higher is better
245-
export const computeScore = (r: ByResource): number => {
266+
export const computeScore = (r: ByResource): Score => {
246267
const allAlerts = getAllAlerts(r);
247-
const score = allAlerts.map(a => computeAlertScore(a)).reduce((a, b) => a + b, 0);
248-
if (score === 0) {
249-
return 10;
268+
const allScores = allAlerts
269+
.map(computeAlertScore)
270+
.concat(r.critical.inactive.map(name => ({ alertName: name, rawScore: 10, weight: criticalWeight })))
271+
.concat(r.warning.inactive.map(name => ({ alertName: name, rawScore: 10, weight: warningWeight })))
272+
.concat(r.other.inactive.map(name => ({ alertName: name, rawScore: 10, weight: minorWeight })));
273+
const sum = allScores.map(s => s.rawScore * s.weight).reduce((a, b) => a + b, 0);
274+
const sumWeights = allScores.map(s => s.weight).reduce((a, b) => a + b, 0);
275+
if (sumWeights === 0) {
276+
return { total: 10, details: [] };
250277
}
251-
const div =
252-
allAlerts.map(getSeverityWeight).reduce((a, b) => a + b, 0) +
253-
r.critical.inactive.length * criticalWeight +
254-
r.warning.inactive.length * warningWeight +
255-
r.other.inactive.length * minorWeight;
256-
return 10 * (1 - score / div);
278+
return { total: sum / sumWeights, details: allScores };
257279
};
258280

259281
// Score [0,1]; lower is better
260-
export const computeAlertScore = (a: AlertWithRuleName, ignoreSeverity?: boolean): number => {
261-
let multiplier = ignoreSeverity ? 1 : getSeverityWeight(a);
262-
switch (a.state) {
263-
case 'pending':
264-
multiplier *= pendingWeight;
265-
break;
266-
case 'silenced':
267-
multiplier *= silencedWeight;
268-
break;
269-
}
282+
const computeExcessRatio = (a: AlertWithRuleName): number => {
270283
// Assuming the alert value is a [0-100] percentage. Needs update if more use cases come up.
271284
const threshold = (a.metadata?.thresholdF || 0) / 2;
272285
const range = 100 - threshold;
273286
const excess = Math.max((a.value as number) - threshold, 0);
274-
return (excess * multiplier) / range;
287+
return excess / range;
288+
};
289+
290+
export const computeExcessRatioStatusWeighted = (a: AlertWithRuleName): number => {
291+
return computeExcessRatio(a) * getStateWeight(a);
292+
};
293+
294+
// Score [0,10]; higher is better
295+
export const computeAlertScore = (a: AlertWithRuleName): ScoreDetail => {
296+
return {
297+
alertName: a.ruleName,
298+
rawScore: 10 * (1 - computeExcessRatio(a)),
299+
weight: getSeverityWeight(a) * getStateWeight(a)
300+
};
275301
};
276302

277303
export const isSilenced = (silence: SilenceMatcher[], labels: PrometheusLabels): boolean => {

0 commit comments

Comments
 (0)