diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 240e584e786ce..acbea037663e5 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -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 */ diff --git a/x-pack/plugins/monitoring/common/formatting.js b/x-pack/plugins/monitoring/common/formatting.ts similarity index 65% rename from x-pack/plugins/monitoring/common/formatting.js rename to x-pack/plugins/monitoring/common/formatting.ts index b2a67b3cd48da..65159f532d2aa 100644 --- a/x-pack/plugins/monitoring/common/formatting.js +++ b/x-pack/plugins/monitoring/common/formatting.ts @@ -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 guessed 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(); +} diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 0daa947b1c82a..0f10e0e48962b 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -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; } diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index b9e39e43ff73d..31a86757cac8e 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -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) => { + // 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(null); const inSetupMode = isInSetupMode(React.useContext(SetupModeContext)); - const alerts = Object.values(props.alerts).filter((alertItem) => Boolean(alertItem?.rawAlert)); - - if (alerts.length === 0) { - return null; - } - - const badges = []; - - if (inSetupMode) { - const button = ( - setShowPopover(true)} - > - {numberOfAlertsLabel(alerts.length)} - - ); - const panels = [ - { - id: 0, - title: i18n.translate('xpack.monitoring.alerts.badge.panelTitle', { - defaultMessage: 'Alerts', - }), - items: alerts.map(({ rawAlert }, index) => { - return { - name: {rawAlert.name}, - panel: index + 1, - }; - }), - }, - ...alerts.map((alertStatus, index) => { - return { - id: index + 1, - title: alertStatus.rawAlert.name, - width: 400, - content: , - }; - }), - ]; - - badges.push( - setShowPopover(null)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - ); - } else { - const byType = { - [AlertSeverity.Danger]: [] as AlertInPanel[], - [AlertSeverity.Warning]: [] as AlertInPanel[], - [AlertSeverity.Success]: [] as AlertInPanel[], - }; + 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 + ); - 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, - }); - } - } + React.useEffect(() => { + if (inSetupMode && showByNode) { + setShowByNode(false); } + }, [inSetupMode, showByNode]); - const typesToShow = [AlertSeverity.Danger, AlertSeverity.Warning]; - for (const type of typesToShow) { - const list = byType[type]; - if (list.length === 0) { - continue; - } + if (alertCount === 0) { + return null; + } - const button = ( - setShowPopover(type)} - > - {numberOfAlertsLabel(list.length)} - - ); + const groupByType = GROUP_BY_NODE; + const panels = showByNode + ? getAlertPanelsByNode(PANEL_TITLE, alerts, stateFilter) + : getAlertPanelsByCategory(PANEL_TITLE, inSetupMode, alerts, alertsContext, stateFilter); - 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: ( - - -

{getDateFromState(alertState)}

-
- {alert.rawAlert.name} -
- ), - panel: index + 1, - }; - }), + isSeparator: true as const, }, - ...list.map((alertStatus, index) => { - return { - id: index + 1, - title: getDateFromState(alertStatus.alertState), - width: 400, - content: , - }; - }), - ]; - - badges.push( - setShowPopover(null)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - ); - } + { + name: ( + setShowByNode(!showByNode)} + label={showByNode ? GROUP_BY_TYPE : groupByType} + /> + ), + }, + ] + ); } + const button = ( + setShowPopover(true)} + > + {numberOfAlertsLabel(alertCount)} + + ); + return ( - - {badges.map((badge, index) => ( - - {badge} - - ))} - + setShowPopover(null)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + ); }; diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx index 2f670ac221bf2..d3feb148cf986 100644 --- a/x-pack/plugins/monitoring/public/alerts/callout.tsx +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -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) => { - const { alerts, stateFilter = () => true } = props; + const { alerts } = props; + const inSetupMode = isInSetupMode(React.useContext(SetupModeContext)); + + if (inSetupMode) { + return null; + } - 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); - } - } + 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 ( - - -
    - {list.map((state, index) => { - const nextStepsUi = - state.ui.message.nextSteps && state.ui.message.nextSteps.length ? ( -
      - {state.ui.message.nextSteps.map( - (step: AlertMessage, nextStepIndex: number) => ( -
    • {replaceTokens(step)}
    • - ) - )} -
    - ) : null; + if (list.length === 0) { + return null; + } - return ( -
  • - {replaceTokens(state.ui.message)} - {nextStepsUi} -
  • - ); - })} -
-
- -
- ); - } + const accordions = list.map((status, index) => { + const buttonContent = ( +
+ + + + + + + + {replaceTokens(status.state.state.ui.message)} + + + +
+ ); + + const accordion = ( + + + {(status.state.state.ui.message.nextSteps || []).map((step: AlertMessage) => { + return {}} label={replaceTokens(step)} />; + })} + } + /> + + + ); + + const spacer = index !== list.length - 1 ? : null; + return ( +
+ {accordion} + {spacer} +
+ ); }); - return {callouts}; + + return ( + + {accordions} + + + ); }; diff --git a/x-pack/plugins/monitoring/public/alerts/configuration.tsx b/x-pack/plugins/monitoring/public/alerts/configuration.tsx new file mode 100644 index 0000000000000..c570e2c840f01 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/configuration.tsx @@ -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) => { + 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 ( + + + + { + setShowFlyout(true); + hideBottomBar(); + }} + > + {i18n.translate('xpack.monitoring.alerts.panel.editAlert', { + defaultMessage: `Edit alert`, + })} + + + + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(); + } else { + setIsEnabled(true); + await enableAlert(); + } + }} + label={ + + } + /> + + + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(); + } else { + setIsMuted(true); + await muteAlert(); + } + }} + label={ + + } + /> + + + {flyoutUi} + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/context.ts b/x-pack/plugins/monitoring/public/alerts/context.ts new file mode 100644 index 0000000000000..1017a4ade6c73 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/context.ts @@ -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, +}); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_category.test.tsx.snap b/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_category.test.tsx.snap new file mode 100644 index 0000000000000..75637b5bfd6c2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_category.test.tsx.snap @@ -0,0 +1,1287 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertPanelsByCategory non setup mode should allow for state filtering 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + + Resource utilization + ( + 1 + ) + + , + "panel": 1, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_cpu_usage_label + ( + 1 + ) + , + "panel": 2, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 3, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_cpu_usage_label", + }, + Object { + "content": , + "id": 3, + "title": "monitoring_alert_cpu_usage_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory non setup mode should not show any alert if none are firing 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [], + "title": "Alerts", + }, +] +`; + +exports[`getAlertPanelsByCategory non setup mode should properly group for alerts in a single category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + + Resource utilization + ( + 2 + ) + + , + "panel": 1, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_jvm_memory_usage_label + ( + 2 + ) + , + "panel": 2, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + + + triggered:1 + + + + es_name_1 + + , + "panel": 3, + }, + Object { + "isSeparator": true, + }, + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 4, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_jvm_memory_usage_label", + }, + Object { + "content": , + "id": 3, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory non setup mode should properly group for alerts in each category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + + Cluster health + ( + 2 + ) + + , + "panel": 1, + }, + Object { + "name": + + Resource utilization + ( + 1 + ) + + , + "panel": 2, + }, + Object { + "name": + + Errors and exceptions + ( + 2 + ) + + , + "panel": 3, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_nodes_changed_label + ( + 2 + ) + , + "panel": 4, + }, + ], + "title": "Cluster health", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + monitoring_alert_disk_usage_label + ( + 1 + ) + , + "panel": 5, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 3, + "items": Array [ + Object { + "name": + monitoring_alert_license_expiration_label + ( + 2 + ) + , + "panel": 6, + }, + ], + "title": "Errors and exceptions", + }, + Object { + "id": 4, + "items": Array [ + Object { + "name": + + + triggered:1 + + + + es_name_1 + + , + "panel": 7, + }, + Object { + "isSeparator": true, + }, + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 8, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_nodes_changed_label", + }, + Object { + "id": 5, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 9, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_disk_usage_label", + }, + Object { + "id": 6, + "items": Array [ + Object { + "name": + + + triggered:1 + + + + es_name_1 + + , + "panel": 10, + }, + Object { + "isSeparator": true, + }, + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 11, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_license_expiration_label", + }, + Object { + "content": , + "id": 7, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 8, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 9, + "title": "monitoring_alert_disk_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 10, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, + Object { + "content": , + "id": 11, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory setup mode should properly group for alerts in a single category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + Resource utilization + , + "panel": 1, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_jvm_memory_usage_label + , + "panel": 2, + }, + ], + "title": "Resource utilization", + }, + Object { + "content": , + "id": 2, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory setup mode should properly group for alerts in each category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + Cluster health + , + "panel": 1, + }, + Object { + "name": + Resource utilization + , + "panel": 2, + }, + Object { + "name": + Errors and exceptions + , + "panel": 3, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_nodes_changed_label + , + "panel": 4, + }, + ], + "title": "Cluster health", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + monitoring_alert_disk_usage_label + , + "panel": 5, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 3, + "items": Array [ + Object { + "name": + monitoring_alert_license_expiration_label + , + "panel": 6, + }, + ], + "title": "Errors and exceptions", + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 5, + "title": "monitoring_alert_disk_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 6, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory setup mode should still show alerts if none are firing 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + Cluster health + , + "panel": 1, + }, + Object { + "name": + Resource utilization + , + "panel": 2, + }, + Object { + "name": + Errors and exceptions + , + "panel": 3, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_logstash_version_mismatch_label + , + "panel": 4, + }, + ], + "title": "Cluster health", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + monitoring_alert_cpu_usage_label + , + "panel": 5, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 3, + "items": Array [ + Object { + "name": + monitoring_alert_thread_pool_write_rejections_label + , + "panel": 6, + }, + ], + "title": "Errors and exceptions", + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_logstash_version_mismatch_label", + "width": 400, + }, + Object { + "content": , + "id": 5, + "title": "monitoring_alert_cpu_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 6, + "title": "monitoring_alert_thread_pool_write_rejections_label", + "width": 400, + }, +] +`; diff --git a/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_node.test.tsx.snap b/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_node.test.tsx.snap new file mode 100644 index 0000000000000..e9e89112a9758 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_node.test.tsx.snap @@ -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": + es_name_0 + ( + 1 + ) + , + "panel": 1, + }, + Object { + "name": + es_name_1 + ( + 1 + ) + , + "panel": 2, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + monitoring_alert_jvm_memory_usage_label + + , + "panel": 3, + }, + ], + "title": "es_name_0", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + monitoring_alert_jvm_memory_usage_label + + , + "panel": 4, + }, + ], + "title": "es_name_1", + }, + Object { + "content": , + "id": 3, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, + Object { + "content": , + "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": + es_name_0 + ( + 3 + ) + , + "panel": 1, + }, + Object { + "name": + es_name_1 + ( + 2 + ) + , + "panel": 2, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + monitoring_alert_nodes_changed_label + + , + "panel": 3, + }, + Object { + "name": + + + triggered:0 + + + + monitoring_alert_disk_usage_label + + , + "panel": 4, + }, + Object { + "name": + + + triggered:0 + + + + monitoring_alert_license_expiration_label + + , + "panel": 5, + }, + ], + "title": "es_name_0", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + monitoring_alert_nodes_changed_label + + , + "panel": 6, + }, + Object { + "name": + + + triggered:0 + + + + monitoring_alert_license_expiration_label + + , + "panel": 7, + }, + ], + "title": "es_name_1", + }, + Object { + "content": , + "id": 3, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_disk_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 5, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, + Object { + "content": , + "id": 6, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 7, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, +] +`; diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx new file mode 100644 index 0000000000000..16b20119c9607 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx @@ -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(); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx new file mode 100644 index 0000000000000..82a1a1f841a22 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx @@ -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 ? ( + {category.label} + ) : ( + + + {category.label} ({category.alertCount}) + + + ); + 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: {alertStatus.rawAlert.name}, + 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: , + }); + } + } + } 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 ? ( + {alertStatus.rawAlert.name} + ) : ( + + {alertStatus.rawAlert.name} ({filteredStates.length}) + + ); + 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: ( + + + + {getDateFromNow( + alertState.state.ui.triggeredMS, + Legacy.shims.uiSettings.get('dateFormat:tz') + )} + + + {alertState.state.nodeName} + + ), + 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: , + }); + } + } + } + } + + return panels; +} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx new file mode 100644 index 0000000000000..be6ccb1e0981b --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx @@ -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(); + }); +}); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx new file mode 100644 index 0000000000000..c48706f4edcb9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx @@ -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: ( + + {states[0].state.nodeName} ({states.length}) + + ), + 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: ( + + + + {getDateFromNow( + alertState.state.ui.triggeredMS, + Legacy.shims.uiSettings.get('dateFormat:tz') + )} + + + {alert.name} + + ), + 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: , + }); + } + } + return accum; + }, []), + ]; + + return panels; +} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.test.ts b/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.test.ts new file mode 100644 index 0000000000000..29981c3ed32fe --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.test.ts @@ -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); + }); +}); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.ts b/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.ts new file mode 100644 index 0000000000000..f5a20e0bae37e --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.ts @@ -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; +} diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx index b480e46215108..139010a3d2446 100644 --- a/x-pack/plugins/monitoring/public/alerts/panel.tsx +++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx @@ -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) => { - 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 = ( - - - - { - setShowFlyout(true); - hideBottomBar(); - }} - > - {i18n.translate('xpack.monitoring.alerts.panel.editAlert', { - defaultMessage: `Edit alert`, - })} - - - - { - if (isEnabled) { - setIsEnabled(false); - await disableAlert(); - } else { - setIsEnabled(true); - await enableAlert(); - } - }} - label={ - - } - /> - - - { - if (isMuted) { - setIsMuted(false); - await unmuteAlert(); - } else { - setIsMuted(true); - await muteAlert(); - } - }} - label={ - - } - /> - - - {flyoutUi} - - ); - if (inSetupMode || !alertState) { - return
{configurationUi}
; + return ( +
+ +
+ ); } const nextStepsUi = @@ -204,7 +57,9 @@ export const AlertPanel: React.FC = (props: Props) => { {nextStepsUi} -
{configurationUi}
+
+ +
); }; diff --git a/x-pack/plugins/monitoring/public/alerts/types.ts b/x-pack/plugins/monitoring/public/alerts/types.ts new file mode 100644 index 0000000000000..80918b5d8767a --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/types.ts @@ -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; +} + +export interface ContextMenuItem { + name: React.ReactElement; + panel?: number; + onClick?: () => void; +} + +export interface ContextMenuItemSeparator { + isSeparator: true; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js index c16f55b438c35..5f45f53adddee 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js @@ -18,7 +18,7 @@ import { NodeDetailStatus } from '../node_detail_status'; import { MonitoringTimeseriesContainer } from '../../chart'; 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, @@ -44,7 +44,7 @@ export const AdvancedNode = ({ nodeSummary, metrics, alerts, nodeId, ...props }) - state.nodeId === nodeId} /> + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 7cf7227f50202..111d036e84706 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -128,7 +128,6 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler defaultMessage: 'Alerts', }), field: 'alerts', - // width: '175px', sortable: true, render: (_field, node) => { return ( diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index 18c3a59d6b9da..f9c6cfb6024da 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -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 = ( - {!this._isDataInitialized ? ( - - ) : ( - component - )} + + {!this._isDataInitialized ? ( + + ) : ( + component + )} + ); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index 586261eecb250..2f0aa67e4e16b 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -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( - ( + + {flyoutComponent} + + {bottomBarComponent} + + )} /> ); } diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 1d8de2bab015c..4f989b37421ef 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -42,6 +42,7 @@ import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; interface LegacyOptions { watchName: string; + nodeNameLabel: string; changeDataValues?: Partial; } @@ -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; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index a4e9f56109698..fbf81bc3513f6 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -119,6 +119,7 @@ describe('ClusterHealthAlert', () => { { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs: undefined, + nodeName: 'Elasticsearch cluster alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 3b375654548d8..d7c33ae85cc9d 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -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: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 7bdef1ee2c2c4..c587ca52f26ab 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -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: [ diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts index 133fe261d0791..4a736f27320eb 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts @@ -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: [ diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 46fdd1fa98563..ed39cbea3381c 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -124,6 +124,7 @@ describe('ElasticsearchVersionMismatchAlert', () => { { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs: undefined, + nodeName: 'Elasticsearch node alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index 88b5b708d41f3..9002fb54fcf23 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -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', diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 2367b53330ec5..96464e16f8c72 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -126,6 +126,7 @@ describe('KibanaVersionMismatchAlert', () => { { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs: undefined, + nodeName: 'Kibana instance alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index c9e5786484899..0d394b8f45ecc 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -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', diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index f7a3d321b960b..c64b6e4b92984 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -130,6 +130,7 @@ describe('LicenseExpirationAlert', () => { { cluster: { clusterUuid, clusterName }, ccs: undefined, + nodeName: 'Elasticsearch cluster alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index 80479023a3a60..9dacba1dfe0ec 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -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: [ diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index a021a0e6fe179..dd23cfc76dc6d 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -125,6 +125,7 @@ describe('LogstashVersionMismatchAlert', () => { { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs: undefined, + nodeName: 'Logstash node alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 98640fb6e183a..4eae3cd12eed4 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -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', diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts index 860cd41f9057d..d5ea291aa52ed 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts @@ -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: [ diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts index 12bb27ce132d0..6ba4333309f00 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts @@ -128,9 +128,9 @@ describe('MissingMonitoringDataAlert', () => { { ccs: undefined, cluster: { clusterUuid, clusterName }, - gapDuration, - nodeName, nodeId, + nodeName, + gapDuration, ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts index 1c93ff4a28719..b4c8a667a1ce8 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -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 ) { diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 99be91dc293cb..e6017e799cba8 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -137,6 +137,7 @@ describe('NodesChangedAlert', () => { { cluster: { clusterUuid, clusterName }, ccs: undefined, + nodeName: 'Elasticsearch nodes alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 47d5c5ac2c241..09c12d345d930 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -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: [ diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts index 2d8ccabaac853..1e539c52eeedc 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts @@ -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'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts index a3743a8ff206f..5055017051816 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts @@ -47,6 +47,7 @@ describe('fetchLegacyAlerts', () => { message, metadata, nodes, + nodeName: '', prefix, }, ]); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts index fbf7608a737ba..0ea37b4ac4daa 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts @@ -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; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap index 947db23377e4c..38748af753d31 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap @@ -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", }, ] `; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js index 3766845d39b4f..ac4fcea6150a0 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js @@ -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) { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js index 62cf138c99506..3f82e8ec3e646 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js @@ -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, diff --git a/x-pack/plugins/monitoring/server/lib/pagination/filter.js b/x-pack/plugins/monitoring/server/lib/pagination/filter.js index 7592f2bb3afde..e906081a8eb5a 100644 --- a/x-pack/plugins/monitoring/server/lib/pagination/filter.js +++ b/x-pack/plugins/monitoring/server/lib/pagination/filter.js @@ -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; } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 86b006ce7cba3..afde4f99a08f0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13577,8 +13577,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": "クラスターの正常性。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 24da57edffff7..6d2082326b5c7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13594,8 +13594,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": "集群的运行状况。", diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json index b9c0a32beba58..6833e1d3d9bc4 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json @@ -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 diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json index 0a18664faf445..ef9fb46909715 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json @@ -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" diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json index d9c04838fab10..f2e129cca32d1 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json @@ -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"