From a61f4d4cbfd08dfe2502cebd059464bf3df75707 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 1 Oct 2020 12:28:34 -0400 Subject: [PATCH] [Monitoring] Missing data alert (#78208) * WIP for alert * Surface alert most places * Fix up alert placement * Fix tests * Type fix * Update copy * Add alert presence to APM in the UI * Fetch data a little differently * We don't need moment * Add tests * PR feedback * Update copy * Fix up bug around grabbing old data * PR feedback * PR feedback * Fix tests --- x-pack/plugins/monitoring/common/constants.ts | 2 + x-pack/plugins/monitoring/common/types.ts | 6 +- .../monitoring/public/alerts/badge.tsx | 15 +- .../monitoring/public/alerts/callout.tsx | 11 +- .../public/alerts/filter_alert_states.ts | 23 + .../expression.tsx | 61 +++ .../missing_monitoring_data_alert/index.ts | 7 + .../missing_monitoring_data_alert.tsx | 28 + .../validation.tsx | 35 ++ .../monitoring/public/alerts/panel.tsx | 10 +- .../monitoring/public/alerts/status.tsx | 15 +- .../components/apm/instance/instance.js | 14 +- .../public/components/apm/instance/status.js | 3 +- .../components/apm/instances/instances.js | 32 +- .../public/components/apm/instances/status.js | 3 +- .../public/components/apm/overview/index.js | 4 +- .../public/components/beats/beat/beat.js | 18 +- .../components/beats/listing/listing.js | 29 +- .../components/beats/overview/overview.js | 3 +- .../public/components/beats/stats.js | 4 +- .../components/cluster/overview/apm_panel.js | 29 +- .../cluster/overview/beats_panel.js | 25 +- .../cluster/overview/elasticsearch_panel.js | 2 + .../components/cluster/overview/index.js | 54 +- .../cluster/overview/kibana_panel.js | 8 +- .../cluster/overview/logstash_panel.js | 3 +- .../components/elasticsearch/node/node.js | 15 +- .../components/elasticsearch/nodes/nodes.js | 10 +- .../components/kibana/instances/instances.js | 13 +- .../components/logstash/listing/listing.js | 13 +- x-pack/plugins/monitoring/public/plugin.ts | 2 + .../public/views/apm/instance/index.js | 18 +- .../public/views/apm/instances/index.js | 18 +- .../public/views/apm/overview/index.js | 24 +- .../public/views/beats/beat/index.js | 18 +- .../public/views/beats/listing/index.js | 18 +- .../public/views/beats/overview/index.js | 24 +- .../elasticsearch/node/advanced/index.js | 3 +- .../public/views/elasticsearch/node/index.js | 3 +- .../public/views/elasticsearch/nodes/index.js | 8 +- .../public/views/kibana/instance/index.js | 24 +- .../public/views/kibana/instances/index.js | 8 +- .../views/logstash/node/advanced/index.js | 28 +- .../public/views/logstash/node/index.js | 24 +- .../public/views/logstash/nodes/index.js | 8 +- .../server/alerts/alerts_factory.test.ts | 2 +- .../server/alerts/alerts_factory.ts | 3 + .../monitoring/server/alerts/base_alert.ts | 41 +- .../server/alerts/cpu_usage_alert.ts | 4 +- .../plugins/monitoring/server/alerts/index.ts | 1 + .../missing_monitoring_data_alert.test.ts | 459 ++++++++++++++++ .../alerts/missing_monitoring_data_alert.ts | 504 ++++++++++++++++++ .../monitoring/server/alerts/types.d.ts | 16 + .../server/lib/alerts/fetch_clusters.ts | 19 +- .../fetch_missing_monitoring_data.test.ts | 249 +++++++++ .../alerts/fetch_missing_monitoring_data.ts | 275 ++++++++++ .../get_listing_link_for_stack_product.ts | 28 + .../lib/alerts/get_stack_product_label.ts | 17 + .../get_type_label_for_stack_product.ts | 51 ++ 59 files changed, 2303 insertions(+), 89 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx create mode 100644 x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_listing_link_for_stack_product.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_stack_product_label.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_type_label_for_stack_product.ts diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 6eb0d6e93d58a..860f6439f3fdf 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -236,6 +236,7 @@ export const ALERT_NODES_CHANGED = `${ALERT_PREFIX}alert_nodes_changed`; export const ALERT_ELASTICSEARCH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_elasticsearch_version_mismatch`; export const ALERT_KIBANA_VERSION_MISMATCH = `${ALERT_PREFIX}alert_kibana_version_mismatch`; export const ALERT_LOGSTASH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_logstash_version_mismatch`; +export const ALERT_MISSING_MONITORING_DATA = `${ALERT_PREFIX}alert_missing_monitoring_data`; /** * A listing of all alert types @@ -249,6 +250,7 @@ export const ALERTS = [ ALERT_ELASTICSEARCH_VERSION_MISMATCH, ALERT_KIBANA_VERSION_MISMATCH, ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, ]; /** diff --git a/x-pack/plugins/monitoring/common/types.ts b/x-pack/plugins/monitoring/common/types.ts index 4216a046fef9f..825d2e454b3bb 100644 --- a/x-pack/plugins/monitoring/common/types.ts +++ b/x-pack/plugins/monitoring/common/types.ts @@ -31,10 +31,14 @@ export interface CommonAlertFilter { nodeUuid?: string; } -export interface CommonAlertCpuUsageFilter extends CommonAlertFilter { +export interface CommonAlertNodeUuidFilter extends CommonAlertFilter { nodeUuid: string; } +export interface CommonAlertStackProductFilter extends CommonAlertFilter { + stackProduct: string; +} + export interface CommonAlertParamDetail { label: string; type: AlertParamType; diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index 1d67eebb1705c..cf75939b14efc 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -18,7 +18,7 @@ import { CommonAlertStatus, CommonAlertState } from '../../common/types'; import { AlertSeverity } from '../../common/enums'; // @ts-ignore import { formatDateTimeLocal } from '../../common/formatting'; -import { AlertState } from '../../server/alerts/types'; +import { AlertMessage, AlertState } from '../../server/alerts/types'; import { AlertPanel } from './panel'; import { Legacy } from '../legacy_shims'; import { isInSetupMode } from '../lib/setup_mode'; @@ -39,9 +39,10 @@ interface AlertInPanel { interface Props { alerts: { [alertTypeId: string]: CommonAlertStatus }; stateFilter: (state: AlertState) => boolean; + nextStepsFilter: (nextStep: AlertMessage) => boolean; } export const AlertsBadge: React.FC = (props: Props) => { - const { stateFilter = () => true } = props; + const { stateFilter = () => true, nextStepsFilter = () => true } = props; const [showPopover, setShowPopover] = React.useState(null); const inSetupMode = isInSetupMode(); const alerts = Object.values(props.alerts).filter(Boolean); @@ -80,7 +81,7 @@ export const AlertsBadge: React.FC = (props: Props) => { id: index + 1, title: alertStatus.alert.label, width: 400, - content: , + content: , }; }), ]; @@ -158,7 +159,13 @@ export const AlertsBadge: React.FC = (props: Props) => { id: index + 1, title: getDateFromState(alertStatus.alertState), width: 400, - content: , + content: ( + + ), }; }), ]; diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx index cad98dd1e6aec..1ddd41c268456 100644 --- a/x-pack/plugins/monitoring/public/alerts/callout.tsx +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -32,9 +32,10 @@ const TYPES = [ interface Props { alerts: { [alertTypeId: string]: CommonAlertStatus }; stateFilter: (state: AlertState) => boolean; + nextStepsFilter: (nextStep: AlertMessage) => boolean; } export const AlertsCallout: React.FC = (props: Props) => { - const { alerts, stateFilter = () => true } = props; + const { alerts, stateFilter = () => true, nextStepsFilter = () => true } = props; const callouts = TYPES.map((type) => { const list = []; @@ -56,11 +57,11 @@ export const AlertsCallout: React.FC = (props: Props) => { const nextStepsUi = state.ui.message.nextSteps && state.ui.message.nextSteps.length ? (
    - {state.ui.message.nextSteps.map( - (step: AlertMessage, nextStepIndex: number) => ( + {state.ui.message.nextSteps + .filter(nextStepsFilter) + .map((step: AlertMessage, nextStepIndex: number) => (
  • {replaceTokens(step)}
  • - ) - )} + ))}
) : null; diff --git a/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts b/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts new file mode 100644 index 0000000000000..63714a6921e3f --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts @@ -0,0 +1,23 @@ +/* + * 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, CommonAlertStatus } from '../../common/types'; + +export function filterAlertStates( + alerts: { [type: string]: CommonAlertStatus }, + filter: (type: string, state: CommonAlertState) => boolean +) { + return Object.keys(alerts).reduce( + (accum: { [type: string]: CommonAlertStatus }, type: string) => { + accum[type] = { + ...alerts[type], + states: alerts[type].states.filter((state) => filter(type, state)), + }; + return accum; + }, + {} + ); +} diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx new file mode 100644 index 0000000000000..7dc6155de529e --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx @@ -0,0 +1,61 @@ +/* + * 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 { EuiForm, EuiSpacer } from '@elastic/eui'; +import { CommonAlertParamDetails } from '../../../common/types'; +import { AlertParamDuration } from '../flyout_expressions/alert_param_duration'; +import { AlertParamType } from '../../../common/enums'; +import { AlertParamPercentage } from '../flyout_expressions/alert_param_percentage'; + +export interface Props { + alertParams: { [property: string]: any }; + setAlertParams: (property: string, value: any) => void; + setAlertProperty: (property: string, value: any) => void; + errors: { [key: string]: string[] }; + paramDetails: CommonAlertParamDetails; +} + +export const Expression: React.FC = (props) => { + const { alertParams, paramDetails, setAlertParams, errors } = props; + + const alertParamsUi = Object.keys(alertParams).map((alertParamName) => { + const details = paramDetails[alertParamName]; + const value = alertParams[alertParamName]; + + switch (details.type) { + case AlertParamType.Duration: + return ( + + ); + case AlertParamType.Percentage: + return ( + + ); + } + }); + + return ( + + {alertParamsUi} + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts new file mode 100644 index 0000000000000..5169601c0e6e3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { createMissingMonitoringDataAlertType } from './missing_monitoring_data_alert'; diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx new file mode 100644 index 0000000000000..bcea98592adb2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx @@ -0,0 +1,28 @@ +/* + * 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'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { validate } from './validation'; +import { ALERT_MISSING_MONITORING_DATA } from '../../../common/constants'; +import { Expression } from './expression'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MissingMonitoringDataAlert } from '../../../server/alerts'; + +export function createMissingMonitoringDataAlertType(): AlertTypeModel { + const alert = new MissingMonitoringDataAlert(); + return { + id: ALERT_MISSING_MONITORING_DATA, + name: alert.label, + iconClass: 'bell', + alertParamsExpression: (props: any) => ( + + ), + validate, + defaultActionMessage: '{{context.internalFullMessage}}', + requiresAppContext: true, + }; +} diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx new file mode 100644 index 0000000000000..fe84de9bd00ea --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx @@ -0,0 +1,35 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../triggers_actions_ui/public/types'; + +export function validate(opts: any): ValidationResult { + const validationResult = { errors: {} }; + + const errors: { [key: string]: string[] } = { + duration: [], + limit: [], + }; + if (!opts.duration) { + errors.duration.push( + i18n.translate('xpack.monitoring.alerts.missingData.validation.duration', { + defaultMessage: 'A valid duration is required.', + }) + ); + } + if (!opts.limit) { + errors.limit.push( + i18n.translate('xpack.monitoring.alerts.missingData.validation.limit', { + defaultMessage: 'A valid limit is required.', + }) + ); + } + + validationResult.errors = errors; + return validationResult; +} diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx index 91604acf115fa..ee605592e9408 100644 --- a/x-pack/plugins/monitoring/public/alerts/panel.tsx +++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx @@ -30,11 +30,13 @@ import { BASE_ALERT_API_PATH } from '../../../alerts/common'; interface Props { alert: CommonAlertStatus; alertState?: CommonAlertState; + nextStepsFilter: (nextStep: AlertMessage) => boolean; } export const AlertPanel: React.FC = (props: Props) => { const { alert: { alert }, alertState, + nextStepsFilter = () => true, } = props; const [showFlyout, setShowFlyout] = React.useState(false); const [isEnabled, setIsEnabled] = React.useState(alert.rawAlert.enabled); @@ -198,9 +200,11 @@ export const AlertPanel: React.FC = (props: Props) => { const nextStepsUi = alertState.state.ui.message.nextSteps && alertState.state.ui.message.nextSteps.length ? ( - {alertState.state.ui.message.nextSteps.map((step: AlertMessage, index: number) => ( - - ))} + {alertState.state.ui.message.nextSteps + .filter(nextStepsFilter) + .map((step: AlertMessage, index: number) => ( + + ))} ) : null; diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx index 0407ddfecf5e9..dba66df0e4474 100644 --- a/x-pack/plugins/monitoring/public/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/alerts/status.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { CommonAlertStatus } from '../../common/types'; import { AlertSeverity } from '../../common/enums'; -import { AlertState } from '../../server/alerts/types'; +import { AlertMessage, AlertState } from '../../server/alerts/types'; import { AlertsBadge } from './badge'; import { isInSetupMode } from '../lib/setup_mode'; @@ -18,9 +18,16 @@ interface Props { showBadge: boolean; showOnlyCount: boolean; stateFilter: (state: AlertState) => boolean; + nextStepsFilter: (nextStep: AlertMessage) => boolean; } export const AlertsStatus: React.FC = (props: Props) => { - const { alerts, showBadge = false, showOnlyCount = false, stateFilter = () => true } = props; + const { + alerts, + showBadge = false, + showOnlyCount = false, + stateFilter = () => true, + nextStepsFilter = () => true, + } = props; const inSetupMode = isInSetupMode(); if (!alerts) { @@ -71,7 +78,9 @@ export const AlertsStatus: React.FC = (props: Props) => { } if (showBadge || inSetupMode) { - return ; + return ( + + ); } const severity = atLeastOneDanger ? AlertSeverity.Danger : AlertSeverity.Warning; diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js index eec24e741ac41..8934bbc41f5f6 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js +++ b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js @@ -18,8 +18,9 @@ import { } from '@elastic/eui'; import { Status } from './status'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsCallout } from '../../../alerts/callout'; -export function ApmServerInstance({ summary, metrics, ...props }) { +export function ApmServerInstance({ summary, metrics, alerts, ...props }) { const seriesToShow = [ metrics.apm_requests, metrics.apm_responses_valid, @@ -58,9 +59,18 @@ export function ApmServerInstance({ summary, metrics, ...props }) { - + + { + if (nextStep.text.includes('APM servers')) { + return false; + } + return true; + }} + /> {charts} diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/status.js b/x-pack/plugins/monitoring/public/components/apm/instance/status.js index 9b78db54a2aa7..02a15d214ab9b 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instance/status.js +++ b/x-pack/plugins/monitoring/public/components/apm/instance/status.js @@ -14,7 +14,7 @@ import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -export function Status({ stats }) { +export function Status({ alerts, stats }) { const { name, output, version, uptime, timeOfLastEvent } = stats; const metrics = [ @@ -78,6 +78,7 @@ export function Status({ stats }) { return ( diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index e05ba1878caed..4932fb9068fcc 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -28,8 +28,9 @@ import { SetupModeBadge } from '../../setup_mode/badge'; import { FormattedMessage } from '@kbn/i18n/react'; import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; +import { AlertsStatus } from '../../../alerts/status'; -function getColumns(setupMode) { +function getColumns(alerts, setupMode) { return [ { name: i18n.translate('xpack.monitoring.apm.instances.nameTitle', { @@ -71,6 +72,29 @@ function getColumns(setupMode) { ); }, }, + { + name: i18n.translate('xpack.monitoring.beats.instances.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'alerts', + width: '175px', + sortable: true, + render: (_field, beat) => { + return ( + state.stackProductUuid === beat.uuid} + nextStepsFilter={(nextStep) => { + if (nextStep.text.includes('APM servers')) { + return false; + } + return true; + }} + /> + ); + }, + }, { name: i18n.translate('xpack.monitoring.apm.instances.outputEnabledTitle', { defaultMessage: 'Output Enabled', @@ -127,7 +151,7 @@ function getColumns(setupMode) { ]; } -export function ApmServerInstances({ apms, setupMode }) { +export function ApmServerInstances({ apms, alerts, setupMode }) { const { pagination, sorting, onTableChange, data } = apms; let setupModeCallout = null; @@ -157,7 +181,7 @@ export function ApmServerInstances({ apms, setupMode }) { - + @@ -165,7 +189,7 @@ export function ApmServerInstances({ apms, setupMode }) { diff --git a/x-pack/plugins/monitoring/public/components/apm/overview/index.js b/x-pack/plugins/monitoring/public/components/apm/overview/index.js index 35efa6ac67ea8..b10592c2a49d2 100644 --- a/x-pack/plugins/monitoring/public/components/apm/overview/index.js +++ b/x-pack/plugins/monitoring/public/components/apm/overview/index.js @@ -19,7 +19,7 @@ import { import { Status } from '../instances/status'; import { FormattedMessage } from '@kbn/i18n/react'; -export function ApmOverview({ stats, metrics, ...props }) { +export function ApmOverview({ stats, metrics, alerts, ...props }) { const seriesToShow = [ metrics.apm_responses_valid, metrics.apm_responses_errors, @@ -54,7 +54,7 @@ export function ApmOverview({ stats, metrics, ...props }) { - + diff --git a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js index f489271659bfe..470cdf588ca3d 100644 --- a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js +++ b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js @@ -20,8 +20,9 @@ import { import { i18n } from '@kbn/i18n'; import { SummaryStatus } from '../../summary_status'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsCallout } from '../../../alerts/callout'; -export function Beat({ summary, metrics, ...props }) { +export function Beat({ summary, metrics, alerts, ...props }) { const metricsToShow = [ metrics.beat_event_rates, metrics.beat_fail_rates, @@ -134,13 +135,26 @@ export function Beat({ summary, metrics, ...props }) { - + + { + if (nextStep.text.includes('Beat instances')) { + return false; + } + return true; + }} + />

diff --git a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js index 60a35e00a4c63..dc65cd38aac53 100644 --- a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js @@ -26,10 +26,12 @@ import { SetupModeBadge } from '../../setup_mode/badge'; import { FormattedMessage } from '@kbn/i18n/react'; import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; +import { AlertsStatus } from '../../../alerts/status'; export class Listing extends PureComponent { getColumns() { const setupMode = this.props.setupMode; + const alerts = this.props.alerts; return [ { @@ -72,6 +74,29 @@ export class Listing extends PureComponent { ); }, }, + { + name: i18n.translate('xpack.monitoring.beats.instances.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'alerts', + width: '175px', + sortable: true, + render: (_field, beat) => { + return ( + state.stackProductUuid === beat.uuid} + nextStepsFilter={(nextStep) => { + if (nextStep.text.includes('Beat instances')) { + return false; + } + return true; + }} + /> + ); + }, + }, { name: i18n.translate('xpack.monitoring.beats.instances.typeTitle', { defaultMessage: 'Type', @@ -122,7 +147,7 @@ export class Listing extends PureComponent { } render() { - const { stats, data, sorting, pagination, onTableChange, setupMode } = this.props; + const { stats, data, sorting, pagination, onTableChange, setupMode, alerts } = this.props; let setupModeCallOut = null; if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { @@ -155,7 +180,7 @@ export class Listing extends PureComponent {

- + diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js index 897f017f44f41..aa09da31504d3 100644 --- a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js @@ -84,6 +84,7 @@ export function BeatsOverview({ latestVersions, stats, metrics, + alerts, ...props }) { const seriesToShow = [ @@ -113,7 +114,7 @@ export function BeatsOverview({ - + {renderLatestActive(latestActive, latestTypes, latestVersions)} diff --git a/x-pack/plugins/monitoring/public/components/beats/stats.js b/x-pack/plugins/monitoring/public/components/beats/stats.js index 89ec10bbaf1bb..c1f06af14a453 100644 --- a/x-pack/plugins/monitoring/public/components/beats/stats.js +++ b/x-pack/plugins/monitoring/public/components/beats/stats.js @@ -9,7 +9,7 @@ import { formatMetric } from '../../lib/format_number'; import { SummaryStatus } from '../summary_status'; import { i18n } from '@kbn/i18n'; -export function Stats({ stats }) { +export function Stats({ stats, alerts }) { const { total, types, @@ -51,5 +51,5 @@ export function Stats({ stats }) { 'data-test-subj': 'bytesSent', }); - return ; + return ; } diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js index 4bf07710393ea..d0d5a36c3829b 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js @@ -24,14 +24,22 @@ import { EuiFlexGroup, } from '@elastic/eui'; import { formatTimestampToDuration } from '../../../../common'; -import { CALCULATE_DURATION_SINCE, APM_SYSTEM_ID } from '../../../../common/constants'; +import { + CALCULATE_DURATION_SINCE, + APM_SYSTEM_ID, + ALERT_MISSING_MONITORING_DATA, +} from '../../../../common/constants'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; +import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { AlertsBadge } from '../../../alerts/badge'; + +const SERVERS_PANEL_ALERTS = [ALERT_MISSING_MONITORING_DATA]; export function ApmPanel(props) { - const { setupMode } = props; + const { setupMode, alerts } = props; const apmsTotal = get(props, 'apms.total') || 0; // Do not show if we are not in setup mode if (apmsTotal === 0 && !setupMode.enabled) { @@ -50,6 +58,16 @@ export function ApmPanel(props) { /> ) : null; + let apmServersAlertStatus = null; + if (shouldShowAlertBadge(alerts, SERVERS_PANEL_ALERTS)) { + const alertsList = SERVERS_PANEL_ALERTS.map((alertType) => alerts[alertType]); + apmServersAlertStatus = ( + + + + ); + } + return ( - {setupModeMetricbeatMigrationTooltip} + + + {setupModeMetricbeatMigrationTooltip} + {apmServersAlertStatus} + + diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js index 3d9b455787a96..628f57a0ffde3 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -23,13 +23,17 @@ import { ClusterItemContainer, DisabledIfNoDataAndInSetupModeLink } from './help import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; -import { BEATS_SYSTEM_ID } from '../../../../common/constants'; +import { ALERT_MISSING_MONITORING_DATA, BEATS_SYSTEM_ID } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; +import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { AlertsBadge } from '../../../alerts/badge'; + +const BEATS_PANEL_ALERTS = [ALERT_MISSING_MONITORING_DATA]; export function BeatsPanel(props) { - const { setupMode } = props; + const { setupMode, alerts } = props; const beatsTotal = get(props, 'beats.total') || 0; // Do not show if we are not in setup mode if (beatsTotal === 0 && !setupMode.enabled) { @@ -47,6 +51,16 @@ export function BeatsPanel(props) { /> ) : null; + let beatsAlertsStatus = null; + if (shouldShowAlertBadge(alerts, BEATS_PANEL_ALERTS)) { + const alertsList = BEATS_PANEL_ALERTS.map((alertType) => alerts[alertType]); + beatsAlertsStatus = ( + + + + ); + } + const beatTypes = props.beats.types.map((beat, index) => { return [ - {setupModeMetricbeatMigrationTooltip} + + + {setupModeMetricbeatMigrationTooltip} + {beatsAlertsStatus} + + {beatTypes} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 61a24f31ca89a..667f64458b8f9 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -43,6 +43,7 @@ import { ALERT_DISK_USAGE, ALERT_NODES_CHANGED, ALERT_ELASTICSEARCH_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, } from '../../../../common/constants'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; @@ -161,6 +162,7 @@ const NODES_PANEL_ALERTS = [ ALERT_DISK_USAGE, ALERT_NODES_CHANGED, ALERT_ELASTICSEARCH_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, ]; export function ElasticsearchPanel(props) { diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js index 66701c1dfd95a..aebd1cee5f0be 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js @@ -12,7 +12,16 @@ import { BeatsPanel } from './beats_panel'; import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { ApmPanel } from './apm_panel'; import { FormattedMessage } from '@kbn/i18n/react'; -import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; +import { + STANDALONE_CLUSTER_CLUSTER_UUID, + ALERT_MISSING_MONITORING_DATA, + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + LOGSTASH_SYSTEM_ID, + BEATS_SYSTEM_ID, + APM_SYSTEM_ID, +} from '../../../../common/constants'; +import { filterAlertStates } from '../../../alerts/filter_alert_states'; export function Overview(props) { const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID; @@ -37,12 +46,22 @@ export function Overview(props) { license={props.cluster.license} setupMode={props.setupMode} showLicenseExpiration={props.showLicenseExpiration} - alerts={props.alerts} + alerts={filterAlertStates(props.alerts, (type, { state }) => { + if (type === ALERT_MISSING_MONITORING_DATA) { + return state.stackProduct === ELASTICSEARCH_SYSTEM_ID; + } + return true; + })} /> { + if (type === ALERT_MISSING_MONITORING_DATA) { + return state.stackProduct === KIBANA_SYSTEM_ID; + } + return true; + })} /> ) : null} @@ -50,12 +69,35 @@ export function Overview(props) { { + if (type === ALERT_MISSING_MONITORING_DATA) { + return state.stackProduct === LOGSTASH_SYSTEM_ID; + } + return true; + })} /> - + { + if (type === ALERT_MISSING_MONITORING_DATA) { + return state.stackProduct === BEATS_SYSTEM_ID; + } + return true; + })} + /> - + { + if (type === ALERT_MISSING_MONITORING_DATA) { + return state.stackProduct === APM_SYSTEM_ID; + } + return true; + })} + />
); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index 7df0a3ca7138e..1f20684bd97d7 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -28,14 +28,18 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; -import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; +import { + KIBANA_SYSTEM_ID, + ALERT_KIBANA_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, +} from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; -const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH]; +const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA]; export function KibanaPanel(props) { const setupMode = props.setupMode; diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index 2c61438dca17c..7c0e04ab5d615 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -15,6 +15,7 @@ import { LOGSTASH, LOGSTASH_SYSTEM_ID, ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, } from '../../../../common/constants'; import { @@ -40,7 +41,7 @@ import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badg import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; -const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH]; +const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA]; export function LogstashPanel(props) { const { setupMode } = props; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js index ac1a5212a8d26..47e30b71e03d0 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js @@ -73,11 +73,22 @@ export const Node = ({ state.nodeId === nodeId} + alertsStateFilter={(state) => + state.nodeId === nodeId || state.stackProductUuid === nodeId + } /> - state.nodeId === nodeId} /> + state.nodeId === nodeId || state.stackProductUuid === nodeId} + nextStepsFilter={(nextStep) => { + if (nextStep.text.includes('Elasticsearch nodes')) { + return false; + } + return true; + }} + /> {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 f088f7c0d348a..41d3a579db5a2 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -137,7 +137,15 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler state.nodeId === node.resolver} + stateFilter={(state) => + state.nodeId === node.resolver || state.stackProductUuid === node.resolver + } + nextStepsFilter={(nextStep) => { + if (nextStep.text.includes('Elasticsearch nodes')) { + return false; + } + return true; + }} /> ); }, diff --git a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js index 10ad1634eda86..8095337dd3796 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js @@ -91,7 +91,18 @@ const getColumns = (setupMode, alerts) => { width: '175px', sortable: true, render: () => { - return ; + return ( + { + if (nextStep.text.includes('Kibana instances')) { + return false; + } + return true; + }} + /> + ); }, }, { diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js index 4a1137079ebb4..a5db433bbfe0a 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js @@ -84,7 +84,18 @@ export class Listing extends PureComponent { width: '175px', sortable: true, render: () => { - return ; + return ( + { + if (nextStep.text.includes('Logstash nodes')) { + return false; + } + return true; + }} + /> + ); }, }, { diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index a9c26ca7103a2..f4f66185346e8 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -23,6 +23,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { MonitoringStartPluginDependencies, MonitoringConfig } from './types'; import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; +import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; import { createLegacyAlertTypes } from './alerts/legacy_alert'; import { createDiskUsageAlertType } from './alerts/disk_usage_alert'; @@ -72,6 +73,7 @@ export class MonitoringPlugin } plugins.triggers_actions_ui.alertTypeRegistry.register(createCpuUsageAlertType()); + plugins.triggers_actions_ui.alertTypeRegistry.register(createMissingMonitoringDataAlertType()); plugins.triggers_actions_ui.alertTypeRegistry.register(createDiskUsageAlertType()); const legacyAlertTypes = createLegacyAlertTypes(); for (const legacyAlertType of legacyAlertTypes) { diff --git a/x-pack/plugins/monitoring/public/views/apm/instance/index.js b/x-pack/plugins/monitoring/public/views/apm/instance/index.js index 752128782194e..396d4651e0c5e 100644 --- a/x-pack/plugins/monitoring/public/views/apm/instance/index.js +++ b/x-pack/plugins/monitoring/public/views/apm/instance/index.js @@ -18,7 +18,11 @@ import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; import { MonitoringViewBaseController } from '../../base_controller'; import { ApmServerInstance } from '../../../components/apm/instance'; -import { CODE_PATH_APM } from '../../../../common/constants'; +import { + CODE_PATH_APM, + ALERT_MISSING_MONITORING_DATA, + APM_SYSTEM_ID, +} from '../../../../common/constants'; uiRoutes.when('/apm/instances/:uuid', { template, @@ -50,6 +54,17 @@ uiRoutes.when('/apm/instances/:uuid', { reactNodeId: 'apmInstanceReact', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: APM_SYSTEM_ID, + }, + ], + }, + }, }); $scope.$watch( @@ -69,6 +84,7 @@ uiRoutes.when('/apm/instances/:uuid', { summary={data.apmSummary || {}} metrics={data.metrics || {}} onBrush={this.onBrush} + alerts={this.alerts} zoomInfo={this.zoomInfo} /> ); diff --git a/x-pack/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/plugins/monitoring/public/views/apm/instances/index.js index 1f5b089ea748e..75f3ded89a595 100644 --- a/x-pack/plugins/monitoring/public/views/apm/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/apm/instances/index.js @@ -13,7 +13,11 @@ import template from './index.html'; import { ApmServerInstances } from '../../../components/apm/instances'; import { MonitoringViewBaseEuiTableController } from '../..'; import { SetupModeRenderer } from '../../../components/renderers'; -import { APM_SYSTEM_ID, CODE_PATH_APM } from '../../../../common/constants'; +import { + APM_SYSTEM_ID, + CODE_PATH_APM, + ALERT_MISSING_MONITORING_DATA, +} from '../../../../common/constants'; uiRoutes.when('/apm/instances', { template, @@ -47,6 +51,17 @@ uiRoutes.when('/apm/instances', { reactNodeId: 'apmInstancesReact', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: APM_SYSTEM_ID, + }, + ], + }, + }, }); this.scope = $scope; @@ -67,6 +82,7 @@ uiRoutes.when('/apm/instances', { {flyoutComponent} this.data, (data) => { this.renderReact( - + ); } ); diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/index.js b/x-pack/plugins/monitoring/public/views/beats/beat/index.js index 6cffae2479128..3e9e4e4b0373d 100644 --- a/x-pack/plugins/monitoring/public/views/beats/beat/index.js +++ b/x-pack/plugins/monitoring/public/views/beats/beat/index.js @@ -11,7 +11,11 @@ import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseController } from '../../'; import { getPageData } from './get_page_data'; import template from './index.html'; -import { CODE_PATH_BEATS } from '../../../../common/constants'; +import { + CODE_PATH_BEATS, + ALERT_MISSING_MONITORING_DATA, + BEATS_SYSTEM_ID, +} from '../../../../common/constants'; import { Beat } from '../../../components/beats/beat'; uiRoutes.when('/beats/beat/:beatUuid', { @@ -52,6 +56,17 @@ uiRoutes.when('/beats/beat/:beatUuid', { $scope, $injector, reactNodeId: 'monitoringBeatsInstanceApp', + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: BEATS_SYSTEM_ID, + }, + ], + }, + }, }); this.data = pageData; @@ -60,6 +75,7 @@ uiRoutes.when('/beats/beat/:beatUuid', { (data) => { this.renderReact( this.data, (data) => { this.renderReact( - + ); } ); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js index 8c30e4a2c1b07..ff7f29c58b2f6 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js @@ -20,6 +20,7 @@ import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, } from '../../../../../common/constants'; @@ -71,7 +72,7 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE], + alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], filters: [ { nodeUuid: nodeName, 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 ed2603e6dfff3..15b9b7b4c0e4a 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -21,6 +21,7 @@ import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, } from '../../../../common/constants'; @@ -55,7 +56,7 @@ uiRoutes.when('/elasticsearch/nodes/:node', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE], + alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], filters: [ { nodeUuid: nodeName, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js index 66fcac43e4fc5..ef807bf9b377d 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -19,6 +19,7 @@ import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, } from '../../../../common/constants'; @@ -87,7 +88,12 @@ uiRoutes.when('/elasticsearch/nodes', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE], + alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: ELASTICSEARCH_SYSTEM_ID, + }, + ], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js index 20a1a51719415..29852501d1667 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js @@ -27,7 +27,12 @@ import { import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { DetailStatus } from '../../../components/kibana/detail_status'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; +import { + CODE_PATH_KIBANA, + ALERT_KIBANA_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, + KIBANA_SYSTEM_ID, +} from '../../../../common/constants'; import { AlertsCallout } from '../../../alerts/callout'; function getPageData($injector) { @@ -76,7 +81,12 @@ uiRoutes.when('/kibana/instances/:uuid', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: KIBANA_SYSTEM_ID, + }, + ], }, }, }); @@ -104,7 +114,15 @@ uiRoutes.when('/kibana/instances/:uuid', { - + { + if (nextStep.text.includes('Kibana instances')) { + return false; + } + return true; + }} + /> diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js index 765e112a23305..fcb2ee53471a1 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js @@ -17,6 +17,7 @@ import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA, ALERT_KIBANA_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, } from '../../../../common/constants'; uiRoutes.when('/kibana/instances', { @@ -46,7 +47,12 @@ uiRoutes.when('/kibana/instances', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: KIBANA_SYSTEM_ID, + }, + ], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js index 0fc3cc47502cd..591db66b2698c 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js @@ -26,7 +26,13 @@ import { EuiFlexItem, } from '@elastic/eui'; import { MonitoringTimeseriesContainer } from '../../../../components/chart'; -import { CODE_PATH_LOGSTASH } from '../../../../../common/constants'; +import { + CODE_PATH_LOGSTASH, + ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, + LOGSTASH_SYSTEM_ID, +} from '../../../../../common/constants'; +import { AlertsCallout } from '../../../../alerts/callout'; function getPageData($injector) { const $http = $injector.get('$http'); @@ -69,6 +75,17 @@ uiRoutes.when('/logstash/node/:uuid/advanced', { reactNodeId: 'monitoringLogstashNodeAdvancedApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: LOGSTASH_SYSTEM_ID, + }, + ], + }, + }, telemetryPageViewTitle: 'logstash_node_advanced', }); @@ -112,6 +129,15 @@ uiRoutes.when('/logstash/node/:uuid/advanced', { + { + if (nextStep.text.includes('Logstash nodes')) { + return false; + } + return true; + }} + /> {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/index.js index e2dee77133c72..cccae6913052a 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/node/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/index.js @@ -26,7 +26,12 @@ import { } from '@elastic/eui'; import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_LOGSTASH, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; +import { + CODE_PATH_LOGSTASH, + ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, + LOGSTASH_SYSTEM_ID, +} from '../../../../common/constants'; import { AlertsCallout } from '../../../alerts/callout'; function getPageData($injector) { @@ -73,7 +78,12 @@ uiRoutes.when('/logstash/node/:uuid', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: LOGSTASH_SYSTEM_ID, + }, + ], }, }, telemetryPageViewTitle: 'logstash_node', @@ -120,7 +130,15 @@ uiRoutes.when('/logstash/node/:uuid', { - + { + if (nextStep.text.includes('Logstash nodes')) { + return false; + } + return true; + }} + /> {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js index 6f2e47eb3f918..20b2f68e2c67e 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js @@ -16,6 +16,7 @@ import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID, ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_MISSING_MONITORING_DATA, } from '../../../../common/constants'; uiRoutes.when('/logstash/nodes', { @@ -45,7 +46,12 @@ uiRoutes.when('/logstash/nodes', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA], + filters: [ + { + stackProduct: LOGSTASH_SYSTEM_ID, + }, + ], }, }, }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts index 60693eb42a30e..ddc8dcafebd21 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts @@ -63,6 +63,6 @@ describe('AlertsFactory', () => { it('should get all', () => { const alerts = AlertsFactory.getAll(); - expect(alerts.length).toBe(8); + expect(alerts.length).toBe(9); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts index 6b1c0d5fffe18..05a92cea5469b 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -6,6 +6,7 @@ import { CpuUsageAlert, + MissingMonitoringDataAlert, DiskUsageAlert, NodesChangedAlert, ClusterHealthAlert, @@ -19,6 +20,7 @@ import { ALERT_CLUSTER_HEALTH, ALERT_LICENSE_EXPIRATION, ALERT_CPU_USAGE, + ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, ALERT_NODES_CHANGED, ALERT_LOGSTASH_VERSION_MISMATCH, @@ -31,6 +33,7 @@ export const BY_TYPE = { [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert, [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert, [ALERT_CPU_USAGE]: CpuUsageAlert, + [ALERT_MISSING_MONITORING_DATA]: MissingMonitoringDataAlert, [ALERT_DISK_USAGE]: DiskUsageAlert, [ALERT_NODES_CHANGED]: NodesChangedAlert, [ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert, diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index aff84710d27ad..61486626040f7 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -198,6 +198,15 @@ export class BaseAlert { const alertInstance: RawAlertInstance = states.alertInstances[instanceId]; if (alertInstance && this.filterAlertInstance(alertInstance, filters)) { accum[instanceId] = alertInstance; + if (alertInstance.state) { + accum[instanceId].state = { + alertStates: (alertInstance.state as AlertInstanceState).alertStates.filter( + (alertState: AlertState) => { + return this.filterAlertState(alertState, filters); + } + ), + }; + } } return accum; }, @@ -209,6 +218,10 @@ export class BaseAlert { return true; } + protected filterAlertState(alertState: AlertState, filters: CommonAlertFilter[]) { + return true; + } + protected async execute({ services, params, state }: AlertExecutorOptions): Promise { const logger = this.getLogger(this.type); logger.debug( @@ -226,13 +239,7 @@ export class BaseAlert { return await mbSafeQuery(async () => _callCluster(endpoint, clientParams, options)); }; const availableCcs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : []; - // Support CCS use cases by querying to find available remote clusters - // and then adding those to the index pattern we are searching against - let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); - if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } - const clusters = await fetchClusters(callCluster, esIndexPattern); + const clusters = await this.fetchClusters(callCluster, availableCcs, params); const uiSettings = (await this.getUiSettingsService()).asScopedToClient( services.savedObjectsClient ); @@ -241,6 +248,26 @@ export class BaseAlert { return await this.processData(data, clusters, services, logger, state); } + protected async fetchClusters( + callCluster: any, + availableCcs: string[] | undefined = undefined, + params: CommonAlertParams + ) { + let ccs; + if (!availableCcs) { + ccs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : undefined; + } else { + ccs = availableCcs; + } + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); + if (ccs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, ccs); + } + return await fetchClusters(callCluster, esIndexPattern); + } + protected async fetchData( params: CommonAlertParams, callCluster: any, 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 4228354f52748..ca9674c57216b 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -26,7 +26,7 @@ import { RawAlertInstance } from '../../../alerts/common'; import { parseDuration } from '../../../alerts/common/parse_duration'; import { CommonAlertFilter, - CommonAlertCpuUsageFilter, + CommonAlertNodeUuidFilter, CommonAlertParams, CommonAlertParamDetail, } from '../../common/types'; @@ -129,7 +129,7 @@ export class CpuUsageAlert extends BaseAlert { const alertInstanceState = (alertInstance.state as unknown) as AlertInstanceState; if (filters && filters.length) { for (const _filter of filters) { - const filter = _filter as CommonAlertCpuUsageFilter; + const filter = _filter as CommonAlertNodeUuidFilter; if (filter && filter.nodeUuid) { let nodeExistsInStates = false; for (const state of alertInstanceState.alertStates) { diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts index 8fdac65514477..41f6daa38d1dc 100644 --- a/x-pack/plugins/monitoring/server/alerts/index.ts +++ b/x-pack/plugins/monitoring/server/alerts/index.ts @@ -6,6 +6,7 @@ export { BaseAlert } from './base_alert'; export { CpuUsageAlert } from './cpu_usage_alert'; +export { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; export { DiskUsageAlert } from './disk_usage_alert'; export { ClusterHealthAlert } from './cluster_health_alert'; export { LicenseExpirationAlert } from './license_expiration_alert'; 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 new file mode 100644 index 0000000000000..4c06d9718c455 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts @@ -0,0 +1,459 @@ +/* + * 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 { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; +import { ALERT_MISSING_MONITORING_DATA } from '../../common/constants'; +import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_missing_monitoring_data', () => ({ + fetchMissingMonitoringData: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('MissingMonitoringDataAlert', () => { + it('should have defaults', () => { + const alert = new MissingMonitoringDataAlert(); + expect(alert.type).toBe(ALERT_MISSING_MONITORING_DATA); + expect(alert.label).toBe('Missing monitoring data'); + expect(alert.defaultThrottle).toBe('1d'); + // @ts-ignore + expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '5m' }); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { name: 'stackProducts', description: 'The stack products missing monitoring data.' }, + { name: 'count', description: 'The number of stack products missing monitoring data.' }, + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const stackProduct = 'elasticsearch'; + const stackProductUuid = 'esNode1'; + const stackProductName = 'esName1'; + const gapDuration = 3000001; + const missingData = [ + { + stackProduct, + stackProductUuid, + stackProductName, + clusterUuid, + gapDuration, + }, + { + stackProduct: 'kibana', + stackProductUuid: 'kibanaUuid1', + stackProductName: 'kibanaInstance1', + clusterUuid, + gapDuration: gapDuration + 10, + }, + ]; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + metricbeat: { index: 'metricbeat-*' }, + }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { + return missingData; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new MissingMonitoringDataAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl, + false + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 2; + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + ccs: undefined, + cluster: { clusterUuid, clusterName }, + gapDuration, + stackProduct, + stackProductName, + stackProductUuid, + ui: { + isFiring: true, + message: { + text: + 'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute', + nextSteps: [ + { + text: '#start_linkView all Elasticsearch nodes#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/nodes', + }, + ], + }, + { + text: 'Verify monitoring settings on the node', + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + ], + }, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + { + ccs: undefined, + cluster: { clusterUuid, clusterName }, + gapDuration: gapDuration + 10, + stackProduct: 'kibana', + stackProductName: 'kibanaInstance1', + stackProductUuid: 'kibanaUuid1', + ui: { + isFiring: true, + message: { + text: + 'For the past an hour, we have not detected any monitoring data from the Kibana instance: kibanaInstance1, starting at #absolute', + nextSteps: [ + { + text: '#start_linkView all Kibana instances#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'kibana/instances', + }, + ], + }, + { + text: 'Verify monitoring settings on the instance', + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + ], + }, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. [View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`, + internalShortMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`, + action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`, + actionPlain: + 'Verify these stack products are up and running, then double check the monitoring settings.', + clusterName, + count, + stackProducts: 'Elasticsearch node: esName1, Kibana instance: kibanaInstance1', + state: 'firing', + }); + }); + + it('should not fire actions if under threshold', async () => { + (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { + return [ + { + ...missingData[0], + gapDuration: 1, + }, + ]; + }); + const alert = new MissingMonitoringDataAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl, + false + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + gapDuration: 1, + stackProduct, + stackProductName, + stackProductUuid, + ui: { + isFiring: false, + lastCheckedMS: 0, + message: null, + resolvedMS: 0, + severity: 'danger', + triggeredMS: 0, + }, + }, + ], + }); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { + return [ + { + ...missingData[0], + gapDuration: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + gapDuration: 1, + stackProduct, + stackProductName, + stackProductUuid, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new MissingMonitoringDataAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl, + false + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + gapDuration: 1, + stackProduct, + stackProductName, + stackProductUuid, + ui: { + isFiring: false, + message: { + text: + 'We are now seeing monitoring data for the Elasticsearch node: esName1, as of #resolved', + tokens: [ + { + startToken: '#resolved', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + ], + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `We are now seeing monitoring data for 1 stack product(s) in cluster testCluster.`, + internalShortMessage: `We are now seeing monitoring data for 1 stack product(s) in cluster: testCluster.`, + clusterName, + count, + stackProducts: 'Elasticsearch node: esName1', + state: 'resolved', + }); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { + return [ + { + ...missingData[0], + ccs, + }, + ]; + }); + const alert = new MissingMonitoringDataAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl, + false + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `We have not detected any monitoring data for 1 stack product(s) in cluster: testCluster. [View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123,ccs:testCluster))`, + internalShortMessage: `We have not detected any monitoring data for 1 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`, + action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123,ccs:testCluster))`, + actionPlain: + 'Verify these stack products are up and running, then double check the monitoring settings.', + clusterName, + count, + stackProducts: 'Elasticsearch node: esName1', + state: 'firing', + }); + }); + + it('should fire with different messaging for cloud', async () => { + const alert = new MissingMonitoringDataAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl, + true + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 2; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`, + internalShortMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`, + action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`, + actionPlain: + 'Verify these stack products are up and running, then double check the monitoring settings.', + clusterName, + count, + stackProducts: 'Elasticsearch node: esName1, Kibana instance: kibanaInstance1', + state: 'firing', + }); + }); + }); +}); 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 new file mode 100644 index 0000000000000..6017314f332e6 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -0,0 +1,504 @@ +/* + * 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 { IUiSettingsClient, Logger } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertMissingDataState, + AlertMissingData, + AlertMessageTimeToken, + AlertInstanceState, +} from './types'; +import { AlertInstance, AlertServices } from '../../../alerts/server'; +import { + INDEX_PATTERN, + ALERT_MISSING_MONITORING_DATA, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { RawAlertInstance } from '../../../alerts/common'; +import { parseDuration } from '../../../alerts/common/parse_duration'; +import { + CommonAlertFilter, + CommonAlertParams, + CommonAlertParamDetail, + CommonAlertStackProductFilter, + CommonAlertNodeUuidFilter, +} from '../../common/types'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; +import { getTypeLabelForStackProduct } from '../lib/alerts/get_type_label_for_stack_product'; +import { getListingLinkForStackProduct } from '../lib/alerts/get_listing_link_for_stack_product'; +import { getStackProductLabel } from '../lib/alerts/get_stack_product_label'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; +import { AlertingDefaults, createLink } from './alerts_common'; + +const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.missingData.firing', { + defaultMessage: 'firing', +}); + +const DEFAULT_DURATION = '5m'; +const DEFAULT_LIMIT = '1d'; + +// Go a bit farther back because we need to detect the difference between seeing the monitoring data versus just not looking far enough back +const LIMIT_BUFFER = 3 * 60 * 1000; + +interface MissingDataParams { + duration: string; + limit: string; +} + +export class MissingMonitoringDataAlert extends BaseAlert { + public static paramDetails = { + duration: { + label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', { + defaultMessage: `Notify if monitoring data is missing for`, + }), + type: AlertParamType.Duration, + } as CommonAlertParamDetail, + limit: { + label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.limit.label', { + defaultMessage: `Look this far back in time for monitoring data`, + }), + type: AlertParamType.Duration, + } as CommonAlertParamDetail, + }; + + public type = ALERT_MISSING_MONITORING_DATA; + public label = i18n.translate('xpack.monitoring.alerts.missingData.label', { + defaultMessage: 'Missing monitoring data', + }); + + protected defaultParams: MissingDataParams = { + duration: DEFAULT_DURATION, + limit: DEFAULT_LIMIT, + }; + + protected actionVariables = [ + { + name: 'stackProducts', + description: i18n.translate( + 'xpack.monitoring.alerts.missingData.actionVariables.stackProducts', + { + defaultMessage: 'The stack products missing monitoring data.', + } + ), + }, + { + name: 'count', + description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.count', { + defaultMessage: 'The number of stack products missing monitoring data.', + }), + }, + ...Object.values(AlertingDefaults.ALERT_TYPE.context), + ]; + + protected async fetchClusters( + callCluster: any, + availableCcs: string[] | undefined = undefined, + params: CommonAlertParams + ) { + const limit = parseDuration(((params as unknown) as MissingDataParams).limit); + let ccs; + if (!availableCcs) { + ccs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : undefined; + } else { + ccs = availableCcs; + } + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); + if (ccs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, ccs); + } + return await fetchClusters(callCluster, esIndexPattern, { + timestamp: { + format: 'epoch_millis', + gte: limit - LIMIT_BUFFER, + }, + }); + } + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let indexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN); + if (availableCcs) { + indexPattern = getCcsIndexPattern(indexPattern, availableCcs); + } + const duration = parseDuration(((params as unknown) as MissingDataParams).duration); + const limit = parseDuration(((params as unknown) as MissingDataParams).limit); + const now = +new Date(); + const missingData = await fetchMissingMonitoringData( + callCluster, + clusters, + indexPattern, + this.config.ui.max_bucket_size, + now, + now - limit - LIMIT_BUFFER + ); + return missingData.map((missing) => { + return { + instanceKey: `${missing.clusterUuid}:${missing.stackProduct}:${missing.stackProductUuid}`, + clusterUuid: missing.clusterUuid, + shouldFire: missing.gapDuration > duration, + severity: AlertSeverity.Danger, + meta: { missing, limit }, + ccs: missing.ccs, + }; + }); + } + + protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) { + const alertInstanceState = (alertInstance.state as unknown) as AlertInstanceState; + if (filters && filters.length) { + for (const filter of filters) { + const stackProductFilter = filter as CommonAlertStackProductFilter; + if (stackProductFilter && stackProductFilter.stackProduct) { + let existsInState = false; + for (const state of alertInstanceState.alertStates) { + if ((state as AlertMissingDataState).stackProduct === stackProductFilter.stackProduct) { + existsInState = true; + break; + } + } + if (!existsInState) { + return false; + } + } + } + } + return true; + } + + protected filterAlertState(alertState: AlertState, filters: CommonAlertFilter[]) { + const state = alertState as AlertMissingDataState; + if (filters && filters.length) { + for (const filter of filters) { + const stackProductFilter = filter as CommonAlertStackProductFilter; + if (stackProductFilter && stackProductFilter.stackProduct) { + if (state.stackProduct !== stackProductFilter.stackProduct) { + return false; + } + } + + const nodeUuidFilter = filter as CommonAlertNodeUuidFilter; + if (nodeUuidFilter && nodeUuidFilter.nodeUuid) { + if (state.stackProductUuid !== nodeUuidFilter.nodeUuid) { + return false; + } + } + } + } + return true; + } + + protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { + const base = super.getDefaultAlertState(cluster, item); + return { + ...base, + ui: { + ...base.ui, + severity: AlertSeverity.Danger, + }, + }; + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const { missing, limit } = item.meta as { missing: AlertMissingData; limit: number }; + if (!alertState.ui.isFiring) { + if (missing.gapDuration > limit) { + return { + text: i18n.translate('xpack.monitoring.alerts.missingData.ui.notQuiteResolvedMessage', { + defaultMessage: `We are still not seeing monitoring data for the {stackProduct} {type}: {stackProductName} and will stop trying. To change this, configure the alert to look farther back for data.`, + values: { + stackProduct: getStackProductLabel(missing.stackProduct), + type: getTypeLabelForStackProduct(missing.stackProduct, false), + stackProductName: missing.stackProductName, + }, + }), + }; + } + return { + text: i18n.translate('xpack.monitoring.alerts.missingData.ui.resolvedMessage', { + defaultMessage: `We are now seeing monitoring data for the {stackProduct} {type}: {stackProductName}, as of #resolved`, + values: { + stackProduct: getStackProductLabel(missing.stackProduct), + type: getTypeLabelForStackProduct(missing.stackProduct, false), + stackProductName: missing.stackProductName, + }, + }), + tokens: [ + { + startToken: '#resolved', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.resolvedMS, + } as AlertMessageTimeToken, + ], + }; + } + return { + text: i18n.translate('xpack.monitoring.alerts.missingData.ui.firingMessage', { + defaultMessage: `For the past {gapDuration}, we have not detected any monitoring data from the {stackProduct} {type}: {stackProductName}, starting at #absolute`, + values: { + gapDuration: moment.duration(missing.gapDuration, 'milliseconds').humanize(), + stackProduct: getStackProductLabel(missing.stackProduct), + type: getTypeLabelForStackProduct(missing.stackProduct, false), + stackProductName: missing.stackProductName, + }, + }), + nextSteps: [ + createLink( + i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.viewAll', { + defaultMessage: `#start_linkView all {stackProduct} {type}#end_link`, + values: { + type: getTypeLabelForStackProduct(missing.stackProduct), + stackProduct: getStackProductLabel(missing.stackProduct), + }, + }), + getListingLinkForStackProduct(missing.stackProduct), + AlertMessageTokenType.Link + ), + { + text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.verifySettings', { + defaultMessage: `Verify monitoring settings on the {type}`, + values: { + type: getTypeLabelForStackProduct(missing.stackProduct, false), + }, + }), + }, + ], + tokens: [ + { + startToken: '#absolute', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.triggeredMS, + } as AlertMessageTimeToken, + ], + }; + } + + protected executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData | null, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + + const ccs = instanceState.alertStates.reduce((accum: string, state): string => { + if (state.ccs) { + return state.ccs; + } + return accum; + }, ''); + + const firingCount = instanceState.alertStates.filter((alertState) => alertState.ui.isFiring) + .length; + const firingStackProducts = instanceState.alertStates + .filter((_state) => (_state as AlertMissingDataState).ui.isFiring) + .map((_state) => { + const state = _state as AlertMissingDataState; + return `${getStackProductLabel(state.stackProduct)} ${getTypeLabelForStackProduct( + state.stackProduct, + false + )}: ${state.stackProductName}`; + }) + .join(', '); + if (firingCount > 0) { + const shortActionText = i18n.translate('xpack.monitoring.alerts.missingData.shortAction', { + defaultMessage: + 'Verify these stack products are up and running, then double check the monitoring settings.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.missingData.fullAction', { + defaultMessage: 'View what monitoring data we do have for these stack products.', + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (ccs) { + globalState.push(`ccs:${ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#overview?_g=(${globalState.join(',')})`; + const action = `[${fullActionText}](${url})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.missingData.firing.internalShortMessage', + { + defaultMessage: `We have not detected any monitoring data for {count} stack product(s) in cluster: {clusterName}. {shortActionText}`, + values: { + count: firingCount, + clusterName: cluster.clusterName, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.missingData.firing.internalFullMessage', + { + defaultMessage: `We have not detected any monitoring data for {count} stack product(s) in cluster: {clusterName}. {action}`, + values: { + count: firingCount, + clusterName: cluster.clusterName, + action, + }, + } + ); + instance.scheduleActions('default', { + internalShortMessage, + internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage, + state: FIRING, + stackProducts: firingStackProducts, + count: firingCount, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }); + } else { + const resolvedCount = instanceState.alertStates.filter( + (alertState) => !alertState.ui.isFiring + ).length; + const resolvedStackProducts = instanceState.alertStates + .filter((_state) => !(_state as AlertMissingDataState).ui.isFiring) + .map((_state) => { + const state = _state as AlertMissingDataState; + return `${getStackProductLabel(state.stackProduct)} ${getTypeLabelForStackProduct( + state.stackProduct, + false + )}: ${state.stackProductName}`; + }) + .join(','); + if (resolvedCount > 0) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.missingData.resolved.internalShortMessage', + { + defaultMessage: `We are now seeing monitoring data for {count} stack product(s) in cluster: {clusterName}.`, + values: { + count: resolvedCount, + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.missingData.resolved.internalFullMessage', + { + defaultMessage: `We are now seeing monitoring data for {count} stack product(s) in cluster {clusterName}.`, + values: { + count: resolvedCount, + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + stackProducts: resolvedStackProducts, + count: resolvedCount, + clusterName: cluster.clusterName, + }); + } + } + } + + protected async processData( + data: AlertData[], + clusters: AlertCluster[], + services: AlertServices, + logger: Logger + ) { + for (const cluster of clusters) { + const stackProducts = data.filter((_item) => _item.clusterUuid === cluster.clusterUuid); + if (stackProducts.length === 0) { + continue; + } + + const firingInstances = stackProducts.reduce((list: string[], stackProduct) => { + const { missing } = stackProduct.meta as { missing: AlertMissingData; limit: number }; + if (stackProduct.shouldFire) { + list.push(`${missing.stackProduct}:${missing.stackProductUuid}`); + } + return list; + }, [] as string[]); + firingInstances.sort(); // It doesn't matter how we sort, but keep the order consistent + const instanceId = `${this.type}:${cluster.clusterUuid}:${firingInstances.join(',')}`; + const instance = services.alertInstanceFactory(instanceId); + const instanceState = (instance.getState() as unknown) as AlertInstanceState; + const alertInstanceState: AlertInstanceState = { + alertStates: instanceState?.alertStates || [], + }; + let shouldExecuteActions = false; + for (const stackProduct of stackProducts) { + const { missing } = stackProduct.meta as { missing: AlertMissingData; limit: number }; + let state: AlertMissingDataState; + const indexInState = alertInstanceState.alertStates.findIndex((alertState) => { + const _alertState = alertState as AlertMissingDataState; + return ( + _alertState.cluster.clusterUuid === cluster.clusterUuid && + _alertState.stackProduct === missing.stackProduct && + _alertState.stackProductUuid === missing.stackProductUuid + ); + }); + if (indexInState > -1) { + state = alertInstanceState.alertStates[indexInState] as AlertMissingDataState; + } else { + state = this.getDefaultAlertState(cluster, stackProduct) as AlertMissingDataState; + } + + state.stackProduct = missing.stackProduct; + state.stackProductUuid = missing.stackProductUuid; + state.stackProductName = missing.stackProductName; + state.gapDuration = missing.gapDuration; + + if (stackProduct.shouldFire) { + if (!state.ui.isFiring) { + state.ui.triggeredMS = new Date().valueOf(); + } + state.ui.isFiring = true; + state.ui.message = this.getUiMessage(state, stackProduct); + state.ui.severity = stackProduct.severity; + state.ui.resolvedMS = 0; + shouldExecuteActions = true; + } else if (!stackProduct.shouldFire && state.ui.isFiring) { + state.ui.isFiring = false; + state.ui.resolvedMS = new Date().valueOf(); + state.ui.message = this.getUiMessage(state, stackProduct); + shouldExecuteActions = true; + } + + if (indexInState === -1) { + alertInstanceState.alertStates.push(state); + } else { + alertInstanceState.alertStates = [ + ...alertInstanceState.alertStates.slice(0, indexInState), + state, + ...alertInstanceState.alertStates.slice(indexInState + 1), + ]; + } + } + + instance.replaceState(alertInstanceState); + if (shouldExecuteActions) { + this.executeActions(instance, alertInstanceState, null, cluster); + } + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts index b685dcaed790f..4b78bca9f47ca 100644 --- a/x-pack/plugins/monitoring/server/alerts/types.d.ts +++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts @@ -28,6 +28,13 @@ export interface AlertCpuUsageState extends AlertState { nodeName: string; } +export interface AlertMissingDataState extends AlertState { + stackProduct: string; + stackProductUuid: string; + stackProductName: string; + gapDuration: number; +} + export interface AlertDiskUsageState extends AlertState { diskUsage: number; nodeId: string; @@ -93,6 +100,15 @@ export interface AlertDiskUsageNodeStats { ccs?: string; } +export interface AlertMissingData { + stackProduct: string; + stackProductUuid: string; + stackProductName: string; + clusterUuid: string; + gapDuration: number; + ccs?: string; +} + export interface AlertData { instanceKey: string; clusterUuid: string; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index 48ad31d20a395..d474338bce922 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -6,7 +6,18 @@ import { get } from 'lodash'; import { AlertCluster } from '../../alerts/types'; -export async function fetchClusters(callCluster: any, index: string): Promise { +interface RangeFilter { + [field: string]: { + format?: string; + gte: string | number; + }; +} + +export async function fetchClusters( + callCluster: any, + index: string, + rangeFilter: RangeFilter = { timestamp: { gte: 'now-2m' } } +): Promise { const params = { index, filterPath: [ @@ -25,11 +36,7 @@ export async function fetchClusters(callCluster: any, index: string): Promise +) { + return { + buckets: products.map((product) => { + return { + key: product.uuid, + most_recent: { + value: product.timestamp, + }, + document: { + hits: { + hits: [ + { + _index: index, + _source: product.nameSource, + }, + ], + }, + }, + }; + }), + }; +} + +describe('fetchMissingMonitoringData', () => { + let callCluster = jest.fn(); + const index = '.monitoring-*'; + const startMs = 100; + const size = 10; + + it('fetch as expected', async () => { + const now = 10; + const clusters = [ + { + clusterUuid: 'clusterUuid1', + clusterName: 'clusterName1', + }, + ]; + callCluster = jest.fn().mockImplementation((...args) => { + return { + aggregations: { + clusters: { + buckets: clusters.map((cluster) => ({ + key: cluster.clusterUuid, + es_uuids: getResponse('.monitoring-es-*', [ + { + uuid: 'nodeUuid1', + nameSource: { + source_node: { + name: 'nodeName1', + }, + }, + timestamp: 9, + }, + { + uuid: 'nodeUuid2', + nameSource: { + source_node: { + name: 'nodeName2', + }, + }, + timestamp: 2, + }, + ]), + kibana_uuids: getResponse('.monitoring-kibana-*', [ + { + uuid: 'kibanaUuid1', + nameSource: { + kibana_stats: { + kibana: { + name: 'kibanaName1', + }, + }, + }, + timestamp: 4, + }, + ]), + logstash_uuids: getResponse('.monitoring-logstash-*', [ + { + uuid: 'logstashUuid1', + nameSource: { + logstash_stats: { + logstash: { + host: 'logstashName1', + }, + }, + }, + timestamp: 2, + }, + ]), + beats: { + beats_uuids: getResponse('.monitoring-beats-*', [ + { + uuid: 'beatUuid1', + nameSource: { + beats_stats: { + beat: { + name: 'beatName1', + }, + }, + }, + timestamp: 0, + }, + ]), + }, + apms: { + apm_uuids: getResponse('.monitoring-beats-*', [ + { + uuid: 'apmUuid1', + nameSource: { + beats_stats: { + beat: { + name: 'apmName1', + type: 'apm-server', + }, + }, + }, + timestamp: 1, + }, + ]), + }, + })), + }, + }, + }; + }); + const result = await fetchMissingMonitoringData( + callCluster, + clusters, + index, + size, + now, + startMs + ); + expect(result).toEqual([ + { + stackProduct: 'elasticsearch', + stackProductUuid: 'nodeUuid1', + stackProductName: 'nodeName1', + clusterUuid: 'clusterUuid1', + gapDuration: 1, + ccs: null, + }, + { + stackProduct: 'elasticsearch', + stackProductUuid: 'nodeUuid2', + stackProductName: 'nodeName2', + clusterUuid: 'clusterUuid1', + gapDuration: 8, + ccs: null, + }, + { + stackProduct: 'kibana', + stackProductUuid: 'kibanaUuid1', + stackProductName: 'kibanaName1', + clusterUuid: 'clusterUuid1', + gapDuration: 6, + ccs: null, + }, + { + stackProduct: 'logstash', + stackProductUuid: 'logstashUuid1', + stackProductName: 'logstashName1', + clusterUuid: 'clusterUuid1', + gapDuration: 8, + ccs: null, + }, + { + stackProduct: 'beats', + stackProductUuid: 'beatUuid1', + stackProductName: 'beatName1', + clusterUuid: 'clusterUuid1', + gapDuration: 10, + ccs: null, + }, + { + stackProduct: 'apm', + stackProductUuid: 'apmUuid1', + stackProductName: 'apmName1', + clusterUuid: 'clusterUuid1', + gapDuration: 9, + ccs: null, + }, + ]); + }); + + it('should handle ccs', async () => { + const now = 10; + const clusters = [ + { + clusterUuid: 'clusterUuid1', + clusterName: 'clusterName1', + }, + ]; + callCluster = jest.fn().mockImplementation((...args) => { + return { + aggregations: { + clusters: { + buckets: clusters.map((cluster) => ({ + key: cluster.clusterUuid, + es_uuids: getResponse('Monitoring:.monitoring-es-*', [ + { + uuid: 'nodeUuid1', + nameSource: { + source_node: { + name: 'nodeName1', + }, + }, + timestamp: 9, + }, + ]), + })), + }, + }, + }; + }); + const result = await fetchMissingMonitoringData( + callCluster, + clusters, + index, + size, + now, + startMs + ); + expect(result).toEqual([ + { + stackProduct: 'elasticsearch', + stackProductUuid: 'nodeUuid1', + stackProductName: 'nodeName1', + clusterUuid: 'clusterUuid1', + gapDuration: 1, + ccs: 'Monitoring', + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts new file mode 100644 index 0000000000000..91fc05137a8c1 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -0,0 +1,275 @@ +/* + * 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 { get } from 'lodash'; +import { AlertCluster, AlertMissingData } from '../../alerts/types'; +import { + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, + APM_SYSTEM_ID, + LOGSTASH_SYSTEM_ID, + ELASTICSEARCH_SYSTEM_ID, +} from '../../../common/constants'; + +interface ClusterBucketESResponse { + key: string; + kibana_uuids?: UuidResponse; + logstash_uuids?: UuidResponse; + es_uuids?: UuidResponse; + beats?: { + beats_uuids: UuidResponse; + }; + apms?: { + apm_uuids: UuidResponse; + }; +} + +interface UuidResponse { + buckets: UuidBucketESResponse[]; +} + +interface UuidBucketESResponse { + key: string; + most_recent: { + value: number; + }; + document: { + hits: { + hits: TopHitESResponse[]; + }; + }; +} + +interface TopHitESResponse { + _index: string; + _source: { + source_node?: { + name: string; + }; + kibana_stats?: { + kibana: { + name: string; + }; + }; + logstash_stats?: { + logstash: { + host: string; + }; + }; + beats_stats?: { + beat: { + name: string; + type: string; + }; + }; + }; +} + +function getStackProductFromIndex(index: string, beatType: string) { + if (index.includes('-kibana-')) { + return KIBANA_SYSTEM_ID; + } + if (index.includes('-beats-')) { + if (beatType === 'apm-server') { + return APM_SYSTEM_ID; + } + return BEATS_SYSTEM_ID; + } + if (index.includes('-logstash-')) { + return LOGSTASH_SYSTEM_ID; + } + if (index.includes('-es-')) { + return ELASTICSEARCH_SYSTEM_ID; + } + return ''; +} + +export async function fetchMissingMonitoringData( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number, + nowInMs: number, + startMs: number +): Promise { + const endMs = nowInMs; + + const nameFields = [ + 'source_node.name', + 'kibana_stats.kibana.name', + 'logstash_stats.logstash.host', + 'beats_stats.beat.name', + 'beat_stats.beat.type', + ]; + const subAggs = { + most_recent: { + max: { + field: 'timestamp', + }, + }, + document: { + top_hits: { + size: 1, + sort: [ + { + timestamp: { + order: 'desc', + }, + }, + ], + _source: { + includes: ['_index', ...nameFields], + }, + }, + }, + }; + + const params = { + index, + filterPath: ['aggregations.clusters.buckets'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + range: { + timestamp: { + format: 'epoch_millis', + gte: startMs, + lte: endMs, + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size, + }, + aggs: { + es_uuids: { + terms: { + field: 'node_stats.node_id', + size, + }, + aggs: subAggs, + }, + kibana_uuids: { + terms: { + field: 'kibana_stats.kibana.uuid', + size, + }, + aggs: subAggs, + }, + beats: { + filter: { + bool: { + must_not: { + term: { + 'beats_stats.beat.type': 'apm-server', + }, + }, + }, + }, + aggs: { + beats_uuids: { + terms: { + field: 'beats_stats.beat.uuid', + size, + }, + aggs: subAggs, + }, + }, + }, + apms: { + filter: { + bool: { + must: { + term: { + 'beats_stats.beat.type': 'apm-server', + }, + }, + }, + }, + aggs: { + apm_uuids: { + terms: { + field: 'beats_stats.beat.uuid', + size, + }, + aggs: subAggs, + }, + }, + }, + logstash_uuids: { + terms: { + field: 'logstash_stats.logstash.uuid', + size, + }, + aggs: subAggs, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const clusterBuckets = get( + response, + 'aggregations.clusters.buckets', + [] + ) as ClusterBucketESResponse[]; + const uniqueList: { [id: string]: AlertMissingData } = {}; + for (const clusterBucket of clusterBuckets) { + const clusterUuid = clusterBucket.key; + + const uuidBuckets = [ + ...(clusterBucket.es_uuids?.buckets || []), + ...(clusterBucket.kibana_uuids?.buckets || []), + ...(clusterBucket.logstash_uuids?.buckets || []), + ...(clusterBucket.beats?.beats_uuids.buckets || []), + ...(clusterBucket.apms?.apm_uuids.buckets || []), + ]; + + for (const uuidBucket of uuidBuckets) { + const stackProductUuid = uuidBucket.key; + const indexName = get(uuidBucket, `document.hits.hits[0]._index`); + const stackProduct = getStackProductFromIndex( + indexName, + get(uuidBucket, `document.hits.hits[0]._source.beats_stats.beat.type`) + ); + const differenceInMs = nowInMs - uuidBucket.most_recent.value; + let stackProductName = stackProductUuid; + for (const nameField of nameFields) { + stackProductName = get(uuidBucket, `document.hits.hits[0]._source.${nameField}`); + if (stackProductName) { + break; + } + } + + uniqueList[`${clusterUuid}${stackProduct}${stackProductUuid}`] = { + stackProduct, + stackProductUuid, + stackProductName, + clusterUuid, + gapDuration: differenceInMs, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + } + } + + const missingData = Object.values(uniqueList); + return missingData; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_listing_link_for_stack_product.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_listing_link_for_stack_product.ts new file mode 100644 index 0000000000000..1936ac1bc6183 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_listing_link_for_stack_product.ts @@ -0,0 +1,28 @@ +/* + * 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 { + BEATS_SYSTEM_ID, + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + LOGSTASH_SYSTEM_ID, + APM_SYSTEM_ID, +} from '../../../common/constants'; + +export function getListingLinkForStackProduct(stackProduct: string) { + switch (stackProduct) { + case ELASTICSEARCH_SYSTEM_ID: + return 'elasticsearch/nodes'; + case LOGSTASH_SYSTEM_ID: + return 'logstash/nodes'; + case KIBANA_SYSTEM_ID: + return 'kibana/instances'; + case BEATS_SYSTEM_ID: + return 'beats/beats'; + case APM_SYSTEM_ID: + return 'apm/instances'; + } + return ''; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_stack_product_label.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_stack_product_label.ts new file mode 100644 index 0000000000000..9dafd775bac14 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_stack_product_label.ts @@ -0,0 +1,17 @@ +/* + * 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 { capitalize } from 'lodash'; +import { APM_SYSTEM_ID, BEATS_SYSTEM_ID } from '../../../common/constants'; + +export function getStackProductLabel(stackProduct: string) { + switch (stackProduct) { + case APM_SYSTEM_ID: + return 'APM'; + case BEATS_SYSTEM_ID: + return 'Beat'; + } + return capitalize(stackProduct); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_type_label_for_stack_product.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_type_label_for_stack_product.ts new file mode 100644 index 0000000000000..74801de10438f --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_type_label_for_stack_product.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 { i18n } from '@kbn/i18n'; +import { + BEATS_SYSTEM_ID, + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + LOGSTASH_SYSTEM_ID, + APM_SYSTEM_ID, +} from '../../../common/constants'; + +const NODES = i18n.translate('xpack.monitoring.alerts.typeLabel.nodes', { + defaultMessage: 'nodes', +}); + +const INSTANCES = i18n.translate('xpack.monitoring.alerts.typeLabel.instances', { + defaultMessage: 'instances', +}); + +const SERVERS = i18n.translate('xpack.monitoring.alerts.typeLabel.servers', { + defaultMessage: 'servers', +}); + +const NODE = i18n.translate('xpack.monitoring.alerts.typeLabel.node', { + defaultMessage: 'node', +}); + +const INSTANCE = i18n.translate('xpack.monitoring.alerts.typeLabel.instance', { + defaultMessage: 'instance', +}); + +const SERVER = i18n.translate('xpack.monitoring.alerts.typeLabel.server', { + defaultMessage: 'server', +}); + +export function getTypeLabelForStackProduct(stackProduct: string, plural: boolean = true) { + switch (stackProduct) { + case ELASTICSEARCH_SYSTEM_ID: + case LOGSTASH_SYSTEM_ID: + return plural ? NODES : NODE; + case KIBANA_SYSTEM_ID: + case BEATS_SYSTEM_ID: + return plural ? INSTANCES : INSTANCE; + case APM_SYSTEM_ID: + return plural ? SERVERS : SERVER; + } + return 'n/a'; +}