[Monitoring] Some progress on making alerts better in the UI (#81569)

* Some progress on making alerts better in the UI

* Handle edge case

* Updates

* More updates

* Show kibana instances alerts better

* Stop showing missing nodes and improve the detail alert UI

* WIP

* Fix the badge display

* Okay I think this is finally working

* Fix type issues

* Fix tests

* Fix tests

* Fix alert counts

* Fix setup mode listing

* Better detail page view of alerts

* Feedback

* Sorting

* Fix a couple small issues

* Start of unit tests

* I don't think we need this Mock type

* Fix types

* More tests

* Improve tests and fix sorting

* Make this test more resilient

* Updates after merging master

* Fix tests

* Fix types, and improve tests

* PR comments

* Remove nextStep logic

* PR feedback

* PR feedback

* Removing unnecessary changes

* Fixing bad merge issues

* Remove unused imports

* Add tooltip to alerts grouped by node

* Fix up stateFilter usage

* Code clean up

* PR feedback

* Fix state filtering in the category list

* Fix types

* Fix test

* Fix types

* Update snapshots

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Chris Roberson 2020-12-13 10:20:29 -05:00 committed by GitHub
parent 95beef7637
commit 1e8f2f66eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 3289 additions and 418 deletions

View file

@ -453,6 +453,42 @@ export const ALERT_DETAILS = {
},
};
export const ALERT_PANEL_MENU = [
{
label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.clusterHealth', {
defaultMessage: 'Cluster health',
}),
alerts: [
{ alertName: ALERT_NODES_CHANGED },
{ alertName: ALERT_CLUSTER_HEALTH },
{ alertName: ALERT_ELASTICSEARCH_VERSION_MISMATCH },
{ alertName: ALERT_KIBANA_VERSION_MISMATCH },
{ alertName: ALERT_LOGSTASH_VERSION_MISMATCH },
],
},
{
label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.resourceUtilization', {
defaultMessage: 'Resource utilization',
}),
alerts: [
{ alertName: ALERT_CPU_USAGE },
{ alertName: ALERT_DISK_USAGE },
{ alertName: ALERT_MEMORY_USAGE },
],
},
{
label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.errors', {
defaultMessage: 'Errors and exceptions',
}),
alerts: [
{ alertName: ALERT_MISSING_MONITORING_DATA },
{ alertName: ALERT_LICENSE_EXPIRATION },
{ alertName: ALERT_THREAD_POOL_SEARCH_REJECTIONS },
{ alertName: ALERT_THREAD_POOL_WRITE_REJECTIONS },
],
},
];
/**
* A listing of all alert types
*/

View file

@ -11,13 +11,14 @@ export const SMALL_FLOAT = '0.[00]';
export const LARGE_BYTES = '0,0.0 b';
export const SMALL_BYTES = '0.0 b';
export const LARGE_ABBREVIATED = '0,0.[0]a';
export const ROUNDED_FLOAT = '00.[00]';
/**
* Format the {@code date} in the user's expected date/time format using their <em>guessed</em> local time zone.
* @param date Either a numeric Unix timestamp or a {@code Date} object
* @returns The date formatted using 'LL LTS'
*/
export function formatDateTimeLocal(date, useUTC = false, timezone = null) {
export function formatDateTimeLocal(date: number | Date, useUTC = false, timezone = null) {
return useUTC
? moment.utc(date).format('LL LTS')
: moment.tz(date, timezone || moment.tz.guess()).format('LL LTS');
@ -28,6 +29,18 @@ export function formatDateTimeLocal(date, useUTC = false, timezone = null) {
* @param {string} hash The complete hash
* @return {string} The shortened hash
*/
export function shortenPipelineHash(hash) {
export function shortenPipelineHash(hash: string) {
return hash.substr(0, 6);
}
export function getDateFromNow(timestamp: string | number | Date, tz: string) {
return moment(timestamp)
.tz(tz === 'Browser' ? moment.tz.guess() : tz)
.fromNow();
}
export function getCalendar(timestamp: string | number | Date, tz: string) {
return moment(timestamp)
.tz(tz === 'Browser' ? moment.tz.guess() : tz)
.calendar();
}

View file

@ -7,6 +7,8 @@
import { Alert, SanitizedAlert } from '../../../alerts/common';
import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums';
export type CommonAlert = Alert | SanitizedAlert;
export interface CommonAlertStatus {
states: CommonAlertState[];
rawAlert: Alert | SanitizedAlert;
@ -179,6 +181,7 @@ export interface LegacyAlert {
message: string;
resolved_timestamp: string;
metadata: LegacyAlertMetadata;
nodeName: string;
nodes?: LegacyAlertNodesChangedList;
}

View file

@ -4,187 +4,115 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiContextMenu,
EuiPopover,
EuiBadge,
EuiFlexGrid,
EuiFlexItem,
EuiText,
} from '@elastic/eui';
import { CommonAlertStatus, CommonAlertState } from '../../common/types/alerts';
import { EuiContextMenu, EuiPopover, EuiBadge, EuiSwitch } from '@elastic/eui';
import { AlertState, CommonAlertStatus } from '../../common/types/alerts';
import { AlertSeverity } from '../../common/enums';
// @ts-ignore
import { formatDateTimeLocal } from '../../common/formatting';
import { AlertState } from '../../common/types/alerts';
import { AlertPanel } from './panel';
import { Legacy } from '../legacy_shims';
import { isInSetupMode } from '../lib/setup_mode';
import { SetupModeContext } from '../components/setup_mode/setup_mode_context';
function getDateFromState(state: CommonAlertState) {
const timestamp = state.state.ui.triggeredMS;
const tz = Legacy.shims.uiSettings.get('dateFormat:tz');
return formatDateTimeLocal(timestamp, false, tz === 'Browser' ? null : tz);
}
import { AlertsContext } from './context';
import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category';
import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node';
export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`;
interface AlertInPanel {
alert: CommonAlertStatus;
alertState: CommonAlertState;
}
const MAX_TO_SHOW_BY_CATEGORY = 8;
const PANEL_TITLE = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
defaultMessage: 'Alerts',
});
const GROUP_BY_NODE = i18n.translate('xpack.monitoring.alerts.badge.groupByNode', {
defaultMessage: 'Group by node',
});
const GROUP_BY_TYPE = i18n.translate('xpack.monitoring.alerts.badge.groupByType', {
defaultMessage: 'Group by alert type',
});
interface Props {
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 [showPopover, setShowPopover] = React.useState<AlertSeverity | boolean | null>(null);
const inSetupMode = isInSetupMode(React.useContext(SetupModeContext));
const alerts = Object.values(props.alerts).filter((alertItem) => Boolean(alertItem?.rawAlert));
const alertsContext = React.useContext(AlertsContext);
const alertCount = inSetupMode
? alerts.length
: alerts.reduce(
(sum, { states }) => sum + states.filter(({ state }) => stateFilter(state)).length,
0
);
const [showByNode, setShowByNode] = React.useState(
!inSetupMode && alertCount > MAX_TO_SHOW_BY_CATEGORY
);
if (alerts.length === 0) {
React.useEffect(() => {
if (inSetupMode && showByNode) {
setShowByNode(false);
}
}, [inSetupMode, showByNode]);
if (alertCount === 0) {
return null;
}
const badges = [];
const groupByType = GROUP_BY_NODE;
const panels = showByNode
? getAlertPanelsByNode(PANEL_TITLE, alerts, stateFilter)
: getAlertPanelsByCategory(PANEL_TITLE, inSetupMode, alerts, alertsContext, stateFilter);
if (inSetupMode) {
const button = (
<EuiBadge
iconType="bell"
onClickAriaLabel={numberOfAlertsLabel(alerts.length)}
onClick={() => setShowPopover(true)}
>
{numberOfAlertsLabel(alerts.length)}
</EuiBadge>
);
const panels = [
{
id: 0,
title: i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
defaultMessage: 'Alerts',
}),
items: alerts.map(({ rawAlert }, index) => {
return {
name: <EuiText>{rawAlert.name}</EuiText>,
panel: index + 1,
};
}),
},
...alerts.map((alertStatus, index) => {
return {
id: index + 1,
title: alertStatus.rawAlert.name,
width: 400,
content: <AlertPanel alert={alertStatus} />,
};
}),
];
badges.push(
<EuiPopover
id="monitoringAlertMenu"
button={button}
isOpen={showPopover === true}
closePopover={() => setShowPopover(null)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
} else {
const byType = {
[AlertSeverity.Danger]: [] as AlertInPanel[],
[AlertSeverity.Warning]: [] as AlertInPanel[],
[AlertSeverity.Success]: [] as AlertInPanel[],
};
for (const alert of alerts) {
for (const alertState of alert.states) {
if (alertState.firing && stateFilter(alertState.state)) {
const state = alertState.state as AlertState;
byType[state.ui.severity].push({
alertState,
alert,
});
}
}
}
const typesToShow = [AlertSeverity.Danger, AlertSeverity.Warning];
for (const type of typesToShow) {
const list = byType[type];
if (list.length === 0) {
continue;
}
const button = (
<EuiBadge
iconType="bell"
color={type}
onClickAriaLabel={numberOfAlertsLabel(list.length)}
onClick={() => setShowPopover(type)}
>
{numberOfAlertsLabel(list.length)}
</EuiBadge>
);
const panels = [
if (panels.length && !inSetupMode && panels[0].items) {
panels[0].items.push(
...[
{
id: 0,
title: `Alerts`,
items: list.map(({ alert, alertState }, index) => {
return {
name: (
<Fragment>
<EuiText size="s">
<h4>{getDateFromState(alertState)}</h4>
</EuiText>
<EuiText>{alert.rawAlert.name}</EuiText>
</Fragment>
),
panel: index + 1,
};
}),
isSeparator: true as const,
},
...list.map((alertStatus, index) => {
return {
id: index + 1,
title: getDateFromState(alertStatus.alertState),
width: 400,
content: <AlertPanel alert={alertStatus.alert} alertState={alertStatus.alertState} />,
};
}),
];
badges.push(
<EuiPopover
id="monitoringAlertMenu"
button={button}
isOpen={showPopover === type}
closePopover={() => setShowPopover(null)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
}
{
name: (
<EuiSwitch
checked={false}
onChange={() => setShowByNode(!showByNode)}
label={showByNode ? GROUP_BY_TYPE : groupByType}
/>
),
},
]
);
}
const button = (
<EuiBadge
iconType="bell"
color={inSetupMode ? 'default' : 'danger'}
onClickAriaLabel={numberOfAlertsLabel(alertCount)}
onClick={() => setShowPopover(true)}
>
{numberOfAlertsLabel(alertCount)}
</EuiBadge>
);
return (
<EuiFlexGrid data-test-subj="monitoringSetupModeAlertBadges">
{badges.map((badge, index) => (
<EuiFlexItem key={index} grow={false}>
{badge}
</EuiFlexItem>
))}
</EuiFlexGrid>
<EuiPopover
id="monitoringAlertMenu"
button={button}
isOpen={showPopover === true}
closePopover={() => setShowPopover(null)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu
key={`${showByNode ? 'byNode' : 'byType'}_${panels.length}`}
initialPanelId={0}
panels={panels}
/>
</EuiPopover>
);
};

View file

@ -5,78 +5,108 @@
*/
import React, { Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { CommonAlertStatus } from '../../common/types/alerts';
import { AlertSeverity } from '../../common/enums';
import {
EuiPanel,
EuiSpacer,
EuiAccordion,
EuiListGroup,
EuiListGroupItem,
EuiTextColor,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
} from '@elastic/eui';
import { replaceTokens } from './lib/replace_tokens';
import { AlertMessage, AlertState } from '../../common/types/alerts';
const TYPES = [
{
severity: AlertSeverity.Warning,
color: 'warning',
label: i18n.translate('xpack.monitoring.alerts.callout.warningLabel', {
defaultMessage: 'Warning alert(s)',
}),
},
{
severity: AlertSeverity.Danger,
color: 'danger',
label: i18n.translate('xpack.monitoring.alerts.callout.dangerLabel', {
defaultMessage: 'Danger alert(s)',
}),
},
];
import { AlertMessage } from '../../common/types/alerts';
import { AlertsByName } from './types';
import { isInSetupMode } from '../lib/setup_mode';
import { SetupModeContext } from '../components/setup_mode/setup_mode_context';
import { AlertConfiguration } from './configuration';
interface Props {
alerts: { [alertTypeId: string]: CommonAlertStatus };
stateFilter: (state: AlertState) => boolean;
alerts: AlertsByName;
}
export const AlertsCallout: React.FC<Props> = (props: Props) => {
const { alerts, stateFilter = () => true } = props;
const { alerts } = props;
const inSetupMode = isInSetupMode(React.useContext(SetupModeContext));
const callouts = TYPES.map((type) => {
const list = [];
for (const alertTypeId of Object.keys(alerts)) {
const alertInstance = alerts[alertTypeId];
for (const { firing, state } of alertInstance.states) {
if (firing && stateFilter(state) && state.ui.severity === type.severity) {
list.push(state);
}
}
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,
});
}
}
if (list.length) {
return (
<Fragment>
<EuiCallOut title={type.label} color={type.severity} iconType="bell">
<ul>
{list.map((state, index) => {
const nextStepsUi =
state.ui.message.nextSteps && state.ui.message.nextSteps.length ? (
<ul>
{state.ui.message.nextSteps.map(
(step: AlertMessage, nextStepIndex: number) => (
<li key={nextStepIndex}>{replaceTokens(step)}</li>
)
)}
</ul>
) : null;
if (list.length === 0) {
return null;
}
return (
<li key={index}>
{replaceTokens(state.ui.message)}
{nextStepsUi}
</li>
);
})}
</ul>
</EuiCallOut>
<EuiSpacer />
</Fragment>
);
}
const accordions = list.map((status, index) => {
const buttonContent = (
<div>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon type="alert" size="m" color="danger" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTextColor color="danger">
{replaceTokens(status.state.state.ui.message)}
</EuiTextColor>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
const accordion = (
<EuiAccordion
id={`monitoringAlertCallout_${index}`}
buttonContent={buttonContent}
paddingSize="s"
>
<EuiListGroup
flush={true}
bordered={true}
gutterSize="m"
size="xs"
style={{
marginTop: '0.5rem',
paddingTop: '0.5rem',
paddingBottom: '0.5rem',
paddingLeft: `0.5rem`,
}}
>
{(status.state.state.ui.message.nextSteps || []).map((step: AlertMessage) => {
return <EuiListGroupItem onClick={() => {}} label={replaceTokens(step)} />;
})}
<EuiListGroupItem
label={<AlertConfiguration alert={status.alert.rawAlert} compressed />}
/>
</EuiListGroup>
</EuiAccordion>
);
const spacer = index !== list.length - 1 ? <EuiSpacer /> : null;
return (
<div key={index}>
{accordion}
{spacer}
</div>
);
});
return <Fragment>{callouts}</Fragment>;
return (
<Fragment>
<EuiPanel>{accordions}</EuiPanel>
<EuiSpacer />
</Fragment>
);
};

View file

@ -0,0 +1,166 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui';
import { CommonAlert } from '../../common/types/alerts';
import { Legacy } from '../legacy_shims';
import { hideBottomBar, showBottomBar } from '../lib/setup_mode';
import { BASE_ALERT_API_PATH } from '../../../alerts/common';
interface Props {
alert: CommonAlert;
compressed?: boolean;
}
export const AlertConfiguration: React.FC<Props> = (props: Props) => {
const { alert, compressed } = props;
const [showFlyout, setShowFlyout] = React.useState(false);
const [isEnabled, setIsEnabled] = React.useState(alert.enabled);
const [isMuted, setIsMuted] = React.useState(alert.muteAll);
const [isSaving, setIsSaving] = React.useState(false);
async function disableAlert() {
setIsSaving(true);
try {
await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.id}/_disable`);
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', {
defaultMessage: `Unable to disable alert`,
}),
text: err.message,
});
}
setIsSaving(false);
}
async function enableAlert() {
setIsSaving(true);
try {
await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.id}/_enable`);
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', {
defaultMessage: `Unable to enable alert`,
}),
text: err.message,
});
}
setIsSaving(false);
}
async function muteAlert() {
setIsSaving(true);
try {
await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.id}/_mute_all`);
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', {
defaultMessage: `Unable to mute alert`,
}),
text: err.message,
});
}
setIsSaving(false);
}
async function unmuteAlert() {
setIsSaving(true);
try {
await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.id}/_unmute_all`);
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', {
defaultMessage: `Unable to unmute alert`,
}),
text: err.message,
});
}
setIsSaving(false);
}
const flyoutUi = useMemo(
() =>
showFlyout &&
Legacy.shims.triggersActionsUi.getEditAlertFlyout({
initialAlert: alert,
onClose: () => {
setShowFlyout(false);
showBottomBar();
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[showFlyout]
);
return (
<Fragment>
<EuiFlexGroup
justifyContent={compressed ? 'flexStart' : 'spaceBetween'}
gutterSize={compressed ? 'm' : 'xs'}
alignItems="center"
>
<EuiFlexItem grow={false}>
<EuiButton
size={compressed ? 's' : 'm'}
onClick={() => {
setShowFlyout(true);
hideBottomBar();
}}
>
{i18n.translate('xpack.monitoring.alerts.panel.editAlert', {
defaultMessage: `Edit alert`,
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
name="disable"
disabled={isSaving}
checked={!isEnabled}
onChange={async () => {
if (isEnabled) {
setIsEnabled(false);
await disableAlert();
} else {
setIsEnabled(true);
await enableAlert();
}
}}
label={
<FormattedMessage
id="xpack.monitoring.alerts.panel.disableTitle"
defaultMessage="Disable"
/>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
name="mute"
disabled={isSaving}
checked={isMuted}
data-test-subj="muteSwitch"
onChange={async () => {
if (isMuted) {
setIsMuted(false);
await unmuteAlert();
} else {
setIsMuted(true);
await muteAlert();
}
}}
label={
<FormattedMessage
id="xpack.monitoring.alerts.panel.muteTitle"
defaultMessage="Mute"
/>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
{flyoutUi}
</Fragment>
);
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { AlertsByName } from './types';
export interface IAlertsContext {
allAlerts: AlertsByName;
}
export const AlertsContext = React.createContext({
allAlerts: {} as AlertsByName,
});

View file

@ -0,0 +1,660 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getAlertPanelsByNode should not show any alert if none are firing 1`] = `
Array [
Object {
"id": 0,
"items": Array [],
"title": "Alerts",
},
]
`;
exports[`getAlertPanelsByNode should properly group for alerts in a single category 1`] = `
Array [
Object {
"id": 0,
"items": Array [
Object {
"name": <EuiText>
es_name_0
(
1
)
</EuiText>,
"panel": 1,
},
Object {
"name": <EuiText>
es_name_1
(
1
)
</EuiText>,
"panel": 2,
},
],
"title": "Alerts",
},
Object {
"id": 1,
"items": Array [
Object {
"name": <React.Fragment>
<EuiToolTip
content="triggered:0"
delay="regular"
position="top"
>
<EuiText
size="s"
>
triggered:0
</EuiText>
</EuiToolTip>
<EuiText
size="s"
>
monitoring_alert_jvm_memory_usage_label
</EuiText>
</React.Fragment>,
"panel": 3,
},
],
"title": "es_name_0",
},
Object {
"id": 2,
"items": Array [
Object {
"name": <React.Fragment>
<EuiToolTip
content="triggered:0"
delay="regular"
position="top"
>
<EuiText
size="s"
>
triggered:0
</EuiText>
</EuiToolTip>
<EuiText
size="s"
>
monitoring_alert_jvm_memory_usage_label
</EuiText>
</React.Fragment>,
"panel": 4,
},
],
"title": "es_name_1",
},
Object {
"content": <AlertPanel
alert={
Object {
"actions": Array [],
"alertTypeId": "monitoring_alert_jvm_memory_usage",
"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": "",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_jvm_memory_usage_label",
"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": "es0",
"nodeName": "es_name_0",
"ui": Object {
"isFiring": false,
"lastCheckedMS": 0,
"message": Object {
"text": "",
},
"resolvedMS": 0,
"severity": "danger",
"triggeredMS": 0,
},
},
}
}
/>,
"id": 3,
"title": "monitoring_alert_jvm_memory_usage_label",
"width": 400,
},
Object {
"content": <AlertPanel
alert={
Object {
"actions": Array [],
"alertTypeId": "monitoring_alert_jvm_memory_usage",
"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": "",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_jvm_memory_usage_label",
"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": 4,
"title": "monitoring_alert_jvm_memory_usage_label",
"width": 400,
},
]
`;
exports[`getAlertPanelsByNode should properly group for alerts in each category 1`] = `
Array [
Object {
"id": 0,
"items": Array [
Object {
"name": <EuiText>
es_name_0
(
3
)
</EuiText>,
"panel": 1,
},
Object {
"name": <EuiText>
es_name_1
(
2
)
</EuiText>,
"panel": 2,
},
],
"title": "Alerts",
},
Object {
"id": 1,
"items": Array [
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
</EuiText>
</React.Fragment>,
"panel": 3,
},
Object {
"name": <React.Fragment>
<EuiToolTip
content="triggered:0"
delay="regular"
position="top"
>
<EuiText
size="s"
>
triggered:0
</EuiText>
</EuiToolTip>
<EuiText
size="s"
>
monitoring_alert_disk_usage_label
</EuiText>
</React.Fragment>,
"panel": 4,
},
Object {
"name": <React.Fragment>
<EuiToolTip
content="triggered:0"
delay="regular"
position="top"
>
<EuiText
size="s"
>
triggered:0
</EuiText>
</EuiToolTip>
<EuiText
size="s"
>
monitoring_alert_license_expiration_label
</EuiText>
</React.Fragment>,
"panel": 5,
},
],
"title": "es_name_0",
},
Object {
"id": 2,
"items": Array [
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
</EuiText>
</React.Fragment>,
"panel": 6,
},
Object {
"name": <React.Fragment>
<EuiToolTip
content="triggered:0"
delay="regular"
position="top"
>
<EuiText
size="s"
>
triggered:0
</EuiText>
</EuiToolTip>
<EuiText
size="s"
>
monitoring_alert_license_expiration_label
</EuiText>
</React.Fragment>,
"panel": 7,
},
],
"title": "es_name_1",
},
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": "",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_nodes_changed_label",
"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": "es0",
"nodeName": "es_name_0",
"ui": Object {
"isFiring": false,
"lastCheckedMS": 0,
"message": Object {
"text": "",
},
"resolvedMS": 0,
"severity": "danger",
"triggeredMS": 0,
},
},
}
}
/>,
"id": 3,
"title": "monitoring_alert_nodes_changed_label",
"width": 400,
},
Object {
"content": <AlertPanel
alert={
Object {
"actions": Array [],
"alertTypeId": "monitoring_alert_disk_usage",
"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": "",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_disk_usage_label",
"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": "es0",
"nodeName": "es_name_0",
"ui": Object {
"isFiring": false,
"lastCheckedMS": 0,
"message": Object {
"text": "",
},
"resolvedMS": 0,
"severity": "danger",
"triggeredMS": 0,
},
},
}
}
/>,
"id": 4,
"title": "monitoring_alert_disk_usage_label",
"width": 400,
},
Object {
"content": <AlertPanel
alert={
Object {
"actions": Array [],
"alertTypeId": "monitoring_alert_license_expiration",
"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": "",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_license_expiration_label",
"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": "es0",
"nodeName": "es_name_0",
"ui": Object {
"isFiring": false,
"lastCheckedMS": 0,
"message": Object {
"text": "",
},
"resolvedMS": 0,
"severity": "danger",
"triggeredMS": 0,
},
},
}
}
/>,
"id": 5,
"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": "",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_nodes_changed_label",
"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": 6,
"title": "monitoring_alert_nodes_changed_label",
"width": 400,
},
Object {
"content": <AlertPanel
alert={
Object {
"actions": Array [],
"alertTypeId": "monitoring_alert_license_expiration",
"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": "",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "monitoring_alert_license_expiration_label",
"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": 7,
"title": "monitoring_alert_license_expiration_label",
"width": 400,
},
]
`;

View file

@ -0,0 +1,212 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
ALERTS,
ALERT_CPU_USAGE,
ALERT_LOGSTASH_VERSION_MISMATCH,
ALERT_THREAD_POOL_WRITE_REJECTIONS,
} from '../../../common/constants';
import { AlertSeverity } from '../../../common/enums';
import { getAlertPanelsByCategory } from './get_alert_panels_by_category';
import {
ALERT_LICENSE_EXPIRATION,
ALERT_NODES_CHANGED,
ALERT_DISK_USAGE,
ALERT_MEMORY_USAGE,
} from '../../../common/constants';
import { AlertsByName } from '../types';
import { AlertExecutionStatusValues } from '../../../../alerts/common';
import { AlertState } from '../../../common/types/alerts';
jest.mock('../../legacy_shims', () => ({
Legacy: {
shims: {
uiSettings: {
get: () => '',
},
},
},
}));
jest.mock('../../../common/formatting', () => ({
getDateFromNow: (timestamp: number) => `triggered:${timestamp}`,
getCalendar: (timestamp: number) => `triggered:${timestamp}`,
}));
const mockAlert = {
id: '',
enabled: true,
tags: [],
consumer: '',
schedule: { interval: '1m' },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
createdAt: new Date('2020-12-08'),
updatedAt: new Date('2020-12-08'),
apiKey: null,
apiKeyOwner: null,
throttle: null,
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: AlertExecutionStatusValues[0],
lastExecutionDate: new Date('2020-12-08'),
},
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,
severity: AlertSeverity.Danger,
message: { text: '' },
resolvedMS: 0,
lastCheckedMS: 0,
triggeredMS: 0,
};
const cluster = { clusterUuid: '1', clusterName: 'one' };
function getAlert(type: string, firingCount: number) {
const states = [];
for (let fi = 0; fi < firingCount; fi++) {
states.push({
firing: true,
meta: {},
state: {
cluster,
ui: {
...ui,
triggeredMS: fi,
},
nodeId: `es${fi}`,
nodeName: `es_name_${fi}`,
},
});
}
return {
states,
rawAlert: {
alertTypeId: type,
name: `${type}_label`,
...mockAlert,
},
};
}
const alertsContext = {
allAlerts: getAllAlerts(),
};
const stateFilter = (state: AlertState) => true;
const panelTitle = 'Alerts';
describe('non setup mode', () => {
it('should properly group for alerts in each category', () => {
const alerts = [
getAlert(ALERT_NODES_CHANGED, 2),
getAlert(ALERT_DISK_USAGE, 1),
getAlert(ALERT_LICENSE_EXPIRATION, 2),
];
const result = getAlertPanelsByCategory(
panelTitle,
false,
alerts,
alertsContext,
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
);
expect(result).toMatchSnapshot();
});
it('should not show any alert if none are firing', () => {
const alerts = [
getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0),
getAlert(ALERT_CPU_USAGE, 0),
getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0),
];
const result = getAlertPanelsByCategory(
panelTitle,
false,
alerts,
alertsContext,
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
);
expect(result).toMatchSnapshot();
});
});
describe('setup mode', () => {
it('should properly group for alerts in each category', () => {
const alerts = [
getAlert(ALERT_NODES_CHANGED, 2),
getAlert(ALERT_DISK_USAGE, 1),
getAlert(ALERT_LICENSE_EXPIRATION, 2),
];
const result = getAlertPanelsByCategory(panelTitle, true, alerts, alertsContext, 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);
expect(result).toMatchSnapshot();
});
it('should still show alerts if none are firing', () => {
const alerts = [
getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0),
getAlert(ALERT_CPU_USAGE, 0),
getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0),
];
const result = getAlertPanelsByCategory(panelTitle, true, alerts, alertsContext, stateFilter);
expect(result).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,228 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
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 { PanelItem } from '../types';
import { sortByNewestAlert } from './sort_by_newest_alert';
import { Legacy } from '../../legacy_shims';
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
);
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,
});
categoryFiringAlertCount += states.length;
}
}
}
if (firingAlertsInCategory.length > 0) {
menu.push({
...category,
alertCount: categoryFiringAlertCount,
alerts: firingAlertsInCategory,
});
}
}
}
for (const item of menu) {
for (const alert of item.alerts) {
alert.states.sort(sortByNewestAlert);
}
}
const panels: PanelItem[] = [
{
id: 0,
title: panelTitle,
items: [
...menu.map((category, index) => {
const name = inSetupMode ? (
<EuiText>{category.label}</EuiText>
) : (
<Fragment>
<EuiText>
{category.label} ({category.alertCount})
</EuiText>
</Fragment>
);
return {
name,
panel: index + 1,
};
}),
],
},
];
if (inSetupMode) {
let secondaryPanelIndex = menu.length;
let tertiaryPanelIndex = menu.length;
let nodeIndex = 0;
for (const category of menu) {
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,
};
}),
});
nodeIndex++;
}
for (const category of menu) {
for (const { alert, alertName } of category.alerts) {
const alertStatus = alertsContext.allAlerts[alertName];
panels.push({
id: ++tertiaryPanelIndex,
title: `${alert.name}`,
width: 400,
content: <AlertPanel alert={alertStatus.rawAlert} />,
});
}
}
} else {
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 }) => {
const filteredStates = states.filter(({ state }) => stateFilter(state));
const alertStatus = alertsContext.allAlerts[alertName];
const name = inSetupMode ? (
<EuiText>{alertStatus.rawAlert.name}</EuiText>
) : (
<EuiText>
{alertStatus.rawAlert.name} ({filteredStates.length})
</EuiText>
);
return {
name,
panel: ++primaryPanelIndex,
};
}),
});
nodeIndex++;
}
let secondaryPanelIndex = menu.length;
let tertiaryPanelIndex = menu.reduce((count, category) => {
count += category.alerts.length;
return count;
}, menu.length);
for (const category of menu) {
for (const { alert, states } of category.alerts) {
const items = [];
for (const alertState of states.filter(({ state }) => stateFilter(state))) {
items.push({
name: (
<Fragment>
<EuiToolTip
position="top"
content={getCalendar(
alertState.state.ui.triggeredMS,
Legacy.shims.uiSettings.get('dateFormat:tz')
)}
>
<EuiText size="s">
{getDateFromNow(
alertState.state.ui.triggeredMS,
Legacy.shims.uiSettings.get('dateFormat:tz')
)}
</EuiText>
</EuiToolTip>
<EuiText size="s">{alertState.state.nodeName}</EuiText>
</Fragment>
),
panel: ++tertiaryPanelIndex,
});
items.push({
isSeparator: true as const,
});
}
panels.push({
id: ++secondaryPanelIndex,
title: `${alert.name}`,
items,
});
}
}
let tertiaryPanelIndex2 = menu.reduce((count, category) => {
count += category.alerts.length;
return count;
}, menu.length);
for (const category of menu) {
for (const { alert, states } of category.alerts) {
for (const state of states.filter(({ state: _state }) => stateFilter(_state))) {
panels.push({
id: ++tertiaryPanelIndex2,
title: `${alert.name}`,
width: 400,
content: <AlertPanel alert={alert} alertState={state} />,
});
}
}
}
}
return panels;
}

View file

@ -0,0 +1,128 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
ALERT_CPU_USAGE,
ALERT_LOGSTASH_VERSION_MISMATCH,
ALERT_THREAD_POOL_WRITE_REJECTIONS,
} from '../../../common/constants';
import { AlertSeverity } from '../../../common/enums';
import { getAlertPanelsByNode } from './get_alert_panels_by_node';
import {
ALERT_LICENSE_EXPIRATION,
ALERT_NODES_CHANGED,
ALERT_DISK_USAGE,
ALERT_MEMORY_USAGE,
} from '../../../common/constants';
import { AlertExecutionStatusValues } from '../../../../alerts/common';
import { AlertState } from '../../../common/types/alerts';
jest.mock('../../legacy_shims', () => ({
Legacy: {
shims: {
uiSettings: {
get: () => '',
},
},
},
}));
jest.mock('../../../common/formatting', () => ({
getDateFromNow: (timestamp: number) => `triggered:${timestamp}`,
getCalendar: (timestamp: number) => `triggered:${timestamp}`,
}));
const mockAlert = {
id: '',
enabled: true,
tags: [],
consumer: '',
schedule: { interval: '1m' },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
createdAt: new Date('2020-12-08'),
updatedAt: new Date('2020-12-08'),
apiKey: null,
apiKeyOwner: null,
throttle: null,
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: AlertExecutionStatusValues[0],
lastExecutionDate: new Date('2020-12-08'),
},
notifyWhen: null,
};
describe('getAlertPanelsByNode', () => {
const ui = {
isFiring: false,
severity: AlertSeverity.Danger,
message: { text: '' },
resolvedMS: 0,
lastCheckedMS: 0,
triggeredMS: 0,
};
const cluster = { clusterUuid: '1', clusterName: 'one' };
function getAlert(type: string, firingCount: number) {
const states = [];
for (let fi = 0; fi < firingCount; fi++) {
states.push({
firing: true,
meta: {},
state: {
cluster,
ui,
nodeId: `es${fi}`,
nodeName: `es_name_${fi}`,
},
});
}
return {
rawAlert: {
alertTypeId: type,
name: `${type}_label`,
...mockAlert,
},
states,
};
}
const panelTitle = 'Alerts';
const stateFilter = (state: AlertState) => true;
it('should properly group for alerts in each category', () => {
const alerts = [
getAlert(ALERT_NODES_CHANGED, 2),
getAlert(ALERT_DISK_USAGE, 1),
getAlert(ALERT_LICENSE_EXPIRATION, 2),
];
const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
it('should properly group for alerts in a single category', () => {
const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)];
const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
it('should not show any alert if none are firing', () => {
const alerts = [
getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0),
getAlert(ALERT_CPU_USAGE, 0),
getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0),
];
const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter);
expect(result).toMatchSnapshot();
});
});

View file

@ -0,0 +1,138 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import { EuiText, EuiToolTip } from '@elastic/eui';
import { AlertPanel } from '../panel';
import {
CommonAlertStatus,
CommonAlertState,
CommonAlert,
AlertState,
} from '../../../common/types/alerts';
import { getDateFromNow, getCalendar } from '../../../common/formatting';
import { PanelItem } from '../types';
import { sortByNewestAlert } from './sort_by_newest_alert';
import { Legacy } from '../../legacy_shims';
export function getAlertPanelsByNode(
panelTitle: string,
alerts: CommonAlertStatus[],
stateFilter: (state: AlertState) => boolean
) {
const alertsByNodes: {
[uuid: string]: {
[alertName: string]: {
alert: CommonAlert;
states: CommonAlertState[];
count: number;
};
};
} = {};
const statesByNodes: {
[uuid: string]: CommonAlertState[];
} = {};
for (const { states, rawAlert } of alerts) {
const { alertTypeId } = 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);
}
}
for (const types of Object.values(alertsByNodes)) {
for (const { states } of Object.values(types)) {
states.sort(sortByNewestAlert);
}
}
const nodeCount = Object.keys(statesByNodes).length;
let secondaryPanelIndex = nodeCount;
let tertiaryPanelIndex = nodeCount;
const panels: PanelItem[] = [
{
id: 0,
title: panelTitle,
items: [
...Object.keys(statesByNodes).map((nodeUuid, index) => {
const states = (statesByNodes[nodeUuid] as CommonAlertState[]).filter(({ state }) =>
stateFilter(state)
);
return {
name: (
<EuiText>
{states[0].state.nodeName} ({states.length})
</EuiText>
),
panel: index + 1,
};
}),
],
},
...Object.keys(statesByNodes).reduce((accum: PanelItem[], nodeUuid, nodeIndex) => {
const alertsForNode = Object.values(alertsByNodes[nodeUuid]);
const panelItems = [];
let title = '';
for (const { alert, states } of alertsForNode) {
for (const alertState of states) {
title = alertState.state.nodeName;
panelItems.push({
name: (
<Fragment>
<EuiToolTip
position="top"
content={getCalendar(
alertState.state.ui.triggeredMS,
Legacy.shims.uiSettings.get('dateFormat:tz')
)}
>
<EuiText size="s">
{getDateFromNow(
alertState.state.ui.triggeredMS,
Legacy.shims.uiSettings.get('dateFormat:tz')
)}
</EuiText>
</EuiToolTip>
<EuiText size="s">{alert.name}</EuiText>
</Fragment>
),
panel: ++secondaryPanelIndex,
});
}
}
accum.push({
id: nodeIndex + 1,
title,
items: panelItems,
});
return accum;
}, []),
...Object.keys(statesByNodes).reduce((accum: PanelItem[], nodeUuid, nodeIndex) => {
const alertsForNode = Object.values(alertsByNodes[nodeUuid]);
for (const { alert, states } of alertsForNode) {
for (const alertState of states) {
accum.push({
id: ++tertiaryPanelIndex,
title: alert.name,
width: 400,
content: <AlertPanel alert={alert} alertState={alertState} />,
});
}
}
return accum;
}, []),
];
return panels;
}

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AlertSeverity } from '../../../common/enums';
import { sortByNewestAlert } from './sort_by_newest_alert';
describe('sortByNewestAlert', () => {
const ui = {
isFiring: false,
severity: AlertSeverity.Danger,
message: { text: '' },
resolvedMS: 0,
lastCheckedMS: 0,
triggeredMS: 0,
};
const cluster = { clusterUuid: '1', clusterName: 'one' };
it('should sort properly', () => {
const a = {
firing: false,
meta: {},
state: {
cluster,
ui: {
...ui,
triggeredMS: 2,
},
nodeId: `es1`,
nodeName: `es_name_1`,
},
};
const b = {
firing: false,
meta: {},
state: {
cluster,
ui: {
...ui,
triggeredMS: 1,
},
nodeId: `es1`,
nodeName: `es_name_1`,
},
};
expect(sortByNewestAlert(a, b)).toBe(-1);
});
});

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CommonAlertState } from '../../../common/types/alerts';
export function sortByNewestAlert(a: CommonAlertState, b: CommonAlertState) {
if (a.state.ui.triggeredMS === b.state.ui.triggeredMS) {
return 0;
}
return a.state.ui.triggeredMS < b.state.ui.triggeredMS ? 1 : -1;
}

View file

@ -3,186 +3,39 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment } from 'react';
import {
EuiSpacer,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiSwitch,
EuiTitle,
EuiHorizontalRule,
EuiListGroup,
EuiListGroupItem,
} from '@elastic/eui';
import { CommonAlertStatus, CommonAlertState, AlertMessage } from '../../common/types/alerts';
import { Legacy } from '../legacy_shims';
import { CommonAlert, CommonAlertState, AlertMessage } from '../../common/types/alerts';
import { replaceTokens } from './lib/replace_tokens';
import { isInSetupMode, hideBottomBar, showBottomBar } from '../lib/setup_mode';
import { BASE_ALERT_API_PATH } from '../../../alerts/common';
import { isInSetupMode } from '../lib/setup_mode';
import { SetupModeContext } from '../components/setup_mode/setup_mode_context';
import { AlertConfiguration } from './configuration';
interface Props {
alert: CommonAlertStatus;
alert: CommonAlert;
alertState?: CommonAlertState;
}
export const AlertPanel: React.FC<Props> = (props: Props) => {
const {
alert: { rawAlert },
alertState,
} = props;
const [showFlyout, setShowFlyout] = React.useState(false);
const [isEnabled, setIsEnabled] = React.useState(rawAlert?.enabled);
const [isMuted, setIsMuted] = React.useState(rawAlert?.muteAll);
const [isSaving, setIsSaving] = React.useState(false);
const { alert, alertState } = props;
const inSetupMode = isInSetupMode(React.useContext(SetupModeContext));
const flyoutUi = useMemo(
() =>
showFlyout &&
Legacy.shims.triggersActionsUi.getEditAlertFlyout({
initialAlert: rawAlert,
onClose: () => {
setShowFlyout(false);
showBottomBar();
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[showFlyout]
);
if (!rawAlert) {
if (!alert) {
return null;
}
async function disableAlert() {
setIsSaving(true);
try {
await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${rawAlert.id}/_disable`);
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', {
defaultMessage: `Unable to disable alert`,
}),
text: err.message,
});
}
setIsSaving(false);
}
async function enableAlert() {
setIsSaving(true);
try {
await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${rawAlert.id}/_enable`);
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', {
defaultMessage: `Unable to enable alert`,
}),
text: err.message,
});
}
setIsSaving(false);
}
async function muteAlert() {
setIsSaving(true);
try {
await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${rawAlert.id}/_mute_all`);
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', {
defaultMessage: `Unable to mute alert`,
}),
text: err.message,
});
}
setIsSaving(false);
}
async function unmuteAlert() {
setIsSaving(true);
try {
await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${rawAlert.id}/_unmute_all`);
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', {
defaultMessage: `Unable to unmute alert`,
}),
text: err.message,
});
}
setIsSaving(false);
}
const configurationUi = (
<Fragment>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => {
setShowFlyout(true);
hideBottomBar();
}}
>
{i18n.translate('xpack.monitoring.alerts.panel.editAlert', {
defaultMessage: `Edit alert`,
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
name="disable"
disabled={isSaving}
checked={!isEnabled}
onChange={async () => {
if (isEnabled) {
setIsEnabled(false);
await disableAlert();
} else {
setIsEnabled(true);
await enableAlert();
}
}}
label={
<FormattedMessage
id="xpack.monitoring.alerts.panel.disableTitle"
defaultMessage="Disable"
/>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
name="mute"
disabled={isSaving}
checked={isMuted}
data-test-subj="muteSwitch"
onChange={async () => {
if (isMuted) {
setIsMuted(false);
await unmuteAlert();
} else {
setIsMuted(true);
await muteAlert();
}
}}
label={
<FormattedMessage
id="xpack.monitoring.alerts.panel.muteTitle"
defaultMessage="Mute"
/>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
{flyoutUi}
</Fragment>
);
if (inSetupMode || !alertState) {
return <div style={{ padding: '1rem' }}>{configurationUi}</div>;
return (
<div style={{ padding: '1rem' }}>
<AlertConfiguration alert={alert} />
</div>
);
}
const nextStepsUi =
@ -204,7 +57,9 @@ export const AlertPanel: React.FC<Props> = (props: Props) => {
{nextStepsUi}
</div>
<EuiHorizontalRule margin="s" />
<div style={{ padding: '0 1rem 1rem 1rem' }}>{configurationUi}</div>
<div style={{ padding: '0 1rem 1rem 1rem' }}>
<AlertConfiguration alert={alert} />
</div>
</Fragment>
);
};

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { CommonAlertStatus } from '../../common/types/alerts';
export interface AlertsByName {
[name: string]: CommonAlertStatus;
}
export interface PanelItem {
id: number;
title: string;
width?: number;
content?: React.ReactElement;
items?: Array<ContextMenuItem | ContextMenuItemSeparator>;
}
export interface ContextMenuItem {
name: React.ReactElement;
panel?: number;
onClick?: () => void;
}
export interface ContextMenuItemSeparator {
isSeparator: true;
}

View file

@ -20,7 +20,7 @@ import { MonitoringTimeseriesContainer } from '../../chart';
import { FormattedMessage } from '@kbn/i18n/react';
import { AlertsCallout } from '../../../alerts/callout';
export const AdvancedNode = ({ nodeSummary, metrics, alerts, nodeId, ...props }) => {
export const AdvancedNode = ({ nodeSummary, metrics, alerts, ...props }) => {
const metricsToShow = [
metrics.node_gc,
metrics.node_gc_time,
@ -54,7 +54,7 @@ export const AdvancedNode = ({ nodeSummary, metrics, alerts, nodeId, ...props })
<NodeDetailStatus stats={nodeSummary} alerts={alerts} />
</EuiPanel>
<EuiSpacer size="m" />
<AlertsCallout alerts={alerts} stateFilter={(state) => state.nodeId === nodeId} />
<AlertsCallout alerts={alerts} />
<EuiPageContent>
<EuiFlexGrid columns={2} gutterSize="s">
{metricsToShow.map((metric, index) => (

View file

@ -130,7 +130,6 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler
defaultMessage: 'Alerts',
}),
field: 'alerts',
// width: '175px',
sortable: true,
render: (_field, node) => {
return (

View file

@ -13,6 +13,7 @@ import { Legacy } from '../legacy_shims';
import { PromiseWithCancel } from '../../common/cancel_promise';
import { SetupModeFeature } from '../../common/enums';
import { updateSetupModeData, isSetupModeFeatureEnabled } from '../lib/setup_mode';
import { AlertsContext } from '../alerts/context';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
/**
@ -243,11 +244,13 @@ export class MonitoringViewBaseController {
const wrappedComponent = (
<KibanaContextProvider services={Legacy.shims.kibanaServices}>
<I18nContext>
{!this._isDataInitialized ? (
<PageLoading pageViewTitle={trackPageView ? this.telemetryPageViewTitle : null} />
) : (
component
)}
<AlertsContext.Provider value={{ allAlerts: this.alerts }}>
{!this._isDataInitialized ? (
<PageLoading pageViewTitle={trackPageView ? this.telemetryPageViewTitle : null} />
) : (
component
)}
</AlertsContext.Provider>
</I18nContext>
</KibanaContextProvider>
);

View file

@ -14,6 +14,7 @@ import { uiRoutes } from '../../../angular/helpers/routes';
import { routeInitProvider } from '../../../lib/route_init';
import { getPageData } from './get_page_data';
import template from './index.html';
import { SetupModeRenderer } from '../../../components/renderers';
import { Node } from '../../../components/elasticsearch/node/node';
import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels';
import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices';
@ -26,7 +27,9 @@ import {
ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
ALERT_MEMORY_USAGE,
ELASTICSEARCH_SYSTEM_ID,
} from '../../../../common/constants';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
uiRoutes.when('/elasticsearch/nodes/:node', {
template,
@ -122,14 +125,26 @@ uiRoutes.when('/elasticsearch/nodes/:node', {
$scope.labels = labels.node;
this.renderReact(
<Node
<SetupModeRenderer
scope={$scope}
alerts={this.alerts}
nodeId={this.nodeName}
clusterUuid={$scope.cluster.cluster_uuid}
onBrush={this.onBrush}
zoomInfo={this.zoomInfo}
{...data}
injector={$injector}
productName={ELASTICSEARCH_SYSTEM_ID}
render={({ setupMode, flyoutComponent, bottomBarComponent }) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>
{flyoutComponent}
<Node
scope={$scope}
setupMode={setupMode}
alerts={this.alerts}
nodeId={this.nodeName}
clusterUuid={$scope.cluster.cluster_uuid}
onBrush={this.onBrush}
zoomInfo={this.zoomInfo}
{...data}
/>
{bottomBarComponent}
</SetupModeContext.Provider>
)}
/>
);
}

View file

@ -42,6 +42,7 @@ import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity';
interface LegacyOptions {
watchName: string;
nodeNameLabel: string;
changeDataValues?: Partial<AlertData>;
}
@ -322,6 +323,7 @@ export class BaseAlert {
shouldFire: !legacyAlert.resolved_timestamp,
severity: mapLegacySeverity(legacyAlert.metadata.severity),
meta: legacyAlert,
nodeName: this.alertOptions.legacy!.nodeNameLabel,
...this.alertOptions.legacy!.changeDataValues,
};
});
@ -394,6 +396,7 @@ export class BaseAlert {
}
const cluster = clusters.find((c: AlertCluster) => c.clusterUuid === item.clusterUuid);
const alertState: AlertState = this.getDefaultAlertState(cluster!, item);
alertState.nodeName = item.nodeName;
alertState.ui.triggeredMS = currentUTC;
alertState.ui.isFiring = true;
alertState.ui.severity = item.severity;

View file

@ -119,6 +119,7 @@ describe('ClusterHealthAlert', () => {
{
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
ccs: undefined,
nodeName: 'Elasticsearch cluster alert',
ui: {
isFiring: true,
message: {

View file

@ -38,6 +38,9 @@ export class ClusterHealthAlert extends BaseAlert {
name: LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label,
legacy: {
watchName: 'elasticsearch_cluster_status',
nodeNameLabel: i18n.translate('xpack.monitoring.alerts.clusterHealth.nodeNameLabel', {
defaultMessage: 'Elasticsearch cluster alert',
}),
},
actionVariables: [
{

View file

@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import numeral from '@elastic/numeral';
import { BaseAlert } from './base_alert';
import {
AlertData,
@ -16,8 +17,8 @@ import {
AlertMessageTimeToken,
AlertMessageLinkToken,
AlertInstanceState,
CommonAlertFilter,
CommonAlertParams,
CommonAlertFilter,
} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
import {
@ -25,6 +26,8 @@ import {
ALERT_CPU_USAGE,
ALERT_DETAILS,
} from '../../common/constants';
// @ts-ignore
import { ROUNDED_FLOAT } from '../../common/formatting';
import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { AlertMessageTokenType, AlertSeverity } from '../../common/enums';
@ -121,7 +124,7 @@ export class CpuUsageAlert extends BaseAlert {
defaultMessage: `Node #start_link{nodeName}#end_link is reporting cpu usage of {cpuUsage}% at #absolute`,
values: {
nodeName: stat.nodeName,
cpuUsage: stat.cpuUsage,
cpuUsage: numeral(stat.cpuUsage).format(ROUNDED_FLOAT),
},
}),
nextSteps: [

View file

@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import numeral from '@elastic/numeral';
import { BaseAlert } from './base_alert';
import {
AlertData,
@ -15,8 +16,9 @@ import {
AlertMessageTimeToken,
AlertMessageLinkToken,
AlertInstanceState,
CommonAlertFilter,
CommonAlertParams,
AlertDiskUsageNodeStats,
CommonAlertFilter,
} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
import {
@ -24,6 +26,8 @@ import {
ALERT_DISK_USAGE,
ALERT_DETAILS,
} from '../../common/constants';
// @ts-ignore
import { ROUNDED_FLOAT } from '../../common/formatting';
import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { AlertMessageTokenType, AlertSeverity } from '../../common/enums';
@ -102,13 +106,13 @@ export class DiskUsageAlert extends BaseAlert {
}
protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage {
const stat = item.meta as AlertDiskUsageState;
const stat = item.meta as AlertDiskUsageNodeStats;
return {
text: i18n.translate('xpack.monitoring.alerts.diskUsage.ui.firingMessage', {
defaultMessage: `Node #start_link{nodeName}#end_link is reporting disk usage of {diskUsage}% at #absolute`,
values: {
nodeName: stat.nodeName,
diskUsage: stat.diskUsage,
diskUsage: numeral(stat.diskUsage).format(ROUNDED_FLOAT),
},
}),
nextSteps: [

View file

@ -124,6 +124,7 @@ describe('ElasticsearchVersionMismatchAlert', () => {
{
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
ccs: undefined,
nodeName: 'Elasticsearch node alert',
ui: {
isFiring: true,
message: {

View file

@ -26,6 +26,12 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert {
name: LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label,
legacy: {
watchName: 'elasticsearch_version_mismatch',
nodeNameLabel: i18n.translate(
'xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel',
{
defaultMessage: 'Elasticsearch node alert',
}
),
changeDataValues: { severity: AlertSeverity.Warning },
},
interval: '1d',

View file

@ -126,6 +126,7 @@ describe('KibanaVersionMismatchAlert', () => {
{
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
ccs: undefined,
nodeName: 'Kibana instance alert',
ui: {
isFiring: true,
message: {

View file

@ -26,6 +26,12 @@ export class KibanaVersionMismatchAlert extends BaseAlert {
name: LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label,
legacy: {
watchName: 'kibana_version_mismatch',
nodeNameLabel: i18n.translate(
'xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel',
{
defaultMessage: 'Kibana instance alert',
}
),
changeDataValues: { severity: AlertSeverity.Warning },
},
interval: '1d',

View file

@ -130,6 +130,7 @@ describe('LicenseExpirationAlert', () => {
{
cluster: { clusterUuid, clusterName },
ccs: undefined,
nodeName: 'Elasticsearch cluster alert',
ui: {
isFiring: true,
message: {

View file

@ -34,6 +34,9 @@ export class LicenseExpirationAlert extends BaseAlert {
name: LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label,
legacy: {
watchName: 'xpack_license_expiration',
nodeNameLabel: i18n.translate('xpack.monitoring.alerts.licenseExpiration.nodeNameLabel', {
defaultMessage: 'Elasticsearch cluster alert',
}),
},
interval: '1d',
actionVariables: [

View file

@ -125,6 +125,7 @@ describe('LogstashVersionMismatchAlert', () => {
{
cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' },
ccs: undefined,
nodeName: 'Logstash node alert',
ui: {
isFiring: true,
message: {

View file

@ -26,6 +26,12 @@ export class LogstashVersionMismatchAlert extends BaseAlert {
name: LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label,
legacy: {
watchName: 'logstash_version_mismatch',
nodeNameLabel: i18n.translate(
'xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel',
{
defaultMessage: 'Logstash node alert',
}
),
changeDataValues: { severity: AlertSeverity.Warning },
},
interval: '1d',

View file

@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import numeral from '@elastic/numeral';
import { BaseAlert } from './base_alert';
import {
AlertData,
@ -15,8 +16,9 @@ import {
AlertMessageTimeToken,
AlertMessageLinkToken,
AlertInstanceState,
CommonAlertFilter,
CommonAlertParams,
AlertMemoryUsageNodeStats,
CommonAlertFilter,
} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
import {
@ -24,6 +26,8 @@ import {
ALERT_MEMORY_USAGE,
ALERT_DETAILS,
} from '../../common/constants';
// @ts-ignore
import { ROUNDED_FLOAT } from '../../common/formatting';
import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { AlertMessageTokenType, AlertSeverity } from '../../common/enums';
@ -108,13 +112,13 @@ export class MemoryUsageAlert extends BaseAlert {
}
protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage {
const stat = item.meta as AlertMemoryUsageState;
const stat = item.meta as AlertMemoryUsageNodeStats;
return {
text: i18n.translate('xpack.monitoring.alerts.memoryUsage.ui.firingMessage', {
defaultMessage: `Node #start_link{nodeName}#end_link is reporting JVM memory usage of {memoryUsage}% at #absolute`,
values: {
nodeName: stat.nodeName,
memoryUsage: stat.memoryUsage,
memoryUsage: numeral(stat.memoryUsage).format(ROUNDED_FLOAT),
},
}),
nextSteps: [

View file

@ -128,9 +128,9 @@ describe('MissingMonitoringDataAlert', () => {
{
ccs: undefined,
cluster: { clusterUuid, clusterName },
gapDuration,
nodeName,
nodeId,
nodeName,
gapDuration,
ui: {
isFiring: true,
message: {

View file

@ -12,10 +12,9 @@ import {
AlertCluster,
AlertState,
AlertMessage,
AlertNodeState,
AlertMessageTimeToken,
CommonAlertFilter,
CommonAlertParams,
CommonAlertFilter,
} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
import {
@ -40,12 +39,12 @@ export class MissingMonitoringDataAlert extends BaseAlert {
super(rawAlert, {
id: ALERT_MISSING_MONITORING_DATA,
name: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].label,
accessorKey: 'gapDuration',
defaultParams: {
duration: '15m',
limit: '1d',
},
throttle: '6h',
accessorKey: 'gapDuration',
actionVariables: [
{
name: 'nodes',
@ -153,7 +152,7 @@ export class MissingMonitoringDataAlert extends BaseAlert {
protected executeActions(
instance: AlertInstance,
{ alertStates }: { alertStates: AlertNodeState[] },
{ alertStates }: { alertStates: AlertState[] },
item: AlertData | null,
cluster: AlertCluster
) {

View file

@ -137,6 +137,7 @@ describe('NodesChangedAlert', () => {
{
cluster: { clusterUuid, clusterName },
ccs: undefined,
nodeName: 'Elasticsearch nodes alert',
ui: {
isFiring: true,
message: {

View file

@ -26,6 +26,9 @@ export class NodesChangedAlert extends BaseAlert {
name: LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label,
legacy: {
watchName: 'elasticsearch_nodes',
nodeNameLabel: i18n.translate('xpack.monitoring.alerts.nodesChanged.nodeNameLabel', {
defaultMessage: 'Elasticsearch nodes alert',
}),
changeDataValues: { shouldFire: true },
},
actionVariables: [

View file

@ -13,8 +13,8 @@ import {
AlertThreadPoolRejectionsState,
AlertMessageTimeToken,
AlertMessageLinkToken,
CommonAlertFilter,
ThreadPoolRejectionsAlertParams,
CommonAlertFilter,
} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants';

View file

@ -47,6 +47,7 @@ describe('fetchLegacyAlerts', () => {
message,
metadata,
nodes,
nodeName: '',
prefix,
},
]);

View file

@ -86,6 +86,7 @@ export async function fetchLegacyAlerts(
message: get(hit, '_source.message'),
resolved_timestamp: get(hit, '_source.resolved_timestamp'),
nodes: get(hit, '_source.nodes'),
nodeName: '', // This is set by BaseAlert
metadata: get(hit, '_source.metadata') as LegacyAlertMetadata,
};
return legacyAlert;

View file

@ -11,6 +11,7 @@ Array [
"shardCount": 6,
"transport_address": "127.0.0.1:9300",
"type": "node",
"uuid": "_x_V2YzPQU-a9KRRBxUxZQ",
},
Object {
"isOnline": false,
@ -21,6 +22,7 @@ Array [
"shardCount": 6,
"transport_address": "127.0.0.1:9301",
"type": "node",
"uuid": "DAiX7fFjS3Wii7g2HYKrOg",
},
]
`;
@ -156,6 +158,7 @@ Array [
"shardCount": 0,
"transport_address": "127.0.0.1:9300",
"type": "master",
"uuid": "_x_V2YzPQU-a9KRRBxUxZQ",
},
Object {
"isOnline": true,
@ -265,6 +268,7 @@ Array [
"shardCount": 0,
"transport_address": "127.0.0.1:9301",
"type": "node",
"uuid": "DAiX7fFjS3Wii7g2HYKrOg",
},
]
`;
@ -286,6 +290,7 @@ Array [
"shardCount": 6,
"transport_address": "127.0.0.1:9300",
"type": "master",
"uuid": "_x_V2YzPQU-a9KRRBxUxZQ",
},
Object {
"isOnline": true,
@ -302,6 +307,7 @@ Array [
"shardCount": 6,
"transport_address": "127.0.0.1:9301",
"type": "node",
"uuid": "DAiX7fFjS3Wii7g2HYKrOg",
},
]
`;
@ -435,6 +441,7 @@ Array [
"shardCount": 6,
"transport_address": "127.0.0.1:9300",
"type": "master",
"uuid": "_x_V2YzPQU-a9KRRBxUxZQ",
},
Object {
"isOnline": true,
@ -544,6 +551,7 @@ Array [
"shardCount": 6,
"transport_address": "127.0.0.1:9301",
"type": "node",
"uuid": "DAiX7fFjS3Wii7g2HYKrOg",
},
]
`;

View file

@ -25,8 +25,9 @@ import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_me
*
* @param {Object} req: server request object
* @param {String} esIndexPattern: index pattern for elasticsearch data in monitoring indices
* @param {Object} pageOfNodes: server-side paginated current page of ES nodes
* @param {Object} clusterStats: cluster stats from cluster state document
* @param {Object} shardStats: per-node information about shards
* @param {Object} nodesShardCount: per-node information about shards
* @return {Array} node info combined with metrics for each node from handle_response
*/
export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, nodesShardCount) {

View file

@ -47,6 +47,7 @@ export function handleResponse(
// nodesInfo is the source of truth for the nodeIds, where nodesMetrics will lack metrics for offline nodes
const nodes = pageOfNodes.map((node) => ({
...node,
...nodesInfo[node.uuid],
...nodesMetrics[node.uuid],
resolver: node.uuid,

View file

@ -15,7 +15,7 @@ function defaultFilterFn(value, query) {
export function filter(data, queryText, fields, filterFn = defaultFilterFn) {
return data.filter((item) => {
for (const field of fields) {
if (filterFn(get(item, field), queryText)) {
if (filterFn(get(item, field, ''), queryText)) {
return true;
}
}

View file

@ -13564,8 +13564,6 @@
"xpack.monitoring.alerts.actionVariables.internalShortMessage": "内部メッセージ省略ありはElasticで生成されました。",
"xpack.monitoring.alerts.actionVariables.state": "現在のアラートの状態。",
"xpack.monitoring.alerts.badge.panelTitle": "アラート",
"xpack.monitoring.alerts.callout.dangerLabel": "危険アラート",
"xpack.monitoring.alerts.callout.warningLabel": "警告アラート",
"xpack.monitoring.alerts.clusterHealth.action.danger": "見つからないプライマリおよびレプリカシャードを割り当てます。",
"xpack.monitoring.alerts.clusterHealth.action.warning": "見つからないレプリカシャードを割り当てます。",
"xpack.monitoring.alerts.clusterHealth.actionVariables.clusterHealth": "クラスターの正常性。",

View file

@ -13581,8 +13581,6 @@
"xpack.monitoring.alerts.actionVariables.internalShortMessage": "Elastic 生成的简短内部消息。",
"xpack.monitoring.alerts.actionVariables.state": "告警的当前状态。",
"xpack.monitoring.alerts.badge.panelTitle": "告警",
"xpack.monitoring.alerts.callout.dangerLabel": "危险告警",
"xpack.monitoring.alerts.callout.warningLabel": "警告告警",
"xpack.monitoring.alerts.clusterHealth.action.danger": "分配缺失的主分片和副本分片。",
"xpack.monitoring.alerts.clusterHealth.action.warning": "分配缺失的副本分片。",
"xpack.monitoring.alerts.clusterHealth.actionVariables.clusterHealth": "集群的运行状况。",

View file

@ -139,7 +139,8 @@
"slope": -1
}
},
"resolver": "_x_V2YzPQU-a9KRRBxUxZQ"
"resolver": "_x_V2YzPQU-a9KRRBxUxZQ",
"uuid": "_x_V2YzPQU-a9KRRBxUxZQ"
},
{
"name": "hello02",
@ -247,7 +248,8 @@
"slope": -1
}
},
"resolver": "DAiX7fFjS3Wii7g2HYKrOg"
"resolver": "DAiX7fFjS3Wii7g2HYKrOg",
"uuid": "DAiX7fFjS3Wii7g2HYKrOg"
}
],
"totalNodeCount": 2

View file

@ -97,6 +97,7 @@
}
},
"resolver": "ENVgDIKRSdCVJo-YqY4kUQ",
"uuid": "ENVgDIKRSdCVJo-YqY4kUQ",
"shardCount": 54,
"transport_address": "127.0.0.1:9300",
"type": "master"
@ -185,6 +186,7 @@
}
},
"resolver": "t9J9jvHpQ2yDw9c1LJ0tHA",
"uuid": "t9J9jvHpQ2yDw9c1LJ0tHA",
"shardCount": 54,
"transport_address": "127.0.0.1:9301",
"type": "node"

View file

@ -97,6 +97,7 @@
}
},
"resolver": "_WmX0plYQwm2z6tfCPyCQw",
"uuid": "_WmX0plYQwm2z6tfCPyCQw",
"shardCount": 23,
"transport_address": "127.0.0.1:9300",
"type": "master"
@ -107,6 +108,7 @@
"nodeTypeClass": "storage",
"nodeTypeLabel": "Node",
"resolver": "1jxg5T33TWub-jJL4qP0Wg",
"uuid": "1jxg5T33TWub-jJL4qP0Wg",
"shardCount": 0,
"transport_address": "127.0.0.1:9302",
"type": "node"