[Stack Monitoring] Enable OOTB alerts in RAC page and multiple rules of a rule type (#106457)

* allow rules to be managed in RAC page

* return all rules of a rule type instead of first one

* update UI to handle multiple rule types

* add comments about creating the menus by category for alerts and rules

* fix parsing of cluster alerts
This commit is contained in:
Sandra G 2021-08-04 09:16:52 -04:00 committed by GitHub
parent 41b6a99282
commit b03d85a20a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 351 additions and 269 deletions

View file

@ -566,7 +566,7 @@ export const ALERT_ACTION_TYPE_LOG = '.server-log';
/**
* To enable modifing of alerts in under actions
*/
export const ALERT_REQUIRES_APP_CONTEXT = true;
export const ALERT_REQUIRES_APP_CONTEXT = false;
export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo'];

View file

@ -15,6 +15,9 @@ import {
export type CommonAlert = Alert<AlertTypeParams> | SanitizedAlert<AlertTypeParams>;
export interface RulesByType {
[type: string]: CommonAlertStatus[];
}
export interface CommonAlertStatus {
states: CommonAlertState[];
rawAlert: Alert<AlertTypeParams> | SanitizedAlert<AlertTypeParams>;

View file

@ -14,7 +14,6 @@ import { AlertSeverity } from '../../common/enums';
import { formatDateTimeLocal } from '../../common/formatting';
import { isInSetupMode } from '../lib/setup_mode';
import { SetupModeContext } from '../components/setup_mode/setup_mode_context';
import { AlertsContext } from './context';
import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category';
import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node';
@ -40,16 +39,16 @@ const GROUP_BY_TYPE = i18n.translate('xpack.monitoring.alerts.badge.groupByType'
});
interface Props {
alerts: { [alertTypeId: string]: CommonAlertStatus };
alerts: { [alertTypeId: string]: CommonAlertStatus[] };
stateFilter: (state: AlertState) => boolean;
}
export const AlertsBadge: React.FC<Props> = (props: Props) => {
// We do not always have the alerts that each consumer wants due to licensing
const { stateFilter = () => true } = props;
const alerts = Object.values(props.alerts).filter((alertItem) => Boolean(alertItem?.rawAlert));
const alertsList = Object.values(props.alerts).flat();
const alerts = alertsList.filter((alertItem) => Boolean(alertItem?.rawAlert));
const [showPopover, setShowPopover] = React.useState<AlertSeverity | boolean | null>(null);
const inSetupMode = isInSetupMode(React.useContext(SetupModeContext));
const alertsContext = React.useContext(AlertsContext);
const alertCount = inSetupMode
? alerts.length
: alerts.reduce(
@ -74,8 +73,7 @@ export const AlertsBadge: React.FC<Props> = (props: Props) => {
const groupByType = GROUP_BY_NODE;
const panels = showByNode
? getAlertPanelsByNode(PANEL_TITLE, alerts, stateFilter)
: getAlertPanelsByCategory(PANEL_TITLE, inSetupMode, alerts, alertsContext, stateFilter);
: getAlertPanelsByCategory(PANEL_TITLE, inSetupMode, alerts, stateFilter);
if (panels.length && !inSetupMode && panels[0].items) {
panels[0].items.push(
...[

View file

@ -35,18 +35,11 @@ export const AlertsCallout: React.FC<Props> = (props: Props) => {
if (inSetupMode) {
return null;
}
const list = [];
for (const alertTypeId of Object.keys(alerts)) {
const alertInstance = alerts[alertTypeId];
for (const state of alertInstance.states) {
list.push({
alert: alertInstance,
state,
});
}
}
// get a list of each alert state for each rule
const list = Object.values(alerts)
.flat()
.map((alert) => alert.states.map((state) => ({ alert, state })))
.flat();
if (list.length === 0) {
return null;
}

View file

@ -106,7 +106,7 @@ Array [
"lastExecutionDate": 2020-12-08T00:00:00.000Z,
"status": "ok",
},
"id": "",
"id": "monitoring_alert_jvm_memory_usage_2",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_jvm_memory_usage_label",
@ -166,7 +166,7 @@ Array [
"lastExecutionDate": 2020-12-08T00:00:00.000Z,
"status": "ok",
},
"id": "",
"id": "monitoring_alert_jvm_memory_usage_2",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_jvm_memory_usage_label",
@ -231,7 +231,7 @@ Array [
"name": <EuiText>
es_name_1
(
2
3
)
</EuiText>,
"panel": 2,
@ -353,6 +353,27 @@ Array [
</React.Fragment>,
"panel": 7,
},
Object {
"name": <React.Fragment>
<EuiToolTip
content="triggered:0"
delay="regular"
position="top"
>
<EuiText
size="s"
>
triggered:0
</EuiText>
</EuiToolTip>
<EuiText
size="s"
>
monitoring_alert_nodes_changed_label_2
</EuiText>
</React.Fragment>,
"panel": 8,
},
],
"title": "es_name_1",
},
@ -372,7 +393,7 @@ Array [
"lastExecutionDate": 2020-12-08T00:00:00.000Z,
"status": "ok",
},
"id": "",
"id": "monitoring_alert_nodes_changed_2",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_nodes_changed_label",
@ -432,7 +453,7 @@ Array [
"lastExecutionDate": 2020-12-08T00:00:00.000Z,
"status": "ok",
},
"id": "",
"id": "monitoring_alert_disk_usage_1",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_disk_usage_label",
@ -492,7 +513,7 @@ Array [
"lastExecutionDate": 2020-12-08T00:00:00.000Z,
"status": "ok",
},
"id": "",
"id": "monitoring_alert_license_expiration_2",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_license_expiration_label",
@ -552,7 +573,7 @@ Array [
"lastExecutionDate": 2020-12-08T00:00:00.000Z,
"status": "ok",
},
"id": "",
"id": "monitoring_alert_nodes_changed_2",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_nodes_changed_label",
@ -612,7 +633,7 @@ Array [
"lastExecutionDate": 2020-12-08T00:00:00.000Z,
"status": "ok",
},
"id": "",
"id": "monitoring_alert_license_expiration_2",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_license_expiration_label",
@ -656,5 +677,65 @@ Array [
"title": "monitoring_alert_license_expiration_label",
"width": 400,
},
Object {
"content": <AlertPanel
alert={
Object {
"actions": Array [],
"alertTypeId": "monitoring_alert_nodes_changed",
"apiKey": null,
"apiKeyOwner": null,
"consumer": "",
"createdAt": 2020-12-08T00:00:00.000Z,
"createdBy": null,
"enabled": true,
"executionStatus": Object {
"lastExecutionDate": 2020-12-08T00:00:00.000Z,
"status": "ok",
},
"id": "monitoring_alert_nodes_changed_3",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_nodes_changed_label_2",
"notifyWhen": null,
"params": Object {},
"schedule": Object {
"interval": "1m",
},
"tags": Array [],
"throttle": null,
"updatedAt": 2020-12-08T00:00:00.000Z,
"updatedBy": null,
}
}
alertState={
Object {
"firing": true,
"meta": Object {},
"state": Object {
"cluster": Object {
"clusterName": "one",
"clusterUuid": "1",
},
"nodeId": "es1",
"nodeName": "es_name_1",
"ui": Object {
"isFiring": false,
"lastCheckedMS": 0,
"message": Object {
"text": "",
},
"resolvedMS": 0,
"severity": "danger",
"triggeredMS": 0,
},
},
}
}
/>,
"id": 8,
"title": "monitoring_alert_nodes_changed_label_2",
"width": 400,
},
]
`;

View file

@ -6,7 +6,6 @@
*/
import {
ALERTS,
ALERT_CPU_USAGE,
ALERT_LOGSTASH_VERSION_MISMATCH,
ALERT_THREAD_POOL_WRITE_REJECTIONS,
@ -19,7 +18,6 @@ import {
ALERT_DISK_USAGE,
ALERT_MEMORY_USAGE,
} from '../../../common/constants';
import { AlertsByName } from '../types';
import { AlertExecutionStatusValues } from '../../../../alerting/common';
import { AlertState } from '../../../common/types/alerts';
@ -62,20 +60,6 @@ const mockAlert = {
notifyWhen: null,
};
function getAllAlerts() {
return ALERTS.reduce((accum: AlertsByName, alertType) => {
accum[alertType] = {
states: [],
rawAlert: {
alertTypeId: alertType,
name: `${alertType}_label`,
...mockAlert,
},
};
return accum;
}, {});
}
describe('getAlertPanelsByCategory', () => {
const ui = {
isFiring: false,
@ -117,10 +101,6 @@ describe('getAlertPanelsByCategory', () => {
};
}
const alertsContext = {
allAlerts: getAllAlerts(),
};
const stateFilter = (state: AlertState) => true;
const panelTitle = 'Alerts';
@ -131,25 +111,13 @@ describe('getAlertPanelsByCategory', () => {
getAlert(ALERT_DISK_USAGE, 1),
getAlert(ALERT_LICENSE_EXPIRATION, 2),
];
const result = getAlertPanelsByCategory(
panelTitle,
false,
alerts,
alertsContext,
stateFilter
);
const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
it('should properly group for alerts in a single category', () => {
const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)];
const result = getAlertPanelsByCategory(
panelTitle,
false,
alerts,
alertsContext,
stateFilter
);
const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
@ -159,26 +127,14 @@ describe('getAlertPanelsByCategory', () => {
getAlert(ALERT_CPU_USAGE, 0),
getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0),
];
const result = getAlertPanelsByCategory(
panelTitle,
false,
alerts,
alertsContext,
stateFilter
);
const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
it('should allow for state filtering', () => {
const alerts = [getAlert(ALERT_CPU_USAGE, 2)];
const customStateFilter = (state: AlertState) => state.nodeName === 'es_name_0';
const result = getAlertPanelsByCategory(
panelTitle,
false,
alerts,
alertsContext,
customStateFilter
);
const result = getAlertPanelsByCategory(panelTitle, false, alerts, customStateFilter);
expect(result).toMatchSnapshot();
});
});
@ -190,13 +146,13 @@ describe('getAlertPanelsByCategory', () => {
getAlert(ALERT_DISK_USAGE, 1),
getAlert(ALERT_LICENSE_EXPIRATION, 2),
];
const result = getAlertPanelsByCategory(panelTitle, true, alerts, alertsContext, stateFilter);
const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
it('should properly group for alerts in a single category', () => {
const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)];
const result = getAlertPanelsByCategory(panelTitle, true, alerts, alertsContext, stateFilter);
const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
@ -206,7 +162,7 @@ describe('getAlertPanelsByCategory', () => {
getAlert(ALERT_CPU_USAGE, 0),
getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0),
];
const result = getAlertPanelsByCategory(panelTitle, true, alerts, alertsContext, stateFilter);
const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
});

View file

@ -10,80 +10,104 @@ import { EuiText, EuiToolTip } from '@elastic/eui';
import { AlertPanel } from '../panel';
import { ALERT_PANEL_MENU } from '../../../common/constants';
import { getDateFromNow, getCalendar } from '../../../common/formatting';
import { IAlertsContext } from '../context';
import { AlertState, CommonAlertStatus } from '../../../common/types/alerts';
import {
AlertState,
CommonAlert,
CommonAlertState,
CommonAlertStatus,
} from '../../../common/types/alerts';
import { PanelItem } from '../types';
import { sortByNewestAlert } from './sort_by_newest_alert';
import { Legacy } from '../../legacy_shims';
interface MenuAlert {
alert: CommonAlert;
alertName: string;
states: CommonAlertState[];
}
interface MenuItem {
alertCount: number;
label: string;
alerts: MenuAlert[];
}
export function getAlertPanelsByCategory(
panelTitle: string,
inSetupMode: boolean,
alerts: CommonAlertStatus[],
alertsContext: IAlertsContext,
stateFilter: (state: AlertState) => boolean
) {
const menu = [];
for (const category of ALERT_PANEL_MENU) {
let categoryFiringAlertCount = 0;
if (inSetupMode) {
const alertsInCategory = [];
for (const categoryAlert of category.alerts) {
if (
Boolean(alerts.find(({ rawAlert }) => rawAlert.alertTypeId === categoryAlert.alertName))
) {
alertsInCategory.push(categoryAlert);
}
}
if (alertsInCategory.length > 0) {
menu.push({
...category,
alerts: alertsInCategory.map(({ alertName }) => {
const alertStatus = alertsContext.allAlerts[alertName];
return {
alert: alertStatus.rawAlert,
states: [],
alertName,
};
}),
alertCount: 0,
});
}
} else {
const firingAlertsInCategory = [];
for (const { alertName } of category.alerts) {
const foundAlert = alerts.find(
({ rawAlert: { alertTypeId } }) => alertName === alertTypeId
// return items organized by categories in ALERT_PANEL_MENU
// only show rules in setup mode
const menu = inSetupMode
? ALERT_PANEL_MENU.reduce<MenuItem[]>((acc, category) => {
// check if we have any rules with that match this category
const alertsInCategory = category.alerts.filter((alert) =>
alerts.find(({ rawAlert }) => rawAlert.alertTypeId === alert.alertName)
);
if (foundAlert && foundAlert.states.length > 0) {
const states = foundAlert.states.filter(({ state }) => stateFilter(state));
if (states.length > 0) {
firingAlertsInCategory.push({
alert: foundAlert.rawAlert,
states: foundAlert.states,
alertName,
// return all the categories that have rules and the rules
if (alertsInCategory.length > 0) {
// add the category item to the menu
acc.push({
...category,
// add the corresponding rules that belong to this category
alerts: alertsInCategory
.map(({ alertName }) => {
return alerts
.filter(({ rawAlert }) => rawAlert.alertTypeId === alertName)
.map((alert) => {
return {
alert: alert.rawAlert,
states: [],
alertName,
};
});
})
.flat(),
alertCount: 0,
});
}
return acc;
}, [])
: ALERT_PANEL_MENU.reduce<MenuItem[]>((acc, category) => {
// return items organized by categories in ALERT_PANEL_MENU, then rule name, then the actual alerts
const firingAlertsInCategory: MenuAlert[] = [];
let categoryFiringAlertCount = 0;
for (const { alertName } of category.alerts) {
const foundAlerts = alerts.filter(
({ rawAlert, states }) => alertName === rawAlert.alertTypeId && states.length > 0
);
if (foundAlerts.length > 0) {
foundAlerts.forEach((foundAlert) => {
// add corresponding alerts to each rule
const states = foundAlert.states.filter(({ state }) => stateFilter(state));
if (states.length > 0) {
firingAlertsInCategory.push({
alert: foundAlert.rawAlert,
states,
alertName,
});
categoryFiringAlertCount += states.length;
}
});
categoryFiringAlertCount += states.length;
}
}
}
if (firingAlertsInCategory.length > 0) {
menu.push({
...category,
alertCount: categoryFiringAlertCount,
alerts: firingAlertsInCategory,
});
}
}
}
if (firingAlertsInCategory.length > 0) {
acc.push({
...category,
alertCount: categoryFiringAlertCount,
alerts: firingAlertsInCategory,
});
}
return acc;
}, []);
for (const item of menu) {
for (const alert of item.alerts) {
alert.states.sort(sortByNewestAlert);
}
}
// if in setup mode add the count of alerts to the category name
const panels: PanelItem[] = [
{
id: 0,
@ -107,8 +131,8 @@ export function getAlertPanelsByCategory(
],
},
];
if (inSetupMode) {
// create the nested UI menu: category name -> rule name -> edit rule
let secondaryPanelIndex = menu.length;
let tertiaryPanelIndex = menu.length;
let nodeIndex = 0;
@ -116,43 +140,42 @@ export function getAlertPanelsByCategory(
panels.push({
id: nodeIndex + 1,
title: `${category.label}`,
items: category.alerts.map(({ alertName }) => {
const alertStatus = alertsContext.allAlerts[alertName];
return {
name: <EuiText>{alertStatus.rawAlert.name}</EuiText>,
panel: ++secondaryPanelIndex,
};
}),
items: category.alerts
.map((alert) => {
return {
name: <EuiText>{alert.alert.name}</EuiText>,
panel: ++secondaryPanelIndex,
};
})
.flat(),
});
nodeIndex++;
}
for (const category of menu) {
for (const { alert, alertName } of category.alerts) {
const alertStatus = alertsContext.allAlerts[alertName];
for (const { alert } of category.alerts) {
panels.push({
id: ++tertiaryPanelIndex,
title: `${alert.name}`,
width: 400,
content: <AlertPanel alert={alertStatus.rawAlert} />,
content: <AlertPanel alert={alert} />,
});
}
}
} else {
// create the nested UI menu: category name (n) -> rule name (n) -> list of firing alerts
let primaryPanelIndex = menu.length;
let nodeIndex = 0;
for (const category of menu) {
panels.push({
id: nodeIndex + 1,
title: `${category.label}`,
items: category.alerts.map(({ alertName, states }) => {
items: category.alerts.map(({ alert, alertName, states }) => {
const filteredStates = states.filter(({ state }) => stateFilter(state));
const alertStatus = alertsContext.allAlerts[alertName];
const name = inSetupMode ? (
<EuiText>{alertStatus.rawAlert.name}</EuiText>
<EuiText>{alert.name}</EuiText>
) : (
<EuiText>
{alertStatus.rawAlert.name} ({filteredStates.length})
{alert.name} ({filteredStates.length})
</EuiText>
);
return {
@ -208,7 +231,6 @@ export function getAlertPanelsByCategory(
});
}
}
let tertiaryPanelIndex2 = menu.reduce((count, category) => {
count += category.alerts.length;
return count;
@ -226,6 +248,5 @@ export function getAlertPanelsByCategory(
}
}
}
return panels;
}

View file

@ -37,7 +37,6 @@ jest.mock('../../../common/formatting', () => ({
}));
const mockAlert = {
id: '',
enabled: true,
tags: [],
consumer: '',
@ -90,6 +89,7 @@ describe('getAlertPanelsByNode', () => {
return {
rawAlert: {
id: `${type}_${firingCount}`,
alertTypeId: type,
name: `${type}_label`,
...mockAlert,
@ -106,6 +106,17 @@ describe('getAlertPanelsByNode', () => {
getAlert(ALERT_NODES_CHANGED, 2),
getAlert(ALERT_DISK_USAGE, 1),
getAlert(ALERT_LICENSE_EXPIRATION, 2),
{
states: [
{ firing: true, meta: {}, state: { cluster, ui, nodeId: 'es1', nodeName: 'es_name_1' } },
],
rawAlert: {
id: `${ALERT_NODES_CHANGED}_3`,
alertTypeId: ALERT_NODES_CHANGED,
name: `${ALERT_NODES_CHANGED}_label_2`,
...mockAlert,
},
},
];
const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter);
expect(result).toMatchSnapshot();

View file

@ -38,18 +38,20 @@ export function getAlertPanelsByNode(
} = {};
for (const { states, rawAlert } of alerts) {
const { alertTypeId } = rawAlert;
const { id: alertId } = rawAlert;
for (const alertState of states.filter(({ state: _state }) => stateFilter(_state))) {
const { state } = alertState;
statesByNodes[state.nodeId] = statesByNodes[state.nodeId] || [];
statesByNodes[state.nodeId].push(alertState);
alertsByNodes[state.nodeId] = alertsByNodes[state.nodeId] || {};
alertsByNodes[state.nodeId][alertTypeId] = alertsByNodes[alertState.state.nodeId][
alertTypeId
] || { alert: rawAlert, states: [], count: 0 };
alertsByNodes[state.nodeId][alertTypeId].count++;
alertsByNodes[state.nodeId][alertTypeId].states.push(alertState);
alertsByNodes[state.nodeId][alertId] = alertsByNodes[alertState.state.nodeId][alertId] || {
alert: rawAlert,
states: [],
count: 0,
};
alertsByNodes[state.nodeId][alertId].count++;
alertsByNodes[state.nodeId][alertId].states.push(alertState);
}
}
@ -137,6 +139,5 @@ export function getAlertPanelsByNode(
return accum;
}, []),
];
return panels;
}

View file

@ -10,7 +10,7 @@ import { CommonAlertStatus } from '../../../common/types/alerts';
import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context';
export function shouldShowAlertBadge(
alerts: { [alertTypeId: string]: CommonAlertStatus },
alerts: { [alertTypeId: string]: CommonAlertStatus[] },
alertTypeIds: string[],
context?: ISetupModeContext
) {
@ -18,5 +18,8 @@ export function shouldShowAlertBadge(
return false;
}
const inSetupMode = isInSetupMode(context);
return inSetupMode || alertTypeIds.find((name) => alerts[name] && alerts[name].states.length);
const alertExists = alertTypeIds.find(
(name) => alerts[name] && alerts[name].find((rule) => rule.states.length > 0)
);
return inSetupMode || alertExists;
}

View file

@ -16,7 +16,7 @@ import { isInSetupMode } from '../lib/setup_mode';
import { SetupModeContext } from '../components/setup_mode/setup_mode_context';
interface Props {
alerts: { [alertTypeId: string]: CommonAlertStatus };
alerts: { [alertTypeId: string]: CommonAlertStatus[] };
showBadge: boolean;
showOnlyCount?: boolean;
stateFilter?: (state: AlertState) => boolean;
@ -30,25 +30,27 @@ export const AlertsStatus: React.FC<Props> = (props: Props) => {
}
let atLeastOneDanger = false;
const count = Object.values(alerts).reduce((cnt, alertStatus) => {
const firingStates = alertStatus.states.filter((state) => state.firing);
const firingAndFilterStates = firingStates.filter((state) => stateFilter(state.state));
cnt += firingAndFilterStates.length;
if (firingStates.length) {
if (!atLeastOneDanger) {
for (const state of alertStatus.states) {
if (
stateFilter(state.state) &&
(state.state as AlertState).ui.severity === AlertSeverity.Danger
) {
atLeastOneDanger = true;
break;
const count = Object.values(alerts)
.flat()
.reduce((cnt, alertStatus) => {
const firingStates = alertStatus.states.filter((state) => state.firing);
const firingAndFilterStates = firingStates.filter((state) => stateFilter(state.state));
cnt += firingAndFilterStates.length;
if (firingStates.length) {
if (!atLeastOneDanger) {
for (const state of alertStatus.states) {
if (
stateFilter(state.state) &&
(state.state as AlertState).ui.severity === AlertSeverity.Danger
) {
atLeastOneDanger = true;
break;
}
}
}
}
}
return cnt;
}, 0);
return cnt;
}, 0);
if (count === 0 && (!inSetupMode || showOnlyCount)) {
return (
@ -75,7 +77,6 @@ export const AlertsStatus: React.FC<Props> = (props: Props) => {
if (showBadge || inSetupMode) {
return <AlertsBadge alerts={alerts} stateFilter={stateFilter} />;
}
const severity = atLeastOneDanger ? AlertSeverity.Danger : AlertSeverity.Warning;
const tooltipText = (() => {

View file

@ -6,7 +6,6 @@
*/
import React from 'react';
import { get } from 'lodash';
import {
EuiPage,
EuiPageContent,
@ -34,6 +33,8 @@ export const Node = ({
scope,
...props
}) => {
/*
// This isn't doing anything due to a possible bug. https://github.com/elastic/kibana/issues/106309
if (alerts) {
for (const alertTypeId of Object.keys(alerts)) {
const alertInstance = alerts[alertTypeId];
@ -48,7 +49,7 @@ export const Node = ({
}
}
}
*/
const metricsToShow = [
metrics.node_jvm_mem,
metrics.node_mem,

View file

@ -41,7 +41,7 @@ import { AlertsStatus } from '../../../alerts/status';
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
import { SetupModeFeature } from '../../../../common/enums';
const getColumns = (setupMode: any, alerts: { [alertTypeId: string]: CommonAlertStatus }) => {
const getColumns = (setupMode: any, alerts: { [alertTypeId: string]: CommonAlertStatus[] }) => {
const columns = [
{
name: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', {
@ -173,7 +173,7 @@ const getColumns = (setupMode: any, alerts: { [alertTypeId: string]: CommonAlert
interface Props {
clusterStatus: any;
alerts: { [alertTypeId: string]: CommonAlertStatus };
alerts: { [alertTypeId: string]: CommonAlertStatus[] };
setupMode: any;
sorting: any;
pagination: any;

View file

@ -31,14 +31,19 @@ describe('AlertsFactory', () => {
total: 1,
data: [
{
id: ALERT_CPU_USAGE,
id: 1,
},
{
id: 2,
},
],
};
});
const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, rulesClient as any);
expect(alert).not.toBeNull();
expect(alert?.getId()).toBe(ALERT_CPU_USAGE);
const alerts = await AlertsFactory.getByType(ALERT_CPU_USAGE, rulesClient as any);
expect(alerts).not.toBeNull();
expect(alerts.length).toBe(2);
expect(alerts[0].getId()).toBe(1);
expect(alerts[1].getId()).toBe(2);
});
it('should pass in the correct filters', async () => {

View file

@ -40,6 +40,7 @@ import {
} from '../../common/constants';
import { RulesClient } from '../../../alerting/server';
import { Alert } from '../../../alerting/common';
import { CommonAlertParams } from '../../common/types/alerts';
const BY_TYPE = {
[ALERT_CLUSTER_HEALTH]: ClusterHealthAlert,
@ -61,24 +62,22 @@ const BY_TYPE = {
export class AlertsFactory {
public static async getByType(
type: string,
rulesClient: RulesClient | undefined
): Promise<BaseAlert | undefined> {
alertsClient: RulesClient | undefined
): Promise<BaseAlert[]> {
const alertCls = BY_TYPE[type];
if (!alertCls || !rulesClient) {
return;
if (!alertCls || !alertsClient) {
return [];
}
const alertClientAlerts = await rulesClient.find({
const alertClientAlerts = await alertsClient.find<CommonAlertParams>({
options: {
filter: `alert.attributes.alertTypeId:${type}`,
},
});
if (!alertClientAlerts.total || !alertClientAlerts.data?.length) {
return;
return [];
}
const [rawAlert] = alertClientAlerts.data as [Alert];
return new alertCls(rawAlert) as BaseAlert;
return alertClientAlerts.data.map((alert) => new alertCls(alert as Alert) as BaseAlert);
}
public static getAll() {

View file

@ -33,7 +33,6 @@ jest.mock('../../static_globals', () => ({
describe('fetchStatus', () => {
const alertType = ALERT_CPU_USAGE;
const alertTypes = [alertType];
const id = 1;
const defaultClusterState = {
clusterUuid: 'abc',
clusterName: 'test',
@ -51,7 +50,10 @@ describe('fetchStatus', () => {
total: 1,
data: [
{
id,
id: 1,
},
{
id: 2,
},
],
})),
@ -77,10 +79,16 @@ describe('fetchStatus', () => {
defaultClusterState.clusterUuid,
]);
expect(status).toEqual({
monitoring_alert_cpu_usage: {
rawAlert: { id: 1 },
states: [],
},
monitoring_alert_cpu_usage: [
{
rawAlert: { id: 1 },
states: [],
},
{
rawAlert: { id: 2 },
states: [],
},
],
});
});
@ -100,7 +108,7 @@ describe('fetchStatus', () => {
]);
expect(Object.values(status).length).toBe(1);
expect(Object.keys(status)).toEqual(alertTypes);
expect(status[alertType].states[0].state.ui.isFiring).toBe(true);
expect(status[alertType][0].states[0].state.ui.isFiring).toBe(true);
});
it('should pass in the right filter to the alerts client', async () => {
@ -118,7 +126,7 @@ describe('fetchStatus', () => {
const status = await fetchStatus(rulesClient as any, alertTypes, [
defaultClusterState.clusterUuid,
]);
expect(status[alertType].states.length).toEqual(0);
expect(status[alertType][0].states.length).toEqual(0);
});
it('should return nothing if no alerts are found', async () => {
@ -155,7 +163,7 @@ describe('fetchStatus', () => {
total: 1,
data: [
{
id,
id: 1,
},
],
})),

View file

@ -8,11 +8,7 @@
import { AlertInstanceState } from '../../../common/types/alerts';
import { RulesClient } from '../../../../alerting/server';
import { AlertsFactory } from '../../alerts';
import {
CommonAlertStatus,
CommonAlertState,
CommonAlertFilter,
} from '../../../common/types/alerts';
import { CommonAlertState, CommonAlertFilter, RulesByType } from '../../../common/types/alerts';
import { ALERTS } from '../../../common/constants';
export async function fetchStatus(
@ -20,62 +16,64 @@ export async function fetchStatus(
alertTypes: string[] | undefined,
clusterUuids: string[],
filters: CommonAlertFilter[] = []
): Promise<{ [type: string]: CommonAlertStatus }> {
const types: Array<{ type: string; result: CommonAlertStatus }> = [];
const byType: { [type: string]: CommonAlertStatus } = {};
await Promise.all(
(alertTypes || ALERTS).map(async (type) => {
const alert = await AlertsFactory.getByType(type, rulesClient);
if (!alert || !alert.rawAlert) {
return;
): Promise<RulesByType> {
const rulesByType = await Promise.all(
(alertTypes || ALERTS).map(async (type) => AlertsFactory.getByType(type, rulesClient))
);
if (!rulesByType.length) return {};
const rulesFlattened = rulesByType.flat();
const rulesWithStates = await Promise.all(
rulesFlattened.map(async (rule) => {
// we should have a different class to distinguish between "alerts" where the rule exists
// and a BaseAlert created without an existing rule for better typing so we don't need to check here
if (!rule.rawAlert) {
throw new Error('alert missing rawAlert');
}
const result: CommonAlertStatus = {
states: [],
rawAlert: alert.rawAlert,
};
types.push({ type, result });
const id = alert.getId();
const id = rule.getId();
if (!id) {
return result;
throw new Error('alert missing id');
}
// Now that we have the id, we can get the state
const states = await alert.getStates(rulesClient, id, filters);
if (!states) {
return result;
}
const states = await rule.getStates(rulesClient, id, filters);
// puts all alert states associated with this rule into a flat array. this works with both the legacy alert
// of having multiple alert states per alert, each representing a firing node, and the current alert where each firing
// node is an alert with a single alert state, the node itself. https://github.com/elastic/kibana/pull/102544
result.states = Object.values(states).reduce((accum: CommonAlertState[], instance: any) => {
const alertInstanceState = instance.state as AlertInstanceState;
if (!alertInstanceState.alertStates) {
return accum;
}
for (const state of alertInstanceState.alertStates) {
const meta = instance.meta;
if (clusterUuids && !clusterUuids.includes(state.cluster.clusterUuid)) {
const alertStates = Object.values(states).reduce(
(accum: CommonAlertState[], instance: any) => {
const alertInstanceState = instance.state as AlertInstanceState;
if (!alertInstanceState.alertStates) {
return accum;
}
for (const state of alertInstanceState.alertStates) {
const meta = instance.meta;
if (clusterUuids && !clusterUuids.includes(state.cluster.clusterUuid)) {
return accum;
}
let firing = false;
if (state.ui.isFiring) {
firing = true;
let firing = false;
if (state.ui.isFiring) {
firing = true;
}
accum.push({ firing, state, meta });
}
accum.push({ firing, state, meta });
}
return accum;
}, []);
return accum;
},
[]
);
const type = rule.alertOptions.id;
const result = {
states: alertStates,
rawAlert: rule.rawAlert,
};
return { type, result };
})
);
types.sort((a, b) => (a.type === b.type ? 0 : a.type.length > b.type.length ? 1 : -1));
for (const { type, result } of types) {
byType[type] = result;
}
return byType;
rulesWithStates.sort((a, b) => (a.type === b.type ? 0 : a.type.length > b.type.length ? 1 : -1));
return rulesWithStates.reduce<RulesByType>((acc, { type, result }) => {
acc[type] = acc[type] ? [...acc[type], result] : [result];
return acc;
}, {});
}

View file

@ -35,6 +35,7 @@ import { getStandaloneClusterDefinition, hasStandaloneClusters } from '../standa
import { getLogTypes } from '../logs';
import { isInCodePath } from './is_in_code_path';
import { LegacyRequest, Cluster } from '../../types';
import { RulesByType } from '../../../common/types/alerts';
/**
* Get all clusters or the cluster associated with {@code clusterUuid} when it is defined.
@ -142,21 +143,16 @@ export async function getClustersFromRequest(
} else {
try {
cluster.alerts = {
list: Object.keys(alertStatus).reduce((accum, alertName) => {
const value = alertStatus[alertName];
if (value.states && value.states.length) {
Reflect.set(accum, alertName, {
...value,
states: value.states.filter(
(state) =>
state.state.cluster.clusterUuid ===
get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid)
),
});
} else {
Reflect.set(accum, alertName, value);
}
return accum;
list: Object.keys(alertStatus).reduce<RulesByType>((acc, ruleTypeName) => {
acc[ruleTypeName] = alertStatus[ruleTypeName].map((rule) => ({
...rule,
states: rule.states.filter(
(state) =>
state.state.cluster.clusterUuid ===
get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid)
),
}));
return acc;
}, {}),
alertsMeta: {
enabled: true,
@ -177,7 +173,6 @@ export async function getClustersFromRequest(
}
}
}
// add kibana data
const kibanas =
isInCodePath(codePaths, [CODE_PATH_KIBANA]) && !isStandaloneCluster

View file

@ -31,6 +31,7 @@ import { PluginSetupContract as FeaturesPluginSetupContract } from '../../featur
import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server';
import { CloudSetup } from '../../cloud/server';
import { ElasticsearchModifiedSource } from '../common/types/es';
import { RulesByType } from '../common/types/alerts';
export interface MonitoringLicenseService {
refresh: () => Promise<any>;
@ -151,9 +152,16 @@ export interface LegacyServer {
export type Cluster = ElasticsearchModifiedSource & {
ml?: { jobs: any };
logs?: any;
alerts?: any;
alerts?: AlertsOnCluster;
};
export interface AlertsOnCluster {
list: RulesByType;
alertsMeta: {
enabled: boolean;
};
}
export interface Bucket {
key: string;
uuids: {