From 627afe3f6cf503450fe04769d2782e757bc9b460 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Mon, 21 Sep 2020 16:01:55 -0400
Subject: [PATCH 01/16] WIP for alert

---
 x-pack/plugins/monitoring/common/constants.ts |   2 +
 .../public/alerts/filter_alert_states.ts      |  23 +
 .../alerts/missing_data_alert/expression.tsx  |  61 +++
 .../public/alerts/missing_data_alert/index.ts |   7 +
 .../missing_data_alert/missing_data_alert.tsx |  28 ++
 .../alerts/missing_data_alert/validation.tsx  |  35 ++
 .../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 +-
 x-pack/plugins/monitoring/public/plugin.ts    |   2 +
 .../server/alerts/alerts_factory.ts           |   3 +
 .../plugins/monitoring/server/alerts/index.ts |   1 +
 .../server/alerts/missing_data_alert.ts       | 468 ++++++++++++++++++
 .../monitoring/server/alerts/types.d.ts       |  15 +
 .../server/lib/alerts/fetch_missing_data.ts   | 291 +++++++++++
 18 files changed, 1042 insertions(+), 15 deletions(-)
 create mode 100644 x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts
 create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_data_alert/expression.tsx
 create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_data_alert/index.ts
 create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_data_alert/missing_data_alert.tsx
 create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_data_alert/validation.tsx
 create mode 100644 x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
 create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts

diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts
index 2c714080969e4..3f2aadf4f87ec 100644
--- a/x-pack/plugins/monitoring/common/constants.ts
+++ b/x-pack/plugins/monitoring/common/constants.ts
@@ -235,6 +235,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_DATA = `${ALERT_PREFIX}alert_missing_data`;
 
 /**
  * A listing of all alert types
@@ -247,6 +248,7 @@ export const ALERTS = [
   ALERT_ELASTICSEARCH_VERSION_MISMATCH,
   ALERT_KIBANA_VERSION_MISMATCH,
   ALERT_LOGSTASH_VERSION_MISMATCH,
+  ALERT_MISSING_DATA,
 ];
 
 /**
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_data_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/missing_data_alert/expression.tsx
new file mode 100644
index 0000000000000..7dc6155de529e
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/missing_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> = (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 (
+          <AlertParamDuration
+            key={alertParamName}
+            name={alertParamName}
+            duration={value}
+            label={details.label}
+            errors={errors[alertParamName]}
+            setAlertParams={setAlertParams}
+          />
+        );
+      case AlertParamType.Percentage:
+        return (
+          <AlertParamPercentage
+            key={alertParamName}
+            name={alertParamName}
+            label={details.label}
+            percentage={value}
+            errors={errors[alertParamName]}
+            setAlertParams={setAlertParams}
+          />
+        );
+    }
+  });
+
+  return (
+    <Fragment>
+      <EuiForm component="form">{alertParamsUi}</EuiForm>
+      <EuiSpacer />
+    </Fragment>
+  );
+};
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/index.ts b/x-pack/plugins/monitoring/public/alerts/missing_data_alert/index.ts
new file mode 100644
index 0000000000000..5a7dbda9d2a53
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/missing_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 { createMissingDataAlertType } from './missing_data_alert';
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/missing_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_data_alert/missing_data_alert.tsx
new file mode 100644
index 0000000000000..84b5545db8795
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/missing_data_alert/missing_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_DATA } from '../../../common/constants';
+import { Expression } from './expression';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { MissingDataAlert } from '../../../server/alerts';
+
+export function createMissingDataAlertType(): AlertTypeModel {
+  const alert = new MissingDataAlert();
+  return {
+    id: ALERT_MISSING_DATA,
+    name: alert.label,
+    iconClass: 'bell',
+    alertParamsExpression: (props: any) => (
+      <Expression {...props} paramDetails={MissingDataAlert.paramDetails} />
+    ),
+    validate,
+    defaultActionMessage: '{{context.internalFullMessage}}',
+    requiresAppContext: true,
+  };
+}
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/validation.tsx b/x-pack/plugins/monitoring/public/alerts/missing_data_alert/validation.tsx
new file mode 100644
index 0000000000000..fe84de9bd00ea
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/missing_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/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js
index ccbf0b0ec711d..297e3f1c62718 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_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_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 = (
+      <EuiFlexItem grow={false}>
+        <AlertsBadge alerts={alertsList} />
+      </EuiFlexItem>
+    );
+  }
+
   return (
     <ClusterItemContainer
       {...props}
@@ -140,7 +158,12 @@ export function ApmPanel(props) {
                   </h3>
                 </EuiTitle>
               </EuiFlexItem>
-              {setupModeMetricbeatMigrationTooltip}
+              <EuiFlexItem grow={false}>
+                <EuiFlexGroup gutterSize="s" alignItems="center">
+                  {setupModeMetricbeatMigrationTooltip}
+                  {apmServersAlertStatus}
+                </EuiFlexGroup>
+              </EuiFlexItem>
             </EuiFlexGroup>
             <EuiHorizontalRule margin="m" />
             <EuiDescriptionList type="column">
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 3591ad178f4cd..178ff2d5823a0 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_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_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 = (
+      <EuiFlexItem grow={false}>
+        <AlertsBadge alerts={alertsList} />
+      </EuiFlexItem>
+    );
+  }
+
   const beatTypes = props.beats.types.map((beat, index) => {
     return [
       <EuiDescriptionListTitle
@@ -145,7 +159,12 @@ export function BeatsPanel(props) {
                   </h3>
                 </EuiTitle>
               </EuiFlexItem>
-              {setupModeMetricbeatMigrationTooltip}
+              <EuiFlexItem grow={false}>
+                <EuiFlexGroup gutterSize="s" alignItems="center">
+                  {setupModeMetricbeatMigrationTooltip}
+                  {beatsAlertsStatus}
+                </EuiFlexGroup>
+              </EuiFlexItem>
             </EuiFlexGroup>
             <EuiHorizontalRule margin="m" />
             <EuiDescriptionList type="column">{beatTypes}</EuiDescriptionList>
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 34e995510cf72..4e5e2506f4680 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
@@ -41,6 +41,7 @@ import {
   ALERT_CPU_USAGE,
   ALERT_NODES_CHANGED,
   ALERT_ELASTICSEARCH_VERSION_MISMATCH,
+  ALERT_MISSING_DATA,
 } from '../../../../common/constants';
 import { AlertsBadge } from '../../../alerts/badge';
 import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
@@ -158,6 +159,7 @@ const NODES_PANEL_ALERTS = [
   ALERT_CPU_USAGE,
   ALERT_NODES_CHANGED,
   ALERT_ELASTICSEARCH_VERSION_MISMATCH,
+  ALERT_MISSING_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..b616898223d98 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_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_DATA) {
+                  return state.stackProduct === ELASTICSEARCH_SYSTEM_ID;
+                }
+                return true;
+              })}
             />
             <KibanaPanel
               {...props.cluster.kibana}
               setupMode={props.setupMode}
-              alerts={props.alerts}
+              alerts={filterAlertStates(props.alerts, (type, { state }) => {
+                if (type === ALERT_MISSING_DATA) {
+                  return state.stackProduct === KIBANA_SYSTEM_ID;
+                }
+                return true;
+              })}
             />
           </Fragment>
         ) : null}
@@ -50,12 +69,35 @@ export function Overview(props) {
         <LogstashPanel
           {...props.cluster.logstash}
           setupMode={props.setupMode}
-          alerts={props.alerts}
+          alerts={filterAlertStates(props.alerts, (type, { state }) => {
+            if (type === ALERT_MISSING_DATA) {
+              return state.stackProduct === LOGSTASH_SYSTEM_ID;
+            }
+            return true;
+          })}
         />
 
-        <BeatsPanel {...props.cluster.beats} setupMode={props.setupMode} />
+        <BeatsPanel
+          {...props.cluster.beats}
+          setupMode={props.setupMode}
+          alerts={filterAlertStates(props.alerts, (type, { state }) => {
+            if (type === ALERT_MISSING_DATA) {
+              return state.stackProduct === BEATS_SYSTEM_ID;
+            }
+            return true;
+          })}
+        />
 
-        <ApmPanel {...props.cluster.apm} setupMode={props.setupMode} />
+        <ApmPanel
+          {...props.cluster.apm}
+          setupMode={props.setupMode}
+          alerts={filterAlertStates(props.alerts, (type, { state }) => {
+            if (type === ALERT_MISSING_DATA) {
+              return state.stackProduct === APM_SYSTEM_ID;
+            }
+            return true;
+          })}
+        />
       </EuiPageBody>
     </EuiPage>
   );
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 6fa533302db48..cc83085f14bc2 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_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_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 9b4a50271a247..f0939f177076a 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_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_DATA];
 
 export function LogstashPanel(props) {
   const { setupMode } = props;
diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts
index 05aa75f586241..754235eb457e0 100644
--- a/x-pack/plugins/monitoring/public/plugin.ts
+++ b/x-pack/plugins/monitoring/public/plugin.ts
@@ -22,6 +22,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 { createMissingDataAlertType } from './alerts/missing_data_alert';
 import { createLegacyAlertTypes } from './alerts/legacy_alert';
 
 interface MonitoringSetupPluginDependencies {
@@ -69,6 +70,7 @@ export class MonitoringPlugin
     }
 
     plugins.triggers_actions_ui.alertTypeRegistry.register(createCpuUsageAlertType());
+    plugins.triggers_actions_ui.alertTypeRegistry.register(createMissingDataAlertType());
     const legacyAlertTypes = createLegacyAlertTypes();
     for (const legacyAlertType of legacyAlertTypes) {
       plugins.triggers_actions_ui.alertTypeRegistry.register(legacyAlertType);
diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
index b91eab05cf912..ab7510a708f09 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,
+  MissingDataAlert,
   NodesChangedAlert,
   ClusterHealthAlert,
   LicenseExpirationAlert,
@@ -18,6 +19,7 @@ import {
   ALERT_CLUSTER_HEALTH,
   ALERT_LICENSE_EXPIRATION,
   ALERT_CPU_USAGE,
+  ALERT_MISSING_DATA,
   ALERT_NODES_CHANGED,
   ALERT_LOGSTASH_VERSION_MISMATCH,
   ALERT_KIBANA_VERSION_MISMATCH,
@@ -29,6 +31,7 @@ export const BY_TYPE = {
   [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert,
   [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert,
   [ALERT_CPU_USAGE]: CpuUsageAlert,
+  [ALERT_MISSING_DATA]: MissingDataAlert,
   [ALERT_NODES_CHANGED]: NodesChangedAlert,
   [ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert,
   [ALERT_KIBANA_VERSION_MISMATCH]: KibanaVersionMismatchAlert,
diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts
index 048e703d2222c..bc4d443b6ac8f 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 { MissingDataAlert } from './missing_data_alert';
 export { ClusterHealthAlert } from './cluster_health_alert';
 export { LicenseExpirationAlert } from './license_expiration_alert';
 export { NodesChangedAlert } from './nodes_changed_alert';
diff --git a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
new file mode 100644
index 0000000000000..5e21855f14698
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
@@ -0,0 +1,468 @@
+/*
+ * 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,
+  AlertMessageDocLinkToken,
+} from './types';
+import { AlertInstance, AlertServices } from '../../../alerts/server';
+import { INDEX_PATTERN, ALERT_MISSING_DATA } 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 } from '../../common/types';
+import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
+import { fetchMissingData } from '../lib/alerts/fetch_missing_data';
+
+// 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';
+
+interface MissingDataParams {
+  duration: string;
+  limit: string;
+}
+
+export class MissingDataAlert extends BaseAlert {
+  public static paramDetails = {
+    duration: {
+      label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', {
+        defaultMessage: `Notify if 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 any data`,
+      }),
+      type: AlertParamType.Duration,
+    } as CommonAlertParamDetail,
+  };
+
+  public type = ALERT_MISSING_DATA;
+  public label = i18n.translate('xpack.monitoring.alerts.missingData.label', {
+    defaultMessage: 'Missing data',
+  });
+
+  protected defaultParams: MissingDataParams = {
+    duration: DEFAULT_DURATION,
+    limit: DEFAULT_LIMIT,
+  };
+
+  protected actionVariables = [
+    {
+      name: 'internalShortMessage',
+      description: i18n.translate(
+        'xpack.monitoring.alerts.missingData.actionVariables.internalShortMessage',
+        {
+          defaultMessage: 'The short internal message generated by Elastic.',
+        }
+      ),
+    },
+    {
+      name: 'internalFullMessage',
+      description: i18n.translate(
+        'xpack.monitoring.alerts.missingData.actionVariables.internalFullMessage',
+        {
+          defaultMessage: 'The full internal message generated by Elastic.',
+        }
+      ),
+    },
+    {
+      name: 'state',
+      description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.state', {
+        defaultMessage: 'The current state of the alert.',
+      }),
+    },
+    {
+      name: 'stackProducts',
+      description: i18n.translate(
+        'xpack.monitoring.alerts.missingData.actionVariables.stackProducts',
+        {
+          defaultMessage: 'The stack products missing data.',
+        }
+      ),
+    },
+    {
+      name: 'count',
+      description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.count', {
+        defaultMessage: 'The number of stack products missing data.',
+      }),
+    },
+    {
+      name: 'clusterName',
+      description: i18n.translate(
+        'xpack.monitoring.alerts.missingData.actionVariables.clusterName',
+        {
+          defaultMessage: 'The cluster to which the stack products belong.',
+        }
+      ),
+    },
+    {
+      name: 'action',
+      description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.action', {
+        defaultMessage: 'The recommended action for this alert.',
+      }),
+    },
+    {
+      name: 'actionPlain',
+      description: i18n.translate(
+        'xpack.monitoring.alerts.missingData.actionVariables.actionPlain',
+        {
+          defaultMessage: 'The recommended action for this alert, without any markdown.',
+        }
+      ),
+    },
+  ];
+
+  protected async fetchData(
+    params: CommonAlertParams,
+    callCluster: any,
+    clusters: AlertCluster[],
+    uiSettings: IUiSettingsClient,
+    availableCcs: string[]
+  ): Promise<AlertData[]> {
+    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 missingData = await fetchMissingData(
+      callCluster,
+      clusters,
+      indexPattern,
+      limit,
+      this.config.ui.max_bucket_size
+    );
+    return missingData.map((missing) => {
+      return {
+        instanceKey: `${missing.clusterUuid}:${missing.stackProduct}:${missing.stackProductUuid}`,
+        clusterUuid: missing.clusterUuid,
+        shouldFire: missing.gapDuration > duration,
+        severity: AlertSeverity.Danger,
+        meta: missing,
+        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 filter = _filter as CommonAlertCpuUsageFilter;
+    //     if (filter && filter.nodeUuid) {
+    //       let nodeExistsInStates = false;
+    //       for (const state of alertInstanceState.alertStates) {
+    //         if ((state as AlertMissingDataState).nodeId === filter.nodeUuid) {
+    //           nodeExistsInStates = true;
+    //           break;
+    //         }
+    //       }
+    //       if (!nodeExistsInStates) {
+    //         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 = item.meta as AlertMissingData;
+    if (!alertState.ui.isFiring) {
+      return {
+        text: i18n.translate('xpack.monitoring.alerts.missingData.ui.resolvedMessage', {
+          defaultMessage: `We are no longer detecting that monitoring data is missing for {stackProduct}:{stackProductName}, as of #resolved`,
+          values: {
+            stackProduct: missing.stackProduct,
+            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 {stackProduct}:{stackProductName}, starting at #absolute`,
+        values: {
+          gapDuration: moment.duration(missing.gapDuration, 'milliseconds').humanize(),
+          stackProduct: missing.stackProduct,
+          stackProductName: missing.stackProductName,
+        },
+      }),
+      nextSteps: [
+        {
+          text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.hotThreads', {
+            defaultMessage: `#start_linkCheck hot threads#end_link`,
+          }),
+          tokens: [
+            {
+              startToken: '#start_link',
+              endToken: '#end_link',
+              type: AlertMessageTokenType.DocLink,
+              partialUrl: `{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html`,
+            } as AlertMessageDocLinkToken,
+          ],
+        },
+        {
+          text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.runningTasks', {
+            defaultMessage: `#start_linkCheck long running tasks#end_link`,
+          }),
+          tokens: [
+            {
+              startToken: '#start_link',
+              endToken: '#end_link',
+              type: AlertMessageTokenType.DocLink,
+              partialUrl: `{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html`,
+            } as AlertMessageDocLinkToken,
+          ],
+        },
+      ],
+      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 `${state.stackProduct}:${state.stackProductUuid}`;
+      })
+      .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 are not detecting 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 are not detecting 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 resolvedNodes = instanceState.alertStates
+      //   .filter((_state) => !(_state as AlertMissingDataState).ui.isFiring)
+      //   .map((_state) => {
+      //     const state = _state as AlertMissingDataState;
+      //     return `${state.stackProduct}:${state.stackProductUuid}`;
+      //   })
+      //   .join(',');
+      // if (resolvedCount > 0) {
+      //   instance.scheduleActions('default', {
+      //     internalShortMessage: i18n.translate(
+      //       'xpack.monitoring.alerts.missingData.resolved.internalShortMessage',
+      //       {
+      //         defaultMessage: `CPU usage alert is resolved for {count} node(s) in cluster: {clusterName}.`,
+      //         values: {
+      //           count: resolvedCount,
+      //           clusterName: cluster.clusterName,
+      //         },
+      //       }
+      //     ),
+      //     internalFullMessage: i18n.translate(
+      //       'xpack.monitoring.alerts.missingData.resolved.internalFullMessage',
+      //       {
+      //         defaultMessage: `CPU usage alert is resolved for {count} node(s) in cluster: {clusterName}.`,
+      //         values: {
+      //           count: resolvedCount,
+      //           clusterName: cluster.clusterName,
+      //         },
+      //       }
+      //     ),
+      //     state: RESOLVED,
+      //     nodes: resolvedNodes,
+      //     count: resolvedCount,
+      //     clusterName: cluster.clusterName,
+      //   });
+      // }
+    }
+  }
+
+  protected 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 AlertMissingData;
+        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 AlertMissingData;
+        let state: AlertMissingDataState;
+        const indexInState = alertInstanceState.alertStates.findIndex((alertState) => {
+          const _alertState = alertState as AlertMissingDataState;
+          return (
+            _alertState.cluster.clusterUuid === cluster.clusterUuid &&
+            _alertState.stackProduct === (stackProduct.meta as AlertMissingData).stackProduct &&
+            _alertState.stackProductUuid ===
+              (stackProduct.meta as AlertMissingData).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.gapDuration = missing.gapDuration;
+
+        if (stackProduct.shouldFire) {
+          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 b6c8427375841..c84618f775ec4 100644
--- a/x-pack/plugins/monitoring/server/alerts/types.d.ts
+++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts
@@ -27,6 +27,12 @@ export interface AlertCpuUsageState extends AlertState {
   nodeName: string;
 }
 
+export interface AlertMissingDataState extends AlertState {
+  stackProduct: string;
+  stackProductUuid: string;
+  gapDuration: number;
+}
+
 export interface AlertUiState {
   isFiring: boolean;
   severity: AlertSeverity;
@@ -78,6 +84,15 @@ export interface AlertCpuUsageNodeStats {
   ccs: string | null;
 }
 
+export interface AlertMissingData {
+  stackProduct: string;
+  stackProductUuid: string;
+  stackProductName: string;
+  clusterUuid: string;
+  gapDuration: number;
+  ccs: string | null;
+}
+
 export interface AlertData {
   instanceKey: string;
   clusterUuid: string;
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
new file mode 100644
index 0000000000000..d58665de23c32
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
@@ -0,0 +1,291 @@
+/*
+ * 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 moment from 'moment';
+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 IndexBucketESResponse {
+  key: string;
+  clusters: {
+    buckets: ClusterBucketESResponse[];
+  };
+}
+
+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;
+  top: {
+    hits: {
+      hits: TopHitESResponse[];
+    };
+  };
+}
+
+interface TopHitESResponse {
+  _source: {
+    timestamp: string;
+  };
+}
+
+function findNonEmptyBucket(bucket: ClusterBucketESResponse): UuidResponse {
+  if (bucket.beats.beats_uuids.buckets.length > 0) {
+    return bucket.beats.beats_uuids;
+  }
+  if (bucket.apms.apm_uuids.buckets.length > 0) {
+    return bucket.apms.apm_uuids;
+  }
+  if (bucket.kibana_uuids.buckets.length > 0) {
+    return bucket.kibana_uuids;
+  }
+  if (bucket.logstash_uuids.buckets.length > 0) {
+    return bucket.logstash_uuids;
+  }
+  if (bucket.es_uuids.buckets.length > 0) {
+    return bucket.es_uuids;
+  }
+  return { buckets: [] };
+}
+
+function getStackProductFromIndex(index: string, bucket: ClusterBucketESResponse) {
+  if (index.includes('-kibana-')) {
+    return KIBANA_SYSTEM_ID;
+  }
+  if (index.includes('-beats-')) {
+    if (bucket.apms.apm_uuids.buckets.length > 0) {
+      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 fetchMissingData(
+  callCluster: any,
+  clusters: AlertCluster[],
+  index: string,
+  limit: number,
+  size: number
+): Promise<AlertMissingData[]> {
+  const endMs = +new Date();
+  const startMs = endMs - limit;
+
+  const nameFields = [
+    'source_node.name',
+    'kibana_stats.kibana.name',
+    'logstash_stats.logstash.host',
+    'beats_stats.beat.name',
+  ];
+  const topHitsAgg = {
+    top: {
+      top_hits: {
+        size: 2,
+        sort: [
+          {
+            timestamp: {
+              order: 'desc',
+            },
+          },
+        ],
+        _source: {
+          includes: ['timestamp', ...nameFields],
+        },
+      },
+    },
+    top_hit: {
+      max: {
+        field: 'timestamp',
+      },
+    },
+  };
+
+  const params = {
+    index,
+    filterPath: ['aggregations.index.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: {
+        index: {
+          terms: {
+            field: '_index',
+            size,
+          },
+          aggs: {
+            clusters: {
+              terms: {
+                field: 'cluster_uuid',
+                size,
+              },
+              aggs: {
+                es_uuids: {
+                  terms: {
+                    field: 'node_stats.node_id',
+                    size,
+                    order: { top_hit: 'desc' },
+                  },
+                  aggs: topHitsAgg,
+                },
+                kibana_uuids: {
+                  terms: {
+                    field: 'kibana_stats.kibana.uuid',
+                    size,
+                    order: { top_hit: 'desc' },
+                  },
+                  aggs: topHitsAgg,
+                },
+                beats: {
+                  filter: {
+                    bool: {
+                      must_not: {
+                        term: {
+                          'beats_stats.beat.type': 'apm-server',
+                        },
+                      },
+                    },
+                  },
+                  aggs: {
+                    beats_uuids: {
+                      terms: {
+                        field: 'beats_stats.beat.uuid',
+                        size,
+                        order: { top_hit: 'desc' },
+                      },
+                      aggs: topHitsAgg,
+                    },
+                  },
+                },
+                apms: {
+                  filter: {
+                    bool: {
+                      must: {
+                        term: {
+                          'beats_stats.beat.type': 'apm-server',
+                        },
+                      },
+                    },
+                  },
+                  aggs: {
+                    apm_uuids: {
+                      terms: {
+                        field: 'beats_stats.beat.uuid',
+                        size,
+                        order: { top_hit: 'desc' },
+                      },
+                      aggs: topHitsAgg,
+                    },
+                  },
+                },
+                logstash_uuids: {
+                  terms: {
+                    field: 'logstash_stats.logstash.uuid',
+                    size,
+                    order: { top_hit: 'desc' },
+                  },
+                  aggs: topHitsAgg,
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const response = await callCluster('search', params);
+  const indexBuckets = get(response, 'aggregations.index.buckets', []) as IndexBucketESResponse[];
+  const uniqueList: {
+    [id: string]: AlertMissingData;
+  } = {};
+  for (const indexBucket of indexBuckets) {
+    const clusterBuckets = indexBucket.clusters.buckets;
+    for (const clusterBucket of clusterBuckets) {
+      const clusterUuid = clusterBucket.key;
+      const uuidBuckets = findNonEmptyBucket(clusterBucket).buckets;
+      for (const uuidBucket of uuidBuckets) {
+        const stackProductUuid = uuidBucket.key;
+        const stackProduct = getStackProductFromIndex(indexBucket.key, clusterBucket);
+        let differenceInMs = -1;
+        if (uuidBucket.top.hits.hits.length === 0) {
+          differenceInMs = -2;
+        } else if (uuidBucket.top.hits.hits.length === 1) {
+          differenceInMs = moment().diff(moment(uuidBucket.top.hits.hits[0]._source.timestamp));
+        } else {
+          const first = moment(uuidBucket.top.hits.hits[0]._source.timestamp);
+          const second = moment(uuidBucket.top.hits.hits[1]._source.timestamp);
+          differenceInMs = first.diff(second);
+        }
+
+        let stackProductName = stackProductUuid;
+        for (const nameField of nameFields) {
+          stackProductName = get(uuidBucket, `top.hits.hits[0]._source.${nameField}`);
+          if (stackProductName) {
+            break;
+          }
+        }
+
+        uniqueList[`${clusterUuid}::${stackProduct}::${stackProductUuid}`] = {
+          stackProduct,
+          stackProductUuid,
+          stackProductName,
+          clusterUuid,
+          gapDuration: differenceInMs,
+          ccs: indexBucket.key.includes(':') ? indexBucket.key.split(':')[0] : null,
+        };
+      }
+    }
+  }
+
+  const missingData = Object.values(uniqueList);
+  return missingData;
+}

From 2f496f13ea3709db936292c671083b2ad0306ed2 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Tue, 22 Sep 2020 15:48:59 -0400
Subject: [PATCH 02/16] Surface alert most places

---
 x-pack/plugins/monitoring/common/types.ts     |   6 +-
 .../monitoring/public/alerts/badge.tsx        |  15 +-
 .../monitoring/public/alerts/callout.tsx      |  15 +-
 .../monitoring/public/alerts/panel.tsx        |  10 +-
 .../monitoring/public/alerts/status.tsx       |  15 +-
 .../components/beats/listing/listing.js       |  39 ++-
 .../public/components/beats/stats.js          |   4 +-
 .../components/elasticsearch/node/node.js     |  11 +-
 .../components/elasticsearch/nodes/nodes.js   |  20 +-
 .../components/kibana/instances/instances.js  |  13 +-
 .../components/logstash/listing/listing.js    |  13 +-
 .../public/views/beats/beat/index.js          |  13 +-
 .../public/views/beats/listing/index.js       |  17 +-
 .../elasticsearch/node/advanced/index.js      |   8 +-
 .../public/views/elasticsearch/node/index.js  |   8 +-
 .../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 +-
 .../monitoring/server/alerts/base_alert.ts    |  17 +-
 .../server/alerts/cpu_usage_alert.ts          |   4 +-
 .../server/alerts/missing_data_alert.ts       | 234 +++++++++++-------
 .../monitoring/server/alerts/types.d.ts       |   1 +
 .../server/lib/alerts/fetch_missing_data.ts   |   6 +-
 .../get_listing_link_for_stack_product.ts     |  28 +++
 .../lib/alerts/get_stack_product_label.ts     |  17 ++
 .../get_type_label_for_stack_product.ts       |  51 ++++
 29 files changed, 524 insertions(+), 141 deletions(-)
 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/types.ts b/x-pack/plugins/monitoring/common/types.ts
index f5dc85dce32e1..9fe71eac93b87 100644
--- a/x-pack/plugins/monitoring/common/types.ts
+++ b/x-pack/plugins/monitoring/common/types.ts
@@ -30,10 +30,14 @@ export interface CommonAlertState {
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
 export interface CommonAlertFilter {}
 
-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: Props) => {
-  const { stateFilter = () => true } = props;
+  const { stateFilter = () => true, nextStepsFilter = () => true } = props;
   const [showPopover, setShowPopover] = React.useState<AlertSeverity | boolean | null>(null);
   const inSetupMode = isInSetupMode();
   const alerts = Object.values(props.alerts).filter(Boolean);
@@ -80,7 +81,7 @@ export const AlertsBadge: React.FC<Props> = (props: Props) => {
           id: index + 1,
           title: alertStatus.alert.label,
           width: 400,
-          content: <AlertPanel alert={alertStatus} />,
+          content: <AlertPanel alert={alertStatus} nextStepsFilter={nextStepsFilter} />,
         };
       }),
     ];
@@ -158,7 +159,13 @@ export const AlertsBadge: React.FC<Props> = (props: Props) => {
             id: index + 1,
             title: getDateFromState(alertStatus.alertState),
             width: 400,
-            content: <AlertPanel alert={alertStatus.alert} alertState={alertStatus.alertState} />,
+            content: (
+              <AlertPanel
+                alert={alertStatus.alert}
+                alertState={alertStatus.alertState}
+                nextStepsFilter={nextStepsFilter}
+              />
+            ),
           };
         }),
       ];
diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx
index cad98dd1e6aec..bc9c5e5633ad1 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: Props) => {
-  const { alerts, stateFilter = () => true } = props;
+  const { alerts, stateFilter = () => true, nextStepsFilter = () => true } = props;
 
   const callouts = TYPES.map((type) => {
     const list = [];
@@ -49,18 +50,18 @@ export const AlertsCallout: React.FC<Props> = (props: Props) => {
 
     if (list.length) {
       return (
-        <Fragment>
+        <div key={type}>
           <EuiCallOut title={type.label} color={type.severity} iconType="bell">
             <ul>
               {list.map((state, index) => {
                 const nextStepsUi =
                   state.ui.message.nextSteps && state.ui.message.nextSteps.length ? (
                     <ul>
-                      {state.ui.message.nextSteps.map(
-                        (step: AlertMessage, nextStepIndex: number) => (
+                      {state.ui.message.nextSteps
+                        .filter(nextStepsFilter)
+                        .map((step: AlertMessage, nextStepIndex: number) => (
                           <li key={nextStepIndex}>{replaceTokens(step)}</li>
-                        )
-                      )}
+                        ))}
                     </ul>
                   ) : null;
 
@@ -74,7 +75,7 @@ export const AlertsCallout: React.FC<Props> = (props: Props) => {
             </ul>
           </EuiCallOut>
           <EuiSpacer />
-        </Fragment>
+        </div>
       );
     }
   });
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: 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: Props) => {
   const nextStepsUi =
     alertState.state.ui.message.nextSteps && alertState.state.ui.message.nextSteps.length ? (
       <EuiListGroup>
-        {alertState.state.ui.message.nextSteps.map((step: AlertMessage, index: number) => (
-          <EuiListGroupItem size="s" key={index} label={replaceTokens(step)} />
-        ))}
+        {alertState.state.ui.message.nextSteps
+          .filter(nextStepsFilter)
+          .map((step: AlertMessage, index: number) => (
+            <EuiListGroupItem size="s" key={index} label={replaceTokens(step)} />
+          ))}
       </EuiListGroup>
     ) : 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: 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: Props) => {
   }
 
   if (showBadge || inSetupMode) {
-    return <AlertsBadge alerts={alerts} stateFilter={stateFilter} />;
+    return (
+      <AlertsBadge alerts={alerts} stateFilter={stateFilter} nextStepsFilter={nextStepsFilter} />
+    );
   }
 
   const severity = atLeastOneDanger ? AlertSeverity.Danger : AlertSeverity.Warning;
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 be8595e8e6bbe..c373794ac9980 100644
--- a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js
+++ b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js
@@ -25,10 +25,13 @@ import { SetupModeBadge } from '../../setup_mode/badge';
 import { FormattedMessage } from '@kbn/i18n/react';
 import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
 import { SetupModeFeature } from '../../../../common/enums';
+import { AlertsCallout } from '../../../alerts/callout';
+import { AlertsStatus } from '../../../alerts/status';
 
 export class Listing extends PureComponent {
   getColumns() {
     const setupMode = this.props.setupMode;
+    const alerts = this.props.alerts;
 
     return [
       {
@@ -71,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 (
+            <AlertsStatus
+              showBadge={true}
+              alerts={alerts}
+              stateFilter={(state) => 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',
@@ -121,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)) {
@@ -154,9 +180,18 @@ export class Listing extends PureComponent {
             </h1>
           </EuiScreenReaderOnly>
           <EuiPageContent>
-            <Stats stats={stats} />
+            <Stats stats={stats} alerts={alerts} />
             <EuiSpacer size="m" />
             {setupModeCallOut}
+            <AlertsCallout
+              alerts={alerts}
+              nextStepsFilter={(nextStep) => {
+                if (nextStep.text.includes('Beat beats')) {
+                  return false;
+                }
+                return true;
+              }}
+            />
             <EuiMonitoringTable
               className="beatsTable"
               rows={data}
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 <SummaryStatus metrics={metrics} data-test-subj="beatsSummaryStatus" />;
+  return <SummaryStatus metrics={metrics} alerts={alerts} data-test-subj="beatsSummaryStatus" />;
 }
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..17802e64d9199 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js
@@ -77,7 +77,16 @@ export const Node = ({
           />
         </EuiPanel>
         <EuiSpacer size="m" />
-        <AlertsCallout alerts={alerts} stateFilter={(state) => state.nodeId === nodeId} />
+        <AlertsCallout
+          alerts={alerts}
+          stateFilter={(state) => state.nodeId === nodeId || state.stackProductUuid === nodeId}
+          nextStepsFilter={(nextStep) => {
+            if (nextStep.text.includes('Elasticsearch nodes')) {
+              return false;
+            }
+            return true;
+          }}
+        />
         <EuiPageContent>
           <EuiFlexGrid columns={2} gutterSize="s">
             {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 43512f8e528f6..d281d7456add6 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
@@ -34,6 +34,7 @@ import { ListingCallOut } from '../../setup_mode/listing_callout';
 import { AlertsStatus } from '../../../alerts/status';
 import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
 import { SetupModeFeature } from '../../../../common/enums';
+import { AlertsCallout } from '../../../alerts/callout';
 
 const getNodeTooltip = (node) => {
   const { nodeTypeLabel, nodeTypeClass } = node;
@@ -138,7 +139,15 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler
         <AlertsStatus
           showBadge={true}
           alerts={alerts}
-          stateFilter={(state) => 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;
+          }}
         />
       );
     },
@@ -459,6 +468,15 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear
         </EuiScreenReaderOnly>
         {renderClusterStatus()}
         {setupModeCallout}
+        <AlertsCallout
+          alerts={alerts}
+          nextStepsFilter={(nextStep) => {
+            if (nextStep.text.includes('Elasticsearch nodes')) {
+              return false;
+            }
+            return true;
+          }}
+        />
         <EuiPageContent>
           <EuiMonitoringSSPTable
             className="elasticsearchNodesTable"
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 <AlertsStatus showBadge={true} alerts={alerts} />;
+        return (
+          <AlertsStatus
+            showBadge={true}
+            alerts={alerts}
+            nextStepsFilter={(nextStep) => {
+              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 <AlertsStatus showBadge={true} alerts={alerts} />;
+          return (
+            <AlertsStatus
+              showBadge={true}
+              alerts={alerts}
+              nextStepsFilter={(nextStep) => {
+                if (nextStep.text.includes('Logstash nodes')) {
+                  return false;
+                }
+                return true;
+              }}
+            />
+          );
         },
       },
       {
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 8b5aeb016080f..03debae53989d 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,7 @@ 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_DATA, BEATS_SYSTEM_ID } from '../../../../common/constants';
 
 uiRoutes.when('/beats/beat/:beatUuid', {
   template,
@@ -43,6 +43,17 @@ uiRoutes.when('/beats/beat/:beatUuid', {
         getPageData,
         $scope,
         $injector,
+        alerts: {
+          shouldFetch: true,
+          options: {
+            alertTypeIds: [ALERT_MISSING_DATA],
+            filters: [
+              {
+                stackProduct: BEATS_SYSTEM_ID,
+              },
+            ],
+          },
+        },
       });
 
       this.data = pageData;
diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/plugins/monitoring/public/views/beats/listing/index.js
index f82e0a437bd0b..336648f3e86d0 100644
--- a/x-pack/plugins/monitoring/public/views/beats/listing/index.js
+++ b/x-pack/plugins/monitoring/public/views/beats/listing/index.js
@@ -14,7 +14,7 @@ import template from './index.html';
 import React, { Fragment } from 'react';
 import { Listing } from '../../../components/beats/listing/listing';
 import { SetupModeRenderer } from '../../../components/renderers';
-import { CODE_PATH_BEATS, BEATS_SYSTEM_ID } from '../../../../common/constants';
+import { CODE_PATH_BEATS, BEATS_SYSTEM_ID, ALERT_MISSING_DATA } from '../../../../common/constants';
 
 uiRoutes.when('/beats/beats', {
   template,
@@ -42,15 +42,23 @@ uiRoutes.when('/beats/beats', {
         reactNodeId: 'monitoringBeatsInstancesApp',
         $scope,
         $injector,
+        alerts: {
+          shouldFetch: true,
+          options: {
+            alertTypeIds: [ALERT_MISSING_DATA],
+            filters: [
+              {
+                stackProduct: BEATS_SYSTEM_ID,
+              },
+            ],
+          },
+        },
       });
 
       this.data = $route.current.locals.pageData;
       this.scope = $scope;
       this.injector = $injector;
 
-      //Bypassing super.updateData, since this controller loads its own data
-      this._isDataInitialized = true;
-
       $scope.$watch(
         () => this.data,
         () => this.renderComponent()
@@ -70,6 +78,7 @@ uiRoutes.when('/beats/beats', {
               <Listing
                 stats={this.data.stats}
                 data={this.data.listing}
+                alerts={this.alerts}
                 setupMode={setupMode}
                 sorting={this.sorting || sorting}
                 pagination={this.pagination || pagination}
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 d75f63d883315..6c45cd1cf666e 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
@@ -16,7 +16,11 @@ import template from './index.html';
 import { Legacy } from '../../../../legacy_shims';
 import { AdvancedNode } from '../../../../components/elasticsearch/node/advanced';
 import { MonitoringViewBaseController } from '../../../base_controller';
-import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../../../../common/constants';
+import {
+  CODE_PATH_ELASTICSEARCH,
+  ALERT_CPU_USAGE,
+  ALERT_MISSING_DATA,
+} from '../../../../../common/constants';
 
 function getPageData($injector) {
   const $http = $injector.get('$http');
@@ -65,7 +69,7 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_CPU_USAGE],
+            alertTypeIds: [ALERT_CPU_USAGE, ALERT_MISSING_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 f6f7a01690529..f7bb864c11b7d 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
@@ -18,7 +18,11 @@ import { Node } from '../../../components/elasticsearch/node/node';
 import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels';
 import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices';
 import { MonitoringViewBaseController } from '../../base_controller';
-import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../../../common/constants';
+import {
+  CODE_PATH_ELASTICSEARCH,
+  ALERT_CPU_USAGE,
+  ALERT_MISSING_DATA,
+} from '../../../../common/constants';
 
 uiRoutes.when('/elasticsearch/nodes/:node', {
   template,
@@ -50,7 +54,7 @@ uiRoutes.when('/elasticsearch/nodes/:node', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_CPU_USAGE],
+            alertTypeIds: [ALERT_CPU_USAGE, ALERT_MISSING_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 152ccf440cd54..674c6d158d0bd 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_DATA,
 } from '../../../../common/constants';
 
 uiRoutes.when('/elasticsearch/nodes', {
@@ -83,7 +84,12 @@ uiRoutes.when('/elasticsearch/nodes', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_CPU_USAGE],
+            alertTypeIds: [ALERT_CPU_USAGE, ALERT_MISSING_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 a7cb6c8094f74..b90e60eecd44b 100644
--- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js
+++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js
@@ -26,7 +26,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_DATA,
+  KIBANA_SYSTEM_ID,
+} from '../../../../common/constants';
 import { AlertsCallout } from '../../../alerts/callout';
 
 function getPageData($injector) {
@@ -74,7 +79,12 @@ uiRoutes.when('/kibana/instances/:uuid', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH],
+            alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_DATA],
+            filters: [
+              {
+                stackProduct: KIBANA_SYSTEM_ID,
+              },
+            ],
           },
         },
       });
@@ -95,7 +105,15 @@ uiRoutes.when('/kibana/instances/:uuid', {
                   <DetailStatus stats={data.kibanaSummary} />
                 </EuiPanel>
                 <EuiSpacer size="m" />
-                <AlertsCallout alerts={this.alerts} />
+                <AlertsCallout
+                  alerts={this.alerts}
+                  nextStepsFilter={(nextStep) => {
+                    if (nextStep.text.includes('Kibana instances')) {
+                      return false;
+                    }
+                    return true;
+                  }}
+                />
                 <EuiPageContent>
                   <EuiFlexGrid columns={2} gutterSize="s">
                     <EuiFlexItem grow={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 7106da0fdabd3..785a3ea575d55 100644
--- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js
+++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js
@@ -16,6 +16,7 @@ import {
   KIBANA_SYSTEM_ID,
   CODE_PATH_KIBANA,
   ALERT_KIBANA_VERSION_MISMATCH,
+  ALERT_MISSING_DATA,
 } from '../../../../common/constants';
 
 uiRoutes.when('/kibana/instances', {
@@ -40,7 +41,12 @@ uiRoutes.when('/kibana/instances', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH],
+            alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_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 de4cc1eb7e2d1..f7d2b5d014ea0 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_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_DATA],
+            filters: [
+              {
+                stackProduct: LOGSTASH_SYSTEM_ID,
+              },
+            ],
+          },
+        },
       });
 
       $scope.$watch(
@@ -102,6 +119,15 @@ uiRoutes.when('/logstash/node/:uuid/advanced', {
                   <DetailStatus stats={data.nodeSummary} />
                 </EuiPanel>
                 <EuiSpacer size="m" />
+                <AlertsCallout
+                  alerts={this.alerts}
+                  nextStepsFilter={(nextStep) => {
+                    if (nextStep.text.includes('Logstash nodes')) {
+                      return false;
+                    }
+                    return true;
+                  }}
+                />
                 <EuiPageContent>
                   <EuiFlexGrid columns={2} gutterSize="s">
                     {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 9138a85e58517..4dfb7e61da37e 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_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_DATA],
+            filters: [
+              {
+                stackProduct: LOGSTASH_SYSTEM_ID,
+              },
+            ],
           },
         },
       });
@@ -110,7 +120,15 @@ uiRoutes.when('/logstash/node/:uuid', {
                   <DetailStatus stats={data.nodeSummary} />
                 </EuiPanel>
                 <EuiSpacer size="m" />
-                <AlertsCallout alerts={this.alerts} />
+                <AlertsCallout
+                  alerts={this.alerts}
+                  nextStepsFilter={(nextStep) => {
+                    if (nextStep.text.includes('Logstash nodes')) {
+                      return false;
+                    }
+                    return true;
+                  }}
+                />
                 <EuiPageContent>
                   <EuiFlexGrid columns={2} gutterSize="s">
                     {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 563d04af55bb2..569e2f70d13fb 100644
--- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js
+++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js
@@ -15,6 +15,7 @@ import {
   CODE_PATH_LOGSTASH,
   LOGSTASH_SYSTEM_ID,
   ALERT_LOGSTASH_VERSION_MISMATCH,
+  ALERT_MISSING_DATA,
 } from '../../../../common/constants';
 
 uiRoutes.when('/logstash/nodes', {
@@ -39,7 +40,12 @@ uiRoutes.when('/logstash/nodes', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH],
+            alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_DATA],
+            filters: [
+              {
+                stackProduct: LOGSTASH_SYSTEM_ID,
+              },
+            ],
           },
         },
       });
diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
index f583b4882f83c..4d3776b7a96e8 100644
--- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
@@ -197,7 +197,18 @@ export class BaseAlert {
         }
         const alertInstance: RawAlertInstance = states.alertInstances[instanceId];
         if (alertInstance && this.filterAlertInstance(alertInstance, filters)) {
-          accum[instanceId] = alertInstance;
+          accum[instanceId] = {
+            ...alertInstance,
+            state: alertInstance.state
+              ? {
+                  alertStates: (alertInstance.state as AlertInstanceState).alertStates.filter(
+                    (alertState: AlertState) => {
+                      return this.filterAlertState(alertState, filters);
+                    }
+                  ),
+                }
+              : alertInstance.state,
+          };
         }
         return accum;
       },
@@ -209,6 +220,10 @@ export class BaseAlert {
     return true;
   }
 
+  protected filterAlertState(alertState: AlertState, filters: CommonAlertFilter[]) {
+    return true;
+  }
+
   protected async execute({ services, params, state }: AlertExecutorOptions): Promise<any> {
     const logger = this.getLogger(this.type);
     logger.debug(
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 5bca84e33da3c..6ed3048e94533 100644
--- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
@@ -27,7 +27,7 @@ import { RawAlertInstance } from '../../../alerts/common';
 import { parseDuration } from '../../../alerts/common/parse_duration';
 import {
   CommonAlertFilter,
-  CommonAlertCpuUsageFilter,
+  CommonAlertNodeUuidFilter,
   CommonAlertParams,
   CommonAlertParamDetail,
 } from '../../common/types';
@@ -177,7 +177,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/missing_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
index 5e21855f14698..2636e0653c8f9 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
@@ -16,7 +16,7 @@ import {
   AlertMissingData,
   AlertMessageTimeToken,
   AlertInstanceState,
-  AlertMessageDocLinkToken,
+  AlertMessageLinkToken,
 } from './types';
 import { AlertInstance, AlertServices } from '../../../alerts/server';
 import { INDEX_PATTERN, ALERT_MISSING_DATA } from '../../common/constants';
@@ -24,13 +24,22 @@ 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 } from '../../common/types';
+import {
+  CommonAlertFilter,
+  CommonAlertParams,
+  CommonAlertParamDetail,
+  CommonAlertStackProductFilter,
+  CommonAlertNodeUuidFilter,
+} from '../../common/types';
 import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
 import { fetchMissingData } from '../lib/alerts/fetch_missing_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';
 
-// const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', {
-//   defaultMessage: 'resolved',
-// });
+const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', {
+  defaultMessage: 'resolved',
+});
 const FIRING = i18n.translate('xpack.monitoring.alerts.missingData.firing', {
   defaultMessage: 'firing',
 });
@@ -159,33 +168,55 @@ export class MissingDataAlert extends BaseAlert {
       return {
         instanceKey: `${missing.clusterUuid}:${missing.stackProduct}:${missing.stackProductUuid}`,
         clusterUuid: missing.clusterUuid,
-        shouldFire: missing.gapDuration > duration,
+        shouldFire: missing.gapDuration > duration && missing.gapDuration <= limit,
         severity: AlertSeverity.Danger,
-        meta: missing,
+        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 filter = _filter as CommonAlertCpuUsageFilter;
-    //     if (filter && filter.nodeUuid) {
-    //       let nodeExistsInStates = false;
-    //       for (const state of alertInstanceState.alertStates) {
-    //         if ((state as AlertMissingDataState).nodeId === filter.nodeUuid) {
-    //           nodeExistsInStates = true;
-    //           break;
-    //         }
-    //       }
-    //       if (!nodeExistsInStates) {
-    //         return false;
-    //       }
-    //     }
-    //   }
-    // }
+    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;
   }
 
@@ -201,13 +232,30 @@ export class MissingDataAlert extends BaseAlert {
   }
 
   protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage {
-    const missing = item.meta as AlertMissingData;
+    const { missing, limit } = item.meta as { missing: AlertMissingData; limit: number };
+    const globalState = [`cluster_uuid:${item.clusterUuid}`];
+    if (item.ccs) {
+      globalState.push(`ccs:${item.ccs}`);
+    }
     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 no longer detecting that monitoring data is missing for {stackProduct}:{stackProductName}, as of #resolved`,
+          defaultMessage: `We are now seeing monitoring data for the {stackProduct} {type}: {stackProductName}, as of #resolved`,
           values: {
-            stackProduct: missing.stackProduct,
+            stackProduct: getStackProductLabel(missing.stackProduct),
+            type: getTypeLabelForStackProduct(missing.stackProduct, false),
             stackProductName: missing.stackProductName,
           },
         }),
@@ -224,39 +272,41 @@ export class MissingDataAlert extends BaseAlert {
     }
     return {
       text: i18n.translate('xpack.monitoring.alerts.missingData.ui.firingMessage', {
-        defaultMessage: `For the past {gapDuration}, we have not detected any monitoring data from {stackProduct}:{stackProductName}, starting at #absolute`,
+        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: missing.stackProduct,
+          stackProduct: getStackProductLabel(missing.stackProduct),
+          type: getTypeLabelForStackProduct(missing.stackProduct, false),
           stackProductName: missing.stackProductName,
         },
       }),
       nextSteps: [
         {
           text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.hotThreads', {
-            defaultMessage: `#start_linkCheck hot threads#end_link`,
+            defaultMessage: `#start_linkView all {stackProduct} {type}#end_link`,
+            values: {
+              type: getTypeLabelForStackProduct(missing.stackProduct),
+              stackProduct: getStackProductLabel(missing.stackProduct),
+            },
           }),
           tokens: [
             {
               startToken: '#start_link',
               endToken: '#end_link',
-              type: AlertMessageTokenType.DocLink,
-              partialUrl: `{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html`,
-            } as AlertMessageDocLinkToken,
+              type: AlertMessageTokenType.Link,
+              url: `${getListingLinkForStackProduct(missing.stackProduct)}?_g=(${globalState.join(
+                ','
+              )})`,
+            } as AlertMessageLinkToken,
           ],
         },
         {
           text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.runningTasks', {
-            defaultMessage: `#start_linkCheck long running tasks#end_link`,
+            defaultMessage: `Verify monitoring settings on the {type}`,
+            values: {
+              type: getTypeLabelForStackProduct(missing.stackProduct, false),
+            },
           }),
-          tokens: [
-            {
-              startToken: '#start_link',
-              endToken: '#end_link',
-              type: AlertMessageTokenType.DocLink,
-              partialUrl: `{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html`,
-            } as AlertMessageDocLinkToken,
-          ],
         },
       ],
       tokens: [
@@ -294,7 +344,10 @@ export class MissingDataAlert extends BaseAlert {
       .filter((_state) => (_state as AlertMissingDataState).ui.isFiring)
       .map((_state) => {
         const state = _state as AlertMissingDataState;
-        return `${state.stackProduct}:${state.stackProductUuid}`;
+        return `${getStackProductLabel(state.stackProduct)} ${getTypeLabelForStackProduct(
+          state.stackProduct,
+          false
+        )}: ${state.stackProductName}`;
       })
       .join(',');
     if (firingCount > 0) {
@@ -314,7 +367,7 @@ export class MissingDataAlert extends BaseAlert {
       const internalShortMessage = i18n.translate(
         'xpack.monitoring.alerts.missingData.firing.internalShortMessage',
         {
-          defaultMessage: `We are not detecting monitoring data for {count} stack product(s) in cluster: {clusterName}. {shortActionText}`,
+          defaultMessage: `We have not detect any monitoring data for {count} stack product(s) in cluster: {clusterName}. {shortActionText}`,
           values: {
             count: firingCount,
             clusterName: cluster.clusterName,
@@ -325,7 +378,7 @@ export class MissingDataAlert extends BaseAlert {
       const internalFullMessage = i18n.translate(
         'xpack.monitoring.alerts.missingData.firing.internalFullMessage',
         {
-          defaultMessage: `We are not detecting monitoring data for {count} stack product(s) in cluster: {clusterName}. {action}`,
+          defaultMessage: `We have not detect any monitoring data for {count} stack product(s) in cluster: {clusterName}. {action}`,
           values: {
             count: firingCount,
             clusterName: cluster.clusterName,
@@ -344,44 +397,47 @@ export class MissingDataAlert extends BaseAlert {
         actionPlain: shortActionText,
       });
     } else {
-      // const resolvedCount = instanceState.alertStates.filter(
-      //   (alertState) => !alertState.ui.isFiring
-      // ).length;
-      // const resolvedNodes = instanceState.alertStates
-      //   .filter((_state) => !(_state as AlertMissingDataState).ui.isFiring)
-      //   .map((_state) => {
-      //     const state = _state as AlertMissingDataState;
-      //     return `${state.stackProduct}:${state.stackProductUuid}`;
-      //   })
-      //   .join(',');
-      // if (resolvedCount > 0) {
-      //   instance.scheduleActions('default', {
-      //     internalShortMessage: i18n.translate(
-      //       'xpack.monitoring.alerts.missingData.resolved.internalShortMessage',
-      //       {
-      //         defaultMessage: `CPU usage alert is resolved for {count} node(s) in cluster: {clusterName}.`,
-      //         values: {
-      //           count: resolvedCount,
-      //           clusterName: cluster.clusterName,
-      //         },
-      //       }
-      //     ),
-      //     internalFullMessage: i18n.translate(
-      //       'xpack.monitoring.alerts.missingData.resolved.internalFullMessage',
-      //       {
-      //         defaultMessage: `CPU usage alert is resolved for {count} node(s) in cluster: {clusterName}.`,
-      //         values: {
-      //           count: resolvedCount,
-      //           clusterName: cluster.clusterName,
-      //         },
-      //       }
-      //     ),
-      //     state: RESOLVED,
-      //     nodes: resolvedNodes,
-      //     count: resolvedCount,
-      //     clusterName: cluster.clusterName,
-      //   });
-      // }
+      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,
+        });
+      }
     }
   }
 
@@ -398,7 +454,7 @@ export class MissingDataAlert extends BaseAlert {
       }
 
       const firingInstances = stackProducts.reduce((list: string[], stackProduct) => {
-        const missing = stackProduct.meta as AlertMissingData;
+        const { missing } = stackProduct.meta as { missing: AlertMissingData; limit: number };
         if (stackProduct.shouldFire) {
           list.push(`${missing.stackProduct}:${missing.stackProductUuid}`);
         }
@@ -413,15 +469,14 @@ export class MissingDataAlert extends BaseAlert {
       };
       let shouldExecuteActions = false;
       for (const stackProduct of stackProducts) {
-        const missing = stackProduct.meta as AlertMissingData;
+        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 === (stackProduct.meta as AlertMissingData).stackProduct &&
-            _alertState.stackProductUuid ===
-              (stackProduct.meta as AlertMissingData).stackProductUuid
+            _alertState.stackProduct === missing.stackProduct &&
+            _alertState.stackProductUuid === missing.stackProductUuid
           );
         });
         if (indexInState > -1) {
@@ -432,6 +487,7 @@ export class MissingDataAlert extends BaseAlert {
 
         state.stackProduct = missing.stackProduct;
         state.stackProductUuid = missing.stackProductUuid;
+        state.stackProductName = missing.stackProductName;
         state.gapDuration = missing.gapDuration;
 
         if (stackProduct.shouldFire) {
diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts
index c84618f775ec4..610d3dbfdc016 100644
--- a/x-pack/plugins/monitoring/server/alerts/types.d.ts
+++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts
@@ -30,6 +30,7 @@ export interface AlertCpuUsageState extends AlertState {
 export interface AlertMissingDataState extends AlertState {
   stackProduct: string;
   stackProductUuid: string;
+  stackProductName: string;
   gapDuration: number;
 }
 
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
index d58665de23c32..d3b73028a09c4 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
@@ -99,7 +99,7 @@ export async function fetchMissingData(
   size: number
 ): Promise<AlertMissingData[]> {
   const endMs = +new Date();
-  const startMs = endMs - limit;
+  const startMs = endMs - limit - limit * 0.25; // 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 nameFields = [
     'source_node.name',
@@ -256,9 +256,7 @@ export async function fetchMissingData(
         const stackProductUuid = uuidBucket.key;
         const stackProduct = getStackProductFromIndex(indexBucket.key, clusterBucket);
         let differenceInMs = -1;
-        if (uuidBucket.top.hits.hits.length === 0) {
-          differenceInMs = -2;
-        } else if (uuidBucket.top.hits.hits.length === 1) {
+        if (uuidBucket.top.hits.hits.length === 1) {
           differenceInMs = moment().diff(moment(uuidBucket.top.hits.hits[0]._source.timestamp));
         } else {
           const first = moment(uuidBucket.top.hits.hits[0]._source.timestamp);
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';
+}

From 2190c2a8d885f88df2b2ed01661e6447aeedbc5c Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Wed, 23 Sep 2020 14:36:07 -0400
Subject: [PATCH 03/16] Fix up alert placement

---
 .../public/components/beats/beat/beat.js      | 18 +++++++++++++++--
 .../components/beats/listing/listing.js       | 10 ----------
 .../components/beats/overview/overview.js     |  3 ++-
 .../components/elasticsearch/nodes/nodes.js   | 10 ----------
 .../public/views/beats/beat/index.js          |  1 +
 .../public/views/beats/overview/index.js      | 20 +++++++++++++++++--
 6 files changed, 37 insertions(+), 25 deletions(-)

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 }) {
     <EuiPage>
       <EuiPageBody>
         <EuiPanel>
-          <SummaryStatus metrics={summarytStatsTop} data-test-subj="beatSummaryStatus01" />
+          <SummaryStatus
+            metrics={summarytStatsTop}
+            alerts={alerts}
+            data-test-subj="beatSummaryStatus01"
+          />
         </EuiPanel>
         <EuiSpacer size="m" />
         <EuiPanel>
           <SummaryStatus metrics={summarytStatsBot} data-test-subj="beatSummaryStatus02" />
         </EuiPanel>
         <EuiSpacer size="m" />
+        <AlertsCallout
+          alerts={alerts}
+          nextStepsFilter={(nextStep) => {
+            if (nextStep.text.includes('Beat instances')) {
+              return false;
+            }
+            return true;
+          }}
+        />
         <EuiPageContent>
           <EuiScreenReaderOnly>
             <h1>
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 7816e89b3f564..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,7 +26,6 @@ import { SetupModeBadge } from '../../setup_mode/badge';
 import { FormattedMessage } from '@kbn/i18n/react';
 import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
 import { SetupModeFeature } from '../../../../common/enums';
-import { AlertsCallout } from '../../../alerts/callout';
 import { AlertsStatus } from '../../../alerts/status';
 
 export class Listing extends PureComponent {
@@ -186,15 +185,6 @@ export class Listing extends PureComponent {
           <EuiSpacer size="m" />
           <EuiPageContent>
             {setupModeCallOut}
-            <AlertsCallout
-              alerts={alerts}
-              nextStepsFilter={(nextStep) => {
-                if (nextStep.text.includes('Beat beats')) {
-                  return false;
-                }
-                return true;
-              }}
-            />
             <EuiMonitoringTable
               className="beatsTable"
               rows={data}
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({
           </h1>
         </EuiScreenReaderOnly>
         <EuiPanel>
-          <Stats stats={stats} />
+          <Stats stats={stats} alerts={alerts} />
         </EuiPanel>
         <EuiSpacer size="m" />
         <EuiPanel>{renderLatestActive(latestActive, latestTypes, latestVersions)}</EuiPanel>
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 ff5056eab7b66..41d3a579db5a2 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
@@ -34,7 +34,6 @@ import { ListingCallOut } from '../../setup_mode/listing_callout';
 import { AlertsStatus } from '../../../alerts/status';
 import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
 import { SetupModeFeature } from '../../../../common/enums';
-import { AlertsCallout } from '../../../alerts/callout';
 
 const getNodeTooltip = (node) => {
   const { nodeTypeLabel, nodeTypeClass } = node;
@@ -469,15 +468,6 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear
         </EuiScreenReaderOnly>
         {renderClusterStatus()}
         {setupModeCallout}
-        <AlertsCallout
-          alerts={alerts}
-          nextStepsFilter={(nextStep) => {
-            if (nextStep.text.includes('Elasticsearch nodes')) {
-              return false;
-            }
-            return true;
-          }}
-        />
         <EuiPageContent>
           <EuiMonitoringSSPTable
             className="elasticsearchNodesTable"
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 18d42aa75a262..f6a3e7080d708 100644
--- a/x-pack/plugins/monitoring/public/views/beats/beat/index.js
+++ b/x-pack/plugins/monitoring/public/views/beats/beat/index.js
@@ -70,6 +70,7 @@ uiRoutes.when('/beats/beat/:beatUuid', {
         (data) => {
           this.renderReact(
             <Beat
+              alerts={this.alerts}
               summary={data.summary}
               metrics={data.metrics}
               onBrush={$scope.onBrush}
diff --git a/x-pack/plugins/monitoring/public/views/beats/overview/index.js b/x-pack/plugins/monitoring/public/views/beats/overview/index.js
index 6ecdc2bfadb7d..d24b21d0c4b30 100644
--- a/x-pack/plugins/monitoring/public/views/beats/overview/index.js
+++ b/x-pack/plugins/monitoring/public/views/beats/overview/index.js
@@ -11,7 +11,7 @@ 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_DATA, BEATS_SYSTEM_ID } from '../../../../common/constants';
 import { BeatsOverview } from '../../../components/beats/overview';
 
 uiRoutes.when('/beats', {
@@ -44,6 +44,17 @@ uiRoutes.when('/beats', {
         $scope,
         $injector,
         reactNodeId: 'monitoringBeatsOverviewApp',
+        alerts: {
+          shouldFetch: true,
+          options: {
+            alertTypeIds: [ALERT_MISSING_DATA],
+            filters: [
+              {
+                stackProduct: BEATS_SYSTEM_ID,
+              },
+            ],
+          },
+        },
       });
 
       this.data = $route.current.locals.pageData;
@@ -51,7 +62,12 @@ uiRoutes.when('/beats', {
         () => this.data,
         (data) => {
           this.renderReact(
-            <BeatsOverview {...data} onBrush={$scope.onBrush} zoomInfo={$scope.zoomInfo} />
+            <BeatsOverview
+              {...data}
+              alerts={this.alerts}
+              onBrush={$scope.onBrush}
+              zoomInfo={$scope.zoomInfo}
+            />
           );
         }
       );

From b24426903db11441e46b30204b234c1929c64db1 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Wed, 23 Sep 2020 14:44:39 -0400
Subject: [PATCH 04/16] Fix tests

---
 .../server/alerts/alerts_factory.test.ts      |  2 +-
 .../monitoring/server/alerts/base_alert.ts    | 20 +++++++++----------
 2 files changed, 10 insertions(+), 12 deletions(-)

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 d8fa703c7f785..60693eb42a30e 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(7);
+    expect(alerts.length).toBe(8);
   });
 });
diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
index 4d3776b7a96e8..71bf1e843975e 100644
--- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
@@ -197,18 +197,16 @@ export class BaseAlert {
         }
         const alertInstance: RawAlertInstance = states.alertInstances[instanceId];
         if (alertInstance && this.filterAlertInstance(alertInstance, filters)) {
-          accum[instanceId] = {
-            ...alertInstance,
-            state: alertInstance.state
-              ? {
-                  alertStates: (alertInstance.state as AlertInstanceState).alertStates.filter(
-                    (alertState: AlertState) => {
-                      return this.filterAlertState(alertState, filters);
-                    }
-                  ),
+          accum[instanceId] = alertInstance;
+          if (alertInstance.state) {
+            accum[instanceId].state = {
+              alertStates: (alertInstance.state as AlertInstanceState).alertStates.filter(
+                (alertState: AlertState) => {
+                  return this.filterAlertState(alertState, filters);
                 }
-              : alertInstance.state,
-          };
+              ),
+            };
+          }
         }
         return accum;
       },

From 8c245400d21368d16833ef56e5fd2e47beb7f673 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Wed, 23 Sep 2020 15:03:37 -0400
Subject: [PATCH 05/16] Type fix

---
 x-pack/plugins/monitoring/public/alerts/callout.tsx           | 4 ++--
 .../monitoring/public/components/elasticsearch/node/node.js   | 4 +++-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx
index bc9c5e5633ad1..1ddd41c268456 100644
--- a/x-pack/plugins/monitoring/public/alerts/callout.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx
@@ -50,7 +50,7 @@ export const AlertsCallout: React.FC<Props> = (props: Props) => {
 
     if (list.length) {
       return (
-        <div key={type}>
+        <Fragment>
           <EuiCallOut title={type.label} color={type.severity} iconType="bell">
             <ul>
               {list.map((state, index) => {
@@ -75,7 +75,7 @@ export const AlertsCallout: React.FC<Props> = (props: Props) => {
             </ul>
           </EuiCallOut>
           <EuiSpacer />
-        </div>
+        </Fragment>
       );
     }
   });
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 17802e64d9199..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,7 +73,9 @@ export const Node = ({
           <NodeDetailStatus
             stats={nodeSummary}
             alerts={alerts}
-            alertsStateFilter={(state) => state.nodeId === nodeId}
+            alertsStateFilter={(state) =>
+              state.nodeId === nodeId || state.stackProductUuid === nodeId
+            }
           />
         </EuiPanel>
         <EuiSpacer size="m" />

From 740ecf7960786b5184e7ae950071c3f368ff85dd Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Wed, 23 Sep 2020 15:05:20 -0400
Subject: [PATCH 06/16] Update copy

---
 x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
index 2636e0653c8f9..90b73005623d1 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
@@ -367,7 +367,7 @@ export class MissingDataAlert extends BaseAlert {
       const internalShortMessage = i18n.translate(
         'xpack.monitoring.alerts.missingData.firing.internalShortMessage',
         {
-          defaultMessage: `We have not detect any monitoring data for {count} stack product(s) in cluster: {clusterName}. {shortActionText}`,
+          defaultMessage: `We have not detected any monitoring data for {count} stack product(s) in cluster: {clusterName}. {shortActionText}`,
           values: {
             count: firingCount,
             clusterName: cluster.clusterName,
@@ -378,7 +378,7 @@ export class MissingDataAlert extends BaseAlert {
       const internalFullMessage = i18n.translate(
         'xpack.monitoring.alerts.missingData.firing.internalFullMessage',
         {
-          defaultMessage: `We have not detect any monitoring data for {count} stack product(s) in cluster: {clusterName}. {action}`,
+          defaultMessage: `We have not detected any monitoring data for {count} stack product(s) in cluster: {clusterName}. {action}`,
           values: {
             count: firingCount,
             clusterName: cluster.clusterName,

From 8a69629f00110e427ef10938cca2162a92ce2d60 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Thu, 24 Sep 2020 12:55:49 -0400
Subject: [PATCH 07/16] Add alert presence to APM in the UI

---
 .../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/views/apm/instance/index.js        | 35 ++++++----
 .../public/views/apm/instances/index.js       | 66 ++++++++++---------
 .../public/views/apm/overview/index.js        | 27 ++++++--
 8 files changed, 123 insertions(+), 61 deletions(-)

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 }) {
           </h1>
         </EuiScreenReaderOnly>
         <EuiPanel>
-          <Status stats={summary} />
+          <Status stats={summary} alerts={alerts} />
         </EuiPanel>
         <EuiSpacer size="m" />
+        <AlertsCallout
+          alerts={alerts}
+          nextStepsFilter={(nextStep) => {
+            if (nextStep.text.includes('APM servers')) {
+              return false;
+            }
+            return true;
+          }}
+        />
         <EuiPageContent>
           <EuiFlexGroup wrap>{charts}</EuiFlexGroup>
         </EuiPageContent>
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 (
     <SummaryStatus
       metrics={metrics}
+      alerts={alerts}
       IconComponent={IconComponent}
       data-test-subj="apmDetailStatus"
     />
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 (
+          <AlertsStatus
+            showBadge={true}
+            alerts={alerts}
+            stateFilter={(state) => 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 }) {
           </h1>
         </EuiScreenReaderOnly>
         <EuiPanel>
-          <Status stats={data.stats} />
+          <Status stats={data.stats} alerts={alerts} />
         </EuiPanel>
         <EuiSpacer size="m" />
         <EuiPageContent>
@@ -165,7 +189,7 @@ export function ApmServerInstances({ apms, setupMode }) {
           <EuiMonitoringTable
             className="apmInstancesTable"
             rows={data.apms}
-            columns={getColumns(setupMode)}
+            columns={getColumns(alerts, setupMode)}
             sorting={sorting}
             pagination={pagination}
             setupMode={setupMode}
diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/status.js b/x-pack/plugins/monitoring/public/components/apm/instances/status.js
index 818671626ec06..c15e22029d9de 100644
--- a/x-pack/plugins/monitoring/public/components/apm/instances/status.js
+++ b/x-pack/plugins/monitoring/public/components/apm/instances/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 {
     apms: { total },
     totalEvents,
@@ -68,6 +68,7 @@ export function Status({ stats }) {
   return (
     <SummaryStatus
       metrics={metrics}
+      alerts={alerts}
       IconComponent={IconComponent}
       data-test-subj="apmDetailStatus"
     />
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 }) {
           </h1>
         </EuiScreenReaderOnly>
         <EuiPanel>
-          <Status stats={stats} />
+          <Status stats={stats} alerts={alerts} />
         </EuiPanel>
         <EuiSpacer size="m" />
         <EuiPageContent>
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 752c46b18bfb4..052bdde4ba3db 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,7 @@ 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_DATA, APM_SYSTEM_ID } from '../../../../common/constants';
 
 uiRoutes.when('/apm/instances/:uuid', {
   template,
@@ -49,6 +49,17 @@ uiRoutes.when('/apm/instances/:uuid', {
         reactNodeId: 'apmInstanceReact',
         $scope,
         $injector,
+        alerts: {
+          shouldFetch: true,
+          options: {
+            alertTypeIds: [ALERT_MISSING_DATA],
+            filters: [
+              {
+                stackProduct: APM_SYSTEM_ID,
+              },
+            ],
+          },
+        },
       });
 
       $scope.$watch(
@@ -63,21 +74,17 @@ uiRoutes.when('/apm/instances/:uuid', {
             })
           );
           title($scope.cluster, `APM server - ${get(data, 'apmSummary.name')}`);
-          this.renderReact(data);
+          this.renderReact(
+            <ApmServerInstance
+              summary={data.apmSummary || {}}
+              metrics={data.metrics || {}}
+              onBrush={this.onBrush}
+              alerts={this.alerts}
+              zoomInfo={this.zoomInfo}
+            />
+          );
         }
       );
     }
-
-    renderReact(data) {
-      const component = (
-        <ApmServerInstance
-          summary={data.apmSummary || {}}
-          metrics={data.metrics || {}}
-          onBrush={this.onBrush}
-          zoomInfo={this.zoomInfo}
-        />
-      );
-      super.renderReact(component);
-    }
   },
 });
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 764e13ccfea8d..b4f1ee2f185c2 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,7 @@ 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_DATA } from '../../../../common/constants';
 
 uiRoutes.when('/apm/instances', {
   template,
@@ -47,6 +47,17 @@ uiRoutes.when('/apm/instances', {
         reactNodeId: 'apmInstancesReact',
         $scope,
         $injector,
+        alerts: {
+          shouldFetch: true,
+          options: {
+            alertTypeIds: [ALERT_MISSING_DATA],
+            filters: [
+              {
+                stackProduct: APM_SYSTEM_ID,
+              },
+            ],
+          },
+        },
       });
 
       this.scope = $scope;
@@ -55,37 +66,32 @@ uiRoutes.when('/apm/instances', {
       $scope.$watch(
         () => this.data,
         (data) => {
-          this.renderReact(data);
+          const { pagination, sorting, onTableChange } = this;
+          this.renderReact(
+            <SetupModeRenderer
+              scope={this.scope}
+              injector={this.injector}
+              productName={APM_SYSTEM_ID}
+              render={({ setupMode, flyoutComponent, bottomBarComponent }) => (
+                <Fragment>
+                  {flyoutComponent}
+                  <ApmServerInstances
+                    setupMode={setupMode}
+                    alerts={this.alerts}
+                    apms={{
+                      pagination,
+                      sorting,
+                      onTableChange,
+                      data,
+                    }}
+                  />
+                  {bottomBarComponent}
+                </Fragment>
+              )}
+            />
+          );
         }
       );
     }
-
-    renderReact(data) {
-      const { pagination, sorting, onTableChange } = this;
-
-      const component = (
-        <SetupModeRenderer
-          scope={this.scope}
-          injector={this.injector}
-          productName={APM_SYSTEM_ID}
-          render={({ setupMode, flyoutComponent, bottomBarComponent }) => (
-            <Fragment>
-              {flyoutComponent}
-              <ApmServerInstances
-                setupMode={setupMode}
-                apms={{
-                  pagination,
-                  sorting,
-                  onTableChange,
-                  data,
-                }}
-              />
-              {bottomBarComponent}
-            </Fragment>
-          )}
-        />
-      );
-      super.renderReact(component);
-    }
   },
 });
diff --git a/x-pack/plugins/monitoring/public/views/apm/overview/index.js b/x-pack/plugins/monitoring/public/views/apm/overview/index.js
index 670acaeacce03..bb5ea38967b60 100644
--- a/x-pack/plugins/monitoring/public/views/apm/overview/index.js
+++ b/x-pack/plugins/monitoring/public/views/apm/overview/index.js
@@ -12,7 +12,7 @@ import { routeInitProvider } from '../../../lib/route_init';
 import template from './index.html';
 import { MonitoringViewBaseController } from '../../base_controller';
 import { ApmOverview } from '../../../components/apm/overview';
-import { CODE_PATH_APM } from '../../../../common/constants';
+import { CODE_PATH_APM, ALERT_MISSING_DATA, APM_SYSTEM_ID } from '../../../../common/constants';
 
 uiRoutes.when('/apm', {
   template,
@@ -42,19 +42,32 @@ uiRoutes.when('/apm', {
         reactNodeId: 'apmOverviewReact',
         $scope,
         $injector,
+        alerts: {
+          shouldFetch: true,
+          options: {
+            alertTypeIds: [ALERT_MISSING_DATA],
+            filters: [
+              {
+                stackProduct: APM_SYSTEM_ID,
+              },
+            ],
+          },
+        },
       });
 
       $scope.$watch(
         () => this.data,
         (data) => {
-          this.renderReact(data);
+          this.renderReact(
+            <ApmOverview
+              alerts={this.alerts}
+              {...data}
+              onBrush={this.onBrush}
+              zoomInfo={this.zoomInfo}
+            />
+          );
         }
       );
     }
-
-    renderReact(data) {
-      const component = <ApmOverview {...data} onBrush={this.onBrush} zoomInfo={this.zoomInfo} />;
-      super.renderReact(component);
-    }
   },
 });

From 35c77f5100ab8c7a8783dca937ca691ad1ceaed4 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Thu, 24 Sep 2020 12:56:23 -0400
Subject: [PATCH 08/16] Fetch data a little differently

---
 .../server/alerts/cpu_usage_alert.ts          |  4 +-
 .../server/alerts/missing_data_alert.ts       |  4 +-
 .../server/lib/alerts/fetch_missing_data.ts   | 67 ++++++++++---------
 3 files changed, 43 insertions(+), 32 deletions(-)

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 6ed3048e94533..772afab7ce012 100644
--- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
@@ -443,7 +443,9 @@ export class CpuUsageAlert extends BaseAlert {
         nodeState.nodeName = stat.nodeName;
 
         if (node.shouldFire) {
-          nodeState.ui.triggeredMS = new Date().valueOf();
+          if (!nodeState.ui.isFiring) {
+            nodeState.ui.triggeredMS = new Date().valueOf();
+          }
           nodeState.ui.isFiring = true;
           nodeState.ui.message = this.getUiMessage(nodeState, node);
           nodeState.ui.severity = node.severity;
diff --git a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
index 90b73005623d1..823019a0d4f30 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
@@ -491,7 +491,9 @@ export class MissingDataAlert extends BaseAlert {
         state.gapDuration = missing.gapDuration;
 
         if (stackProduct.shouldFire) {
-          state.ui.triggeredMS = new Date().valueOf();
+          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;
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
index d3b73028a09c4..d7c5468e155db 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
@@ -40,7 +40,10 @@ interface UuidResponse {
 
 interface UuidBucketESResponse {
   key: string;
-  top: {
+  most_recent: {
+    value: number;
+  };
+  document: {
     hits: {
       hits: TopHitESResponse[];
     };
@@ -49,7 +52,24 @@ interface UuidBucketESResponse {
 
 interface TopHitESResponse {
   _source: {
-    timestamp: string;
+    source_node?: {
+      name?: string;
+    };
+    kibana_stats?: {
+      kibana?: {
+        name?: string;
+      };
+    };
+    logstash_stats?: {
+      logstash?: {
+        host?: string;
+      };
+    };
+    beats_stats?: {
+      beat?: {
+        name?: string;
+      };
+    };
   };
 }
 
@@ -107,10 +127,15 @@ export async function fetchMissingData(
     'logstash_stats.logstash.host',
     'beats_stats.beat.name',
   ];
-  const topHitsAgg = {
-    top: {
+  const subAggs = {
+    most_recent: {
+      max: {
+        field: 'timestamp',
+      },
+    },
+    document: {
       top_hits: {
-        size: 2,
+        size: 1,
         sort: [
           {
             timestamp: {
@@ -119,15 +144,10 @@ export async function fetchMissingData(
           },
         ],
         _source: {
-          includes: ['timestamp', ...nameFields],
+          includes: nameFields,
         },
       },
     },
-    top_hit: {
-      max: {
-        field: 'timestamp',
-      },
-    },
   };
 
   const params = {
@@ -172,17 +192,15 @@ export async function fetchMissingData(
                   terms: {
                     field: 'node_stats.node_id',
                     size,
-                    order: { top_hit: 'desc' },
                   },
-                  aggs: topHitsAgg,
+                  aggs: subAggs,
                 },
                 kibana_uuids: {
                   terms: {
                     field: 'kibana_stats.kibana.uuid',
                     size,
-                    order: { top_hit: 'desc' },
                   },
-                  aggs: topHitsAgg,
+                  aggs: subAggs,
                 },
                 beats: {
                   filter: {
@@ -199,9 +217,8 @@ export async function fetchMissingData(
                       terms: {
                         field: 'beats_stats.beat.uuid',
                         size,
-                        order: { top_hit: 'desc' },
                       },
-                      aggs: topHitsAgg,
+                      aggs: subAggs,
                     },
                   },
                 },
@@ -220,9 +237,8 @@ export async function fetchMissingData(
                       terms: {
                         field: 'beats_stats.beat.uuid',
                         size,
-                        order: { top_hit: 'desc' },
                       },
-                      aggs: topHitsAgg,
+                      aggs: subAggs,
                     },
                   },
                 },
@@ -230,9 +246,8 @@ export async function fetchMissingData(
                   terms: {
                     field: 'logstash_stats.logstash.uuid',
                     size,
-                    order: { top_hit: 'desc' },
                   },
-                  aggs: topHitsAgg,
+                  aggs: subAggs,
                 },
               },
             },
@@ -255,15 +270,7 @@ export async function fetchMissingData(
       for (const uuidBucket of uuidBuckets) {
         const stackProductUuid = uuidBucket.key;
         const stackProduct = getStackProductFromIndex(indexBucket.key, clusterBucket);
-        let differenceInMs = -1;
-        if (uuidBucket.top.hits.hits.length === 1) {
-          differenceInMs = moment().diff(moment(uuidBucket.top.hits.hits[0]._source.timestamp));
-        } else {
-          const first = moment(uuidBucket.top.hits.hits[0]._source.timestamp);
-          const second = moment(uuidBucket.top.hits.hits[1]._source.timestamp);
-          differenceInMs = first.diff(second);
-        }
-
+        const differenceInMs = moment().diff(moment(uuidBucket.most_recent.value));
         let stackProductName = stackProductUuid;
         for (const nameField of nameFields) {
           stackProductName = get(uuidBucket, `top.hits.hits[0]._source.${nameField}`);

From 0b2db894b0ac458b50b215de8f9eea759bafda12 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Thu, 24 Sep 2020 12:58:36 -0400
Subject: [PATCH 09/16] We don't need moment

---
 .../plugins/monitoring/server/lib/alerts/fetch_missing_data.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
index d7c5468e155db..9968fc5812952 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
@@ -4,7 +4,6 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 import { get } from 'lodash';
-import moment from 'moment';
 import { AlertCluster, AlertMissingData } from '../../alerts/types';
 import {
   KIBANA_SYSTEM_ID,
@@ -270,7 +269,7 @@ export async function fetchMissingData(
       for (const uuidBucket of uuidBuckets) {
         const stackProductUuid = uuidBucket.key;
         const stackProduct = getStackProductFromIndex(indexBucket.key, clusterBucket);
-        const differenceInMs = moment().diff(moment(uuidBucket.most_recent.value));
+        const differenceInMs = +new Date() - uuidBucket.most_recent.value;
         let stackProductName = stackProductUuid;
         for (const nameField of nameFields) {
           stackProductName = get(uuidBucket, `top.hits.hits[0]._source.${nameField}`);

From 1ffd812d98470df01887143268f1a0c60a4b3808 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Thu, 24 Sep 2020 15:18:50 -0400
Subject: [PATCH 10/16] Add tests

---
 .../server/alerts/missing_data_alert.test.ts  | 457 ++++++++++++++++++
 .../server/alerts/missing_data_alert.ts       |   5 +-
 .../lib/alerts/fetch_missing_data.test.ts     | 343 +++++++++++++
 .../server/lib/alerts/fetch_missing_data.ts   |  45 +-
 4 files changed, 826 insertions(+), 24 deletions(-)
 create mode 100644 x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts
 create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts

diff --git a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts
new file mode 100644
index 0000000000000..7d9ec13951623
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts
@@ -0,0 +1,457 @@
+/*
+ * 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 { MissingDataAlert } from './missing_data_alert';
+import { ALERT_MISSING_DATA } from '../../common/constants';
+import { fetchMissingData } from '../lib/alerts/fetch_missing_data';
+import { fetchClusters } from '../lib/alerts/fetch_clusters';
+
+const RealDate = Date;
+
+jest.mock('../lib/alerts/fetch_missing_data', () => ({
+  fetchMissingData: jest.fn(),
+}));
+jest.mock('../lib/alerts/fetch_clusters', () => ({
+  fetchClusters: jest.fn(),
+}));
+
+describe('MissingDataAlert', () => {
+  it('should have defaults', () => {
+    const alert = new MissingDataAlert();
+    expect(alert.type).toBe(ALERT_MISSING_DATA);
+    expect(alert.label).toBe('Missing data');
+    expect(alert.defaultThrottle).toBe('1d');
+    // @ts-ignore
+    expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '5m' });
+    // @ts-ignore
+    expect(alert.actionVariables).toStrictEqual([
+      {
+        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: 'stackProducts', description: 'The stack products missing data.' },
+      { name: 'count', description: 'The number of stack products missing data.' },
+      { name: 'clusterName', description: 'The cluster to which the stack products 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;
+      (fetchMissingData 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 MissingDataAlert();
+      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: [
+          {
+            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?_g=(cluster_uuid:abc123)',
+                      },
+                    ],
+                  },
+                  {
+                    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,
+            },
+          },
+          {
+            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?_g=(cluster_uuid:abc123)',
+                      },
+                    ],
+                  },
+                  {
+                    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 () => {
+      (fetchMissingData as jest.Mock).mockImplementation(() => {
+        return [
+          {
+            ...missingData[0],
+            gapDuration: 1,
+          },
+        ];
+      });
+      const alert = new MissingDataAlert();
+      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 () => {
+      (fetchMissingData 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 MissingDataAlert();
+      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';
+      (fetchMissingData as jest.Mock).mockImplementation(() => {
+        return [
+          {
+            ...missingData[0],
+            ccs,
+          },
+        ];
+      });
+      const alert = new MissingDataAlert();
+      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 MissingDataAlert();
+      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_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
index 823019a0d4f30..10c9a483069bf 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
@@ -162,7 +162,8 @@ export class MissingDataAlert extends BaseAlert {
       clusters,
       indexPattern,
       limit,
-      this.config.ui.max_bucket_size
+      this.config.ui.max_bucket_size,
+      +new Date()
     );
     return missingData.map((missing) => {
       return {
@@ -349,7 +350,7 @@ export class MissingDataAlert extends BaseAlert {
           false
         )}: ${state.stackProductName}`;
       })
-      .join(',');
+      .join(', ');
     if (firingCount > 0) {
       const shortActionText = i18n.translate('xpack.monitoring.alerts.missingData.shortAction', {
         defaultMessage:
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
new file mode 100644
index 0000000000000..565126ea499cf
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
@@ -0,0 +1,343 @@
+/*
+ * 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 { set } from '@elastic/safer-lodash-set';
+import { fetchMissingData } from './fetch_missing_data';
+
+function getResponse(
+  index: string,
+  uuidKey: 'es_uuids' | 'kibana_uuids' | 'logstash_uuids' | 'beats.beats_uuids' | 'apms.apm_uuids',
+  clusters: Array<{
+    clusterUuid: string;
+    products: Array<{
+      uuid: string;
+      timestamp: number;
+      nameSource: any;
+    }>;
+  }>
+) {
+  return {
+    key: index,
+    clusters: {
+      buckets: clusters.map((cluster) => {
+        const result = {
+          key: cluster.clusterUuid,
+        };
+        set(result, uuidKey, {
+          buckets: cluster.products.map((product) => {
+            return {
+              key: product.uuid,
+              most_recent: {
+                value: product.timestamp,
+              },
+              document: {
+                hits: {
+                  hits: [
+                    {
+                      _source: product.nameSource,
+                    },
+                  ],
+                },
+              },
+            };
+          }),
+        });
+        return result;
+      }),
+    },
+  };
+}
+
+describe('fetchMissingData', () => {
+  let callCluster = jest.fn();
+  const index = '.monitoring-*';
+  const limit = 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: {
+          index: {
+            buckets: [
+              getResponse(
+                '.monitoring-es-*',
+                'es_uuids',
+                clusters.map((cluster) => ({
+                  clusterUuid: cluster.clusterUuid,
+                  products: [
+                    {
+                      uuid: 'nodeUuid1',
+                      nameSource: {
+                        source_node: {
+                          name: 'nodeName1',
+                        },
+                      },
+                      timestamp: 9,
+                    },
+                    {
+                      uuid: 'nodeUuid2',
+                      nameSource: {
+                        source_node: {
+                          name: 'nodeName2',
+                        },
+                      },
+                      timestamp: 2,
+                    },
+                  ],
+                }))
+              ),
+              getResponse(
+                '.monitoring-kibana-*',
+                'kibana_uuids',
+                clusters.map((cluster) => ({
+                  clusterUuid: cluster.clusterUuid,
+                  products: [
+                    {
+                      uuid: 'kibanaUuid1',
+                      nameSource: {
+                        kibana_stats: {
+                          kibana: {
+                            name: 'kibanaName1',
+                          },
+                        },
+                      },
+                      timestamp: 4,
+                    },
+                  ],
+                }))
+              ),
+              getResponse(
+                '.monitoring-logstash-*',
+                'logstash_uuids',
+                clusters.map((cluster) => ({
+                  clusterUuid: cluster.clusterUuid,
+                  products: [
+                    {
+                      uuid: 'logstashUuid1',
+                      nameSource: {
+                        logstash_stats: {
+                          logstash: {
+                            host: 'logstashName1',
+                          },
+                        },
+                      },
+                      timestamp: 2,
+                    },
+                  ],
+                }))
+              ),
+              getResponse(
+                '.monitoring-beats-*',
+                'beats.beats_uuids',
+                clusters.map((cluster) => ({
+                  clusterUuid: cluster.clusterUuid,
+                  products: [
+                    {
+                      uuid: 'beatUuid1',
+                      nameSource: {
+                        beats_stats: {
+                          beat: {
+                            name: 'beatName1',
+                          },
+                        },
+                      },
+                      timestamp: 0,
+                    },
+                  ],
+                }))
+              ),
+              getResponse(
+                '.monitoring-beats-*',
+                'apms.apm_uuids',
+                clusters.map((cluster) => ({
+                  clusterUuid: cluster.clusterUuid,
+                  products: [
+                    {
+                      uuid: 'apmUuid1',
+                      nameSource: {
+                        beats_stats: {
+                          beat: {
+                            name: 'apmName1',
+                          },
+                        },
+                      },
+                      timestamp: 1,
+                    },
+                  ],
+                }))
+              ),
+            ],
+          },
+        },
+      };
+    });
+    const result = await fetchMissingData(callCluster, clusters, index, limit, size, now);
+    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: {
+          index: {
+            buckets: [
+              getResponse(
+                'Monitoring:.monitoring-es-*',
+                'es_uuids',
+                clusters.map((cluster) => ({
+                  clusterUuid: cluster.clusterUuid,
+                  products: [
+                    {
+                      uuid: 'nodeUuid1',
+                      nameSource: {
+                        source_node: {
+                          name: 'nodeName1',
+                        },
+                      },
+                      timestamp: 9,
+                    },
+                  ],
+                }))
+              ),
+            ],
+          },
+        },
+      };
+    });
+    const result = await fetchMissingData(callCluster, clusters, index, limit, size, now);
+    expect(result).toEqual([
+      {
+        stackProduct: 'elasticsearch',
+        stackProductUuid: 'nodeUuid1',
+        stackProductName: 'nodeName1',
+        clusterUuid: 'clusterUuid1',
+        gapDuration: 1,
+        ccs: 'Monitoring',
+      },
+    ]);
+  });
+
+  it('should not return duplicates', async () => {
+    const now = 10;
+    const clusters = [
+      {
+        clusterUuid: 'clusterUuid1',
+        clusterName: 'clusterName1',
+      },
+    ];
+    callCluster = jest.fn().mockImplementation((...args) => {
+      return {
+        aggregations: {
+          index: {
+            buckets: [
+              getResponse(
+                '.monitoring-es-*',
+                'es_uuids',
+                clusters.map((cluster) => ({
+                  clusterUuid: cluster.clusterUuid,
+                  products: [
+                    {
+                      uuid: 'nodeUuid1',
+                      nameSource: {
+                        source_node: {
+                          name: 'nodeName1',
+                        },
+                      },
+                      timestamp: 9,
+                    },
+                    {
+                      uuid: 'nodeUuid1',
+                      nameSource: {
+                        source_node: {
+                          name: 'nodeName1',
+                        },
+                      },
+                      timestamp: 2,
+                    },
+                  ],
+                }))
+              ),
+            ],
+          },
+        },
+      };
+    });
+    const result = await fetchMissingData(callCluster, clusters, index, limit, size, now);
+    expect(result).toEqual([
+      {
+        stackProduct: 'elasticsearch',
+        stackProductUuid: 'nodeUuid1',
+        stackProductName: 'nodeName1',
+        clusterUuid: 'clusterUuid1',
+        gapDuration: 8,
+        ccs: null,
+      },
+    ]);
+  });
+});
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
index 9968fc5812952..98d8f1cf45df3 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
@@ -22,13 +22,13 @@ interface IndexBucketESResponse {
 
 interface ClusterBucketESResponse {
   key: string;
-  kibana_uuids: UuidResponse;
-  logstash_uuids: UuidResponse;
-  es_uuids: UuidResponse;
-  beats: {
+  kibana_uuids?: UuidResponse;
+  logstash_uuids?: UuidResponse;
+  es_uuids?: UuidResponse;
+  beats?: {
     beats_uuids: UuidResponse;
   };
-  apms: {
+  apms?: {
     apm_uuids: UuidResponse;
   };
 }
@@ -52,40 +52,40 @@ interface UuidBucketESResponse {
 interface TopHitESResponse {
   _source: {
     source_node?: {
-      name?: string;
+      name: string;
     };
     kibana_stats?: {
-      kibana?: {
-        name?: string;
+      kibana: {
+        name: string;
       };
     };
     logstash_stats?: {
-      logstash?: {
-        host?: string;
+      logstash: {
+        host: string;
       };
     };
     beats_stats?: {
-      beat?: {
-        name?: string;
+      beat: {
+        name: string;
       };
     };
   };
 }
 
 function findNonEmptyBucket(bucket: ClusterBucketESResponse): UuidResponse {
-  if (bucket.beats.beats_uuids.buckets.length > 0) {
+  if (bucket.beats && bucket.beats.beats_uuids.buckets.length > 0) {
     return bucket.beats.beats_uuids;
   }
-  if (bucket.apms.apm_uuids.buckets.length > 0) {
+  if (bucket.apms && bucket.apms.apm_uuids.buckets.length > 0) {
     return bucket.apms.apm_uuids;
   }
-  if (bucket.kibana_uuids.buckets.length > 0) {
+  if (bucket.kibana_uuids && bucket.kibana_uuids.buckets.length > 0) {
     return bucket.kibana_uuids;
   }
-  if (bucket.logstash_uuids.buckets.length > 0) {
+  if (bucket.logstash_uuids && bucket.logstash_uuids.buckets.length > 0) {
     return bucket.logstash_uuids;
   }
-  if (bucket.es_uuids.buckets.length > 0) {
+  if (bucket.es_uuids && bucket.es_uuids.buckets.length > 0) {
     return bucket.es_uuids;
   }
   return { buckets: [] };
@@ -96,7 +96,7 @@ function getStackProductFromIndex(index: string, bucket: ClusterBucketESResponse
     return KIBANA_SYSTEM_ID;
   }
   if (index.includes('-beats-')) {
-    if (bucket.apms.apm_uuids.buckets.length > 0) {
+    if (bucket.apms && bucket.apms.apm_uuids.buckets.length > 0) {
       return APM_SYSTEM_ID;
     }
     return BEATS_SYSTEM_ID;
@@ -115,9 +115,10 @@ export async function fetchMissingData(
   clusters: AlertCluster[],
   index: string,
   limit: number,
-  size: number
+  size: number,
+  nowInMS: number
 ): Promise<AlertMissingData[]> {
-  const endMs = +new Date();
+  const endMs = nowInMS;
   const startMs = endMs - limit - limit * 0.25; // 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 nameFields = [
@@ -269,10 +270,10 @@ export async function fetchMissingData(
       for (const uuidBucket of uuidBuckets) {
         const stackProductUuid = uuidBucket.key;
         const stackProduct = getStackProductFromIndex(indexBucket.key, clusterBucket);
-        const differenceInMs = +new Date() - uuidBucket.most_recent.value;
+        const differenceInMs = nowInMS - uuidBucket.most_recent.value;
         let stackProductName = stackProductUuid;
         for (const nameField of nameFields) {
-          stackProductName = get(uuidBucket, `top.hits.hits[0]._source.${nameField}`);
+          stackProductName = get(uuidBucket, `document.hits.hits[0]._source.${nameField}`);
           if (stackProductName) {
             break;
           }

From acc8502895d7ca3306c2e0745691349dbc4e323c Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Mon, 28 Sep 2020 14:45:09 -0400
Subject: [PATCH 11/16] PR feedback

---
 .../server/alerts/missing_data_alert.test.ts  |  6 +-
 .../server/alerts/missing_data_alert.ts       | 10 ++--
 .../lib/alerts/fetch_missing_data.test.ts     | 58 -------------------
 .../server/lib/alerts/fetch_missing_data.ts   | 11 ++--
 4 files changed, 12 insertions(+), 73 deletions(-)

diff --git a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts
index 7d9ec13951623..d30d504fbc97a 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts
@@ -21,7 +21,7 @@ describe('MissingDataAlert', () => {
   it('should have defaults', () => {
     const alert = new MissingDataAlert();
     expect(alert.type).toBe(ALERT_MISSING_DATA);
-    expect(alert.label).toBe('Missing data');
+    expect(alert.label).toBe('Missing monitoring data');
     expect(alert.defaultThrottle).toBe('1d');
     // @ts-ignore
     expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '5m' });
@@ -36,8 +36,8 @@ describe('MissingDataAlert', () => {
         description: 'The full internal message generated by Elastic.',
       },
       { name: 'state', description: 'The current state of the alert.' },
-      { name: 'stackProducts', description: 'The stack products missing data.' },
-      { name: 'count', description: 'The number of stack products missing data.' },
+      { name: 'stackProducts', description: 'The stack products missing monitoring data.' },
+      { name: 'count', description: 'The number of stack products missing monitoring data.' },
       { name: 'clusterName', description: 'The cluster to which the stack products belong.' },
       { name: 'action', description: 'The recommended action for this alert.' },
       {
diff --git a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
index 10c9a483069bf..f7d21d93f8b6b 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
@@ -56,7 +56,7 @@ export class MissingDataAlert extends BaseAlert {
   public static paramDetails = {
     duration: {
       label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', {
-        defaultMessage: `Notify if data is missing for`,
+        defaultMessage: `Notify if monitoring data is missing for`,
       }),
       type: AlertParamType.Duration,
     } as CommonAlertParamDetail,
@@ -70,7 +70,7 @@ export class MissingDataAlert extends BaseAlert {
 
   public type = ALERT_MISSING_DATA;
   public label = i18n.translate('xpack.monitoring.alerts.missingData.label', {
-    defaultMessage: 'Missing data',
+    defaultMessage: 'Missing monitoring data',
   });
 
   protected defaultParams: MissingDataParams = {
@@ -108,14 +108,14 @@ export class MissingDataAlert extends BaseAlert {
       description: i18n.translate(
         'xpack.monitoring.alerts.missingData.actionVariables.stackProducts',
         {
-          defaultMessage: 'The stack products missing data.',
+          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 data.',
+        defaultMessage: 'The number of stack products missing monitoring data.',
       }),
     },
     {
@@ -169,7 +169,7 @@ export class MissingDataAlert extends BaseAlert {
       return {
         instanceKey: `${missing.clusterUuid}:${missing.stackProduct}:${missing.stackProductUuid}`,
         clusterUuid: missing.clusterUuid,
-        shouldFire: missing.gapDuration > duration && missing.gapDuration <= limit,
+        shouldFire: missing.gapDuration > duration,
         severity: AlertSeverity.Danger,
         meta: { missing, limit },
         ccs: missing.ccs,
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
index 565126ea499cf..945e089731684 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
@@ -282,62 +282,4 @@ describe('fetchMissingData', () => {
       },
     ]);
   });
-
-  it('should not return duplicates', async () => {
-    const now = 10;
-    const clusters = [
-      {
-        clusterUuid: 'clusterUuid1',
-        clusterName: 'clusterName1',
-      },
-    ];
-    callCluster = jest.fn().mockImplementation((...args) => {
-      return {
-        aggregations: {
-          index: {
-            buckets: [
-              getResponse(
-                '.monitoring-es-*',
-                'es_uuids',
-                clusters.map((cluster) => ({
-                  clusterUuid: cluster.clusterUuid,
-                  products: [
-                    {
-                      uuid: 'nodeUuid1',
-                      nameSource: {
-                        source_node: {
-                          name: 'nodeName1',
-                        },
-                      },
-                      timestamp: 9,
-                    },
-                    {
-                      uuid: 'nodeUuid1',
-                      nameSource: {
-                        source_node: {
-                          name: 'nodeName1',
-                        },
-                      },
-                      timestamp: 2,
-                    },
-                  ],
-                }))
-              ),
-            ],
-          },
-        },
-      };
-    });
-    const result = await fetchMissingData(callCluster, clusters, index, limit, size, now);
-    expect(result).toEqual([
-      {
-        stackProduct: 'elasticsearch',
-        stackProductUuid: 'nodeUuid1',
-        stackProductName: 'nodeName1',
-        clusterUuid: 'clusterUuid1',
-        gapDuration: 8,
-        ccs: null,
-      },
-    ]);
-  });
 });
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
index 98d8f1cf45df3..62326e33928f5 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
@@ -119,7 +119,7 @@ export async function fetchMissingData(
   nowInMS: number
 ): Promise<AlertMissingData[]> {
   const endMs = nowInMS;
-  const startMs = endMs - limit - limit * 0.25; // 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 startMs = endMs - limit - 3 * 60 * 1000; // 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 nameFields = [
     'source_node.name',
@@ -259,9 +259,7 @@ export async function fetchMissingData(
 
   const response = await callCluster('search', params);
   const indexBuckets = get(response, 'aggregations.index.buckets', []) as IndexBucketESResponse[];
-  const uniqueList: {
-    [id: string]: AlertMissingData;
-  } = {};
+  const missingData: AlertMissingData[] = [];
   for (const indexBucket of indexBuckets) {
     const clusterBuckets = indexBucket.clusters.buckets;
     for (const clusterBucket of clusterBuckets) {
@@ -279,18 +277,17 @@ export async function fetchMissingData(
           }
         }
 
-        uniqueList[`${clusterUuid}::${stackProduct}::${stackProductUuid}`] = {
+        missingData.push({
           stackProduct,
           stackProductUuid,
           stackProductName,
           clusterUuid,
           gapDuration: differenceInMs,
           ccs: indexBucket.key.includes(':') ? indexBucket.key.split(':')[0] : null,
-        };
+        });
       }
     }
   }
 
-  const missingData = Object.values(uniqueList);
   return missingData;
 }

From 6514604f87e8609ed93ab0027066cc049f112485 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Mon, 28 Sep 2020 16:03:56 -0400
Subject: [PATCH 12/16] Update copy

---
 x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
index f7d21d93f8b6b..a3b623e270c25 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
@@ -62,7 +62,7 @@ export class MissingDataAlert extends BaseAlert {
     } as CommonAlertParamDetail,
     limit: {
       label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.limit.label', {
-        defaultMessage: `Look this far back in time for any data`,
+        defaultMessage: `Look this far back in time for monitoring data`,
       }),
       type: AlertParamType.Duration,
     } as CommonAlertParamDetail,

From 9c70e11e6d47a0c2b73ef2fb057ed53ddc9fd576 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Tue, 29 Sep 2020 14:42:43 -0400
Subject: [PATCH 13/16] Fix up bug around grabbing old data

---
 .../lib/alerts/fetch_missing_data.test.ts     | 258 +++++++-----------
 .../server/lib/alerts/fetch_missing_data.ts   | 197 ++++++-------
 2 files changed, 194 insertions(+), 261 deletions(-)

diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
index 945e089731684..f72fddcac2f58 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
@@ -3,50 +3,35 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-import { set } from '@elastic/safer-lodash-set';
 import { fetchMissingData } from './fetch_missing_data';
 
 function getResponse(
   index: string,
-  uuidKey: 'es_uuids' | 'kibana_uuids' | 'logstash_uuids' | 'beats.beats_uuids' | 'apms.apm_uuids',
-  clusters: Array<{
-    clusterUuid: string;
-    products: Array<{
-      uuid: string;
-      timestamp: number;
-      nameSource: any;
-    }>;
+  products: Array<{
+    uuid: string;
+    timestamp: number;
+    nameSource: any;
   }>
 ) {
   return {
-    key: index,
-    clusters: {
-      buckets: clusters.map((cluster) => {
-        const result = {
-          key: cluster.clusterUuid,
-        };
-        set(result, uuidKey, {
-          buckets: cluster.products.map((product) => {
-            return {
-              key: product.uuid,
-              most_recent: {
-                value: product.timestamp,
-              },
-              document: {
-                hits: {
-                  hits: [
-                    {
-                      _source: product.nameSource,
-                    },
-                  ],
-                },
+    buckets: products.map((product) => {
+      return {
+        key: product.uuid,
+        most_recent: {
+          value: product.timestamp,
+        },
+        document: {
+          hits: {
+            hits: [
+              {
+                _index: index,
+                _source: product.nameSource,
               },
-            };
-          }),
-        });
-        return result;
-      }),
-    },
+            ],
+          },
+        },
+      };
+    }),
   };
 }
 
@@ -67,116 +52,87 @@ describe('fetchMissingData', () => {
     callCluster = jest.fn().mockImplementation((...args) => {
       return {
         aggregations: {
-          index: {
-            buckets: [
-              getResponse(
-                '.monitoring-es-*',
-                'es_uuids',
-                clusters.map((cluster) => ({
-                  clusterUuid: cluster.clusterUuid,
-                  products: [
-                    {
-                      uuid: 'nodeUuid1',
-                      nameSource: {
-                        source_node: {
-                          name: 'nodeName1',
-                        },
-                      },
-                      timestamp: 9,
+          clusters: {
+            buckets: clusters.map((cluster) => ({
+              key: cluster.clusterUuid,
+              es_uuids: getResponse('.monitoring-es-*', [
+                {
+                  uuid: 'nodeUuid1',
+                  nameSource: {
+                    source_node: {
+                      name: 'nodeName1',
                     },
-                    {
-                      uuid: 'nodeUuid2',
-                      nameSource: {
-                        source_node: {
-                          name: 'nodeName2',
-                        },
-                      },
-                      timestamp: 2,
+                  },
+                  timestamp: 9,
+                },
+                {
+                  uuid: 'nodeUuid2',
+                  nameSource: {
+                    source_node: {
+                      name: 'nodeName2',
                     },
-                  ],
-                }))
-              ),
-              getResponse(
-                '.monitoring-kibana-*',
-                'kibana_uuids',
-                clusters.map((cluster) => ({
-                  clusterUuid: cluster.clusterUuid,
-                  products: [
-                    {
-                      uuid: 'kibanaUuid1',
-                      nameSource: {
-                        kibana_stats: {
-                          kibana: {
-                            name: 'kibanaName1',
-                          },
-                        },
+                  },
+                  timestamp: 2,
+                },
+              ]),
+              kibana_uuids: getResponse('.monitoring-kibana-*', [
+                {
+                  uuid: 'kibanaUuid1',
+                  nameSource: {
+                    kibana_stats: {
+                      kibana: {
+                        name: 'kibanaName1',
                       },
-                      timestamp: 4,
                     },
-                  ],
-                }))
-              ),
-              getResponse(
-                '.monitoring-logstash-*',
-                'logstash_uuids',
-                clusters.map((cluster) => ({
-                  clusterUuid: cluster.clusterUuid,
-                  products: [
-                    {
-                      uuid: 'logstashUuid1',
-                      nameSource: {
-                        logstash_stats: {
-                          logstash: {
-                            host: 'logstashName1',
-                          },
-                        },
+                  },
+                  timestamp: 4,
+                },
+              ]),
+              logstash_uuids: getResponse('.monitoring-logstash-*', [
+                {
+                  uuid: 'logstashUuid1',
+                  nameSource: {
+                    logstash_stats: {
+                      logstash: {
+                        host: 'logstashName1',
                       },
-                      timestamp: 2,
                     },
-                  ],
-                }))
-              ),
-              getResponse(
-                '.monitoring-beats-*',
-                'beats.beats_uuids',
-                clusters.map((cluster) => ({
-                  clusterUuid: cluster.clusterUuid,
-                  products: [
-                    {
-                      uuid: 'beatUuid1',
-                      nameSource: {
-                        beats_stats: {
-                          beat: {
-                            name: 'beatName1',
-                          },
+                  },
+                  timestamp: 2,
+                },
+              ]),
+              beats: {
+                beats_uuids: getResponse('.monitoring-beats-*', [
+                  {
+                    uuid: 'beatUuid1',
+                    nameSource: {
+                      beats_stats: {
+                        beat: {
+                          name: 'beatName1',
                         },
                       },
-                      timestamp: 0,
                     },
-                  ],
-                }))
-              ),
-              getResponse(
-                '.monitoring-beats-*',
-                'apms.apm_uuids',
-                clusters.map((cluster) => ({
-                  clusterUuid: cluster.clusterUuid,
-                  products: [
-                    {
-                      uuid: 'apmUuid1',
-                      nameSource: {
-                        beats_stats: {
-                          beat: {
-                            name: 'apmName1',
-                          },
+                    timestamp: 0,
+                  },
+                ]),
+              },
+              apms: {
+                apm_uuids: getResponse('.monitoring-beats-*', [
+                  {
+                    uuid: 'apmUuid1',
+                    nameSource: {
+                      beats_stats: {
+                        beat: {
+                          name: 'apmName1',
+                          type: 'apm-server',
                         },
                       },
-                      timestamp: 1,
                     },
-                  ],
-                }))
-              ),
-            ],
+                    timestamp: 1,
+                  },
+                ]),
+              },
+            })),
           },
         },
       };
@@ -245,27 +201,21 @@ describe('fetchMissingData', () => {
     callCluster = jest.fn().mockImplementation((...args) => {
       return {
         aggregations: {
-          index: {
-            buckets: [
-              getResponse(
-                'Monitoring:.monitoring-es-*',
-                'es_uuids',
-                clusters.map((cluster) => ({
-                  clusterUuid: cluster.clusterUuid,
-                  products: [
-                    {
-                      uuid: 'nodeUuid1',
-                      nameSource: {
-                        source_node: {
-                          name: 'nodeName1',
-                        },
-                      },
-                      timestamp: 9,
+          clusters: {
+            buckets: clusters.map((cluster) => ({
+              key: cluster.clusterUuid,
+              es_uuids: getResponse('Monitoring:.monitoring-es-*', [
+                {
+                  uuid: 'nodeUuid1',
+                  nameSource: {
+                    source_node: {
+                      name: 'nodeName1',
                     },
-                  ],
-                }))
-              ),
-            ],
+                  },
+                  timestamp: 9,
+                },
+              ]),
+            })),
           },
         },
       };
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
index 62326e33928f5..7a739258678fe 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
@@ -13,13 +13,6 @@ import {
   ELASTICSEARCH_SYSTEM_ID,
 } from '../../../common/constants';
 
-interface IndexBucketESResponse {
-  key: string;
-  clusters: {
-    buckets: ClusterBucketESResponse[];
-  };
-}
-
 interface ClusterBucketESResponse {
   key: string;
   kibana_uuids?: UuidResponse;
@@ -50,6 +43,7 @@ interface UuidBucketESResponse {
 }
 
 interface TopHitESResponse {
+  _index: string;
   _source: {
     source_node?: {
       name: string;
@@ -67,36 +61,18 @@ interface TopHitESResponse {
     beats_stats?: {
       beat: {
         name: string;
+        type: string;
       };
     };
   };
 }
 
-function findNonEmptyBucket(bucket: ClusterBucketESResponse): UuidResponse {
-  if (bucket.beats && bucket.beats.beats_uuids.buckets.length > 0) {
-    return bucket.beats.beats_uuids;
-  }
-  if (bucket.apms && bucket.apms.apm_uuids.buckets.length > 0) {
-    return bucket.apms.apm_uuids;
-  }
-  if (bucket.kibana_uuids && bucket.kibana_uuids.buckets.length > 0) {
-    return bucket.kibana_uuids;
-  }
-  if (bucket.logstash_uuids && bucket.logstash_uuids.buckets.length > 0) {
-    return bucket.logstash_uuids;
-  }
-  if (bucket.es_uuids && bucket.es_uuids.buckets.length > 0) {
-    return bucket.es_uuids;
-  }
-  return { buckets: [] };
-}
-
-function getStackProductFromIndex(index: string, bucket: ClusterBucketESResponse) {
+function getStackProductFromIndex(index: string, beatType: string) {
   if (index.includes('-kibana-')) {
     return KIBANA_SYSTEM_ID;
   }
   if (index.includes('-beats-')) {
-    if (bucket.apms && bucket.apms.apm_uuids.buckets.length > 0) {
+    if (beatType === 'apm-server') {
       return APM_SYSTEM_ID;
     }
     return BEATS_SYSTEM_ID;
@@ -126,6 +102,7 @@ export async function fetchMissingData(
     'kibana_stats.kibana.name',
     'logstash_stats.logstash.host',
     'beats_stats.beat.name',
+    'beat_stats.beat.type',
   ];
   const subAggs = {
     most_recent: {
@@ -144,7 +121,7 @@ export async function fetchMissingData(
           },
         ],
         _source: {
-          includes: nameFields,
+          includes: ['_index', ...nameFields],
         },
       },
     },
@@ -152,7 +129,7 @@ export async function fetchMissingData(
 
   const params = {
     index,
-    filterPath: ['aggregations.index.buckets'],
+    filterPath: ['aggregations.clusters.buckets'],
     body: {
       size: 0,
       query: {
@@ -176,81 +153,73 @@ export async function fetchMissingData(
         },
       },
       aggs: {
-        index: {
+        clusters: {
           terms: {
-            field: '_index',
+            field: 'cluster_uuid',
             size,
           },
           aggs: {
-            clusters: {
+            es_uuids: {
               terms: {
-                field: 'cluster_uuid',
+                field: 'node_stats.node_id',
                 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: subAggs,
                 },
-                kibana_uuids: {
+              },
+              aggs: {
+                beats_uuids: {
                   terms: {
-                    field: 'kibana_stats.kibana.uuid',
+                    field: 'beats_stats.beat.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,
+              },
+            },
+            apms: {
+              filter: {
+                bool: {
+                  must: {
+                    term: {
+                      'beats_stats.beat.type': 'apm-server',
                     },
                   },
                 },
-                logstash_uuids: {
+              },
+              aggs: {
+                apm_uuids: {
                   terms: {
-                    field: 'logstash_stats.logstash.uuid',
+                    field: 'beats_stats.beat.uuid',
                     size,
                   },
                   aggs: subAggs,
                 },
               },
             },
+            logstash_uuids: {
+              terms: {
+                field: 'logstash_stats.logstash.uuid',
+                size,
+              },
+              aggs: subAggs,
+            },
           },
         },
       },
@@ -258,36 +227,50 @@ export async function fetchMissingData(
   };
 
   const response = await callCluster('search', params);
-  const indexBuckets = get(response, 'aggregations.index.buckets', []) as IndexBucketESResponse[];
-  const missingData: AlertMissingData[] = [];
-  for (const indexBucket of indexBuckets) {
-    const clusterBuckets = indexBucket.clusters.buckets;
-    for (const clusterBucket of clusterBuckets) {
-      const clusterUuid = clusterBucket.key;
-      const uuidBuckets = findNonEmptyBucket(clusterBucket).buckets;
-      for (const uuidBucket of uuidBuckets) {
-        const stackProductUuid = uuidBucket.key;
-        const stackProduct = getStackProductFromIndex(indexBucket.key, clusterBucket);
-        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;
-          }
-        }
+  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 || []),
+    ];
 
-        missingData.push({
-          stackProduct,
-          stackProductUuid,
-          stackProductName,
-          clusterUuid,
-          gapDuration: differenceInMs,
-          ccs: indexBucket.key.includes(':') ? indexBucket.key.split(':')[0] : null,
-        });
+    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;
 }

From f4c9b67abc0531b3729e5fd25fc5135b01e67b4f Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Wed, 30 Sep 2020 14:43:39 -0400
Subject: [PATCH 14/16] PR feedback

---
 x-pack/plugins/monitoring/common/constants.ts |  4 +-
 .../expression.tsx                            |  0
 .../index.ts                                  |  2 +-
 .../missing_monitoring_data_alert.tsx}        | 12 ++---
 .../validation.tsx                            |  0
 .../components/cluster/overview/apm_panel.js  |  4 +-
 .../cluster/overview/beats_panel.js           |  4 +-
 .../cluster/overview/elasticsearch_panel.js   |  4 +-
 .../components/cluster/overview/index.js      | 12 ++---
 .../cluster/overview/kibana_panel.js          |  4 +-
 .../cluster/overview/logstash_panel.js        |  4 +-
 x-pack/plugins/monitoring/public/plugin.ts    |  4 +-
 .../public/views/apm/instance/index.js        |  8 ++-
 .../public/views/apm/instances/index.js       |  8 ++-
 .../public/views/apm/overview/index.js        |  8 ++-
 .../public/views/beats/beat/index.js          |  8 ++-
 .../public/views/beats/listing/index.js       |  8 ++-
 .../public/views/beats/overview/index.js      |  8 ++-
 .../elasticsearch/node/advanced/index.js      |  4 +-
 .../public/views/elasticsearch/node/index.js  |  4 +-
 .../public/views/elasticsearch/nodes/index.js |  4 +-
 .../public/views/kibana/instance/index.js     |  4 +-
 .../public/views/kibana/instances/index.js    |  4 +-
 .../views/logstash/node/advanced/index.js     |  4 +-
 .../public/views/logstash/node/index.js       |  4 +-
 .../public/views/logstash/nodes/index.js      |  4 +-
 .../server/alerts/alerts_factory.test.ts      |  2 +-
 .../server/alerts/alerts_factory.ts           |  6 +--
 .../monitoring/server/alerts/base_alert.ts    | 28 +++++++---
 .../plugins/monitoring/server/alerts/index.ts |  2 +-
 ... => missing_monitoring_data_alert.test.ts} | 34 ++++++-------
 ...rt.ts => missing_monitoring_data_alert.ts} | 51 ++++++++++++++++---
 .../monitoring/server/alerts/types.d.ts       |  2 +-
 .../server/lib/alerts/fetch_clusters.ts       | 19 ++++---
 ... => fetch_missing_monitoring_data.test.ts} |  8 +--
 ...ta.ts => fetch_missing_monitoring_data.ts} | 10 ++--
 36 files changed, 189 insertions(+), 107 deletions(-)
 rename x-pack/plugins/monitoring/public/alerts/{missing_data_alert => missing_monitoring_data_alert}/expression.tsx (100%)
 rename x-pack/plugins/monitoring/public/alerts/{missing_data_alert => missing_monitoring_data_alert}/index.ts (73%)
 rename x-pack/plugins/monitoring/public/alerts/{missing_data_alert/missing_data_alert.tsx => missing_monitoring_data_alert/missing_monitoring_data_alert.tsx} (66%)
 rename x-pack/plugins/monitoring/public/alerts/{missing_data_alert => missing_monitoring_data_alert}/validation.tsx (100%)
 rename x-pack/plugins/monitoring/server/alerts/{missing_data_alert.test.ts => missing_monitoring_data_alert.test.ts} (93%)
 rename x-pack/plugins/monitoring/server/alerts/{missing_data_alert.ts => missing_monitoring_data_alert.ts} (91%)
 rename x-pack/plugins/monitoring/server/lib/alerts/{fetch_missing_data.test.ts => fetch_missing_monitoring_data.test.ts} (94%)
 rename x-pack/plugins/monitoring/server/lib/alerts/{fetch_missing_data.ts => fetch_missing_monitoring_data.ts} (94%)

diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts
index d662584bd73fb..860f6439f3fdf 100644
--- a/x-pack/plugins/monitoring/common/constants.ts
+++ b/x-pack/plugins/monitoring/common/constants.ts
@@ -236,7 +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_DATA = `${ALERT_PREFIX}alert_missing_data`;
+export const ALERT_MISSING_MONITORING_DATA = `${ALERT_PREFIX}alert_missing_monitoring_data`;
 
 /**
  * A listing of all alert types
@@ -250,7 +250,7 @@ export const ALERTS = [
   ALERT_ELASTICSEARCH_VERSION_MISMATCH,
   ALERT_KIBANA_VERSION_MISMATCH,
   ALERT_LOGSTASH_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
 ];
 
 /**
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx
similarity index 100%
rename from x-pack/plugins/monitoring/public/alerts/missing_data_alert/expression.tsx
rename to x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/index.ts b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts
similarity index 73%
rename from x-pack/plugins/monitoring/public/alerts/missing_data_alert/index.ts
rename to x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts
index 5a7dbda9d2a53..5169601c0e6e3 100644
--- a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/index.ts
+++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts
@@ -4,4 +4,4 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-export { createMissingDataAlertType } from './missing_data_alert';
+export { createMissingMonitoringDataAlertType } from './missing_monitoring_data_alert';
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/missing_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx
similarity index 66%
rename from x-pack/plugins/monitoring/public/alerts/missing_data_alert/missing_data_alert.tsx
rename to x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx
index 84b5545db8795..bcea98592adb2 100644
--- a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/missing_data_alert.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx
@@ -7,19 +7,19 @@ 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_DATA } from '../../../common/constants';
+import { ALERT_MISSING_MONITORING_DATA } from '../../../common/constants';
 import { Expression } from './expression';
 // eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { MissingDataAlert } from '../../../server/alerts';
+import { MissingMonitoringDataAlert } from '../../../server/alerts';
 
-export function createMissingDataAlertType(): AlertTypeModel {
-  const alert = new MissingDataAlert();
+export function createMissingMonitoringDataAlertType(): AlertTypeModel {
+  const alert = new MissingMonitoringDataAlert();
   return {
-    id: ALERT_MISSING_DATA,
+    id: ALERT_MISSING_MONITORING_DATA,
     name: alert.label,
     iconClass: 'bell',
     alertParamsExpression: (props: any) => (
-      <Expression {...props} paramDetails={MissingDataAlert.paramDetails} />
+      <Expression {...props} paramDetails={MissingMonitoringDataAlert.paramDetails} />
     ),
     validate,
     defaultActionMessage: '{{context.internalFullMessage}}',
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_data_alert/validation.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx
similarity index 100%
rename from x-pack/plugins/monitoring/public/alerts/missing_data_alert/validation.tsx
rename to x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx
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 790418a16ed83..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
@@ -27,7 +27,7 @@ import { formatTimestampToDuration } from '../../../../common';
 import {
   CALCULATE_DURATION_SINCE,
   APM_SYSTEM_ID,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
 } from '../../../../common/constants';
 import { SetupModeTooltip } from '../../setup_mode/tooltip';
 import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
@@ -36,7 +36,7 @@ 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_DATA];
+const SERVERS_PANEL_ALERTS = [ALERT_MISSING_MONITORING_DATA];
 
 export function ApmPanel(props) {
   const { setupMode, alerts } = props;
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 38b6d7f4d1547..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,14 +23,14 @@ import { ClusterItemContainer, DisabledIfNoDataAndInSetupModeLink } from './help
 import { FormattedMessage } from '@kbn/i18n/react';
 import { i18n } from '@kbn/i18n';
 import { SetupModeTooltip } from '../../setup_mode/tooltip';
-import { ALERT_MISSING_DATA, 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_DATA];
+const BEATS_PANEL_ALERTS = [ALERT_MISSING_MONITORING_DATA];
 
 export function BeatsPanel(props) {
   const { setupMode, alerts } = props;
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 60185a3c20d4c..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,7 +43,7 @@ import {
   ALERT_DISK_USAGE,
   ALERT_NODES_CHANGED,
   ALERT_ELASTICSEARCH_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
 } from '../../../../common/constants';
 import { AlertsBadge } from '../../../alerts/badge';
 import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
@@ -162,7 +162,7 @@ const NODES_PANEL_ALERTS = [
   ALERT_DISK_USAGE,
   ALERT_NODES_CHANGED,
   ALERT_ELASTICSEARCH_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  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 b616898223d98..aebd1cee5f0be 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js
@@ -14,7 +14,7 @@ import { ApmPanel } from './apm_panel';
 import { FormattedMessage } from '@kbn/i18n/react';
 import {
   STANDALONE_CLUSTER_CLUSTER_UUID,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
   ELASTICSEARCH_SYSTEM_ID,
   KIBANA_SYSTEM_ID,
   LOGSTASH_SYSTEM_ID,
@@ -47,7 +47,7 @@ export function Overview(props) {
               setupMode={props.setupMode}
               showLicenseExpiration={props.showLicenseExpiration}
               alerts={filterAlertStates(props.alerts, (type, { state }) => {
-                if (type === ALERT_MISSING_DATA) {
+                if (type === ALERT_MISSING_MONITORING_DATA) {
                   return state.stackProduct === ELASTICSEARCH_SYSTEM_ID;
                 }
                 return true;
@@ -57,7 +57,7 @@ export function Overview(props) {
               {...props.cluster.kibana}
               setupMode={props.setupMode}
               alerts={filterAlertStates(props.alerts, (type, { state }) => {
-                if (type === ALERT_MISSING_DATA) {
+                if (type === ALERT_MISSING_MONITORING_DATA) {
                   return state.stackProduct === KIBANA_SYSTEM_ID;
                 }
                 return true;
@@ -70,7 +70,7 @@ export function Overview(props) {
           {...props.cluster.logstash}
           setupMode={props.setupMode}
           alerts={filterAlertStates(props.alerts, (type, { state }) => {
-            if (type === ALERT_MISSING_DATA) {
+            if (type === ALERT_MISSING_MONITORING_DATA) {
               return state.stackProduct === LOGSTASH_SYSTEM_ID;
             }
             return true;
@@ -81,7 +81,7 @@ export function Overview(props) {
           {...props.cluster.beats}
           setupMode={props.setupMode}
           alerts={filterAlertStates(props.alerts, (type, { state }) => {
-            if (type === ALERT_MISSING_DATA) {
+            if (type === ALERT_MISSING_MONITORING_DATA) {
               return state.stackProduct === BEATS_SYSTEM_ID;
             }
             return true;
@@ -92,7 +92,7 @@ export function Overview(props) {
           {...props.cluster.apm}
           setupMode={props.setupMode}
           alerts={filterAlertStates(props.alerts, (type, { state }) => {
-            if (type === ALERT_MISSING_DATA) {
+            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 56d6977c92f6b..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
@@ -31,7 +31,7 @@ import { SetupModeTooltip } from '../../setup_mode/tooltip';
 import {
   KIBANA_SYSTEM_ID,
   ALERT_KIBANA_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
 } from '../../../../common/constants';
 import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
 import { AlertsBadge } from '../../../alerts/badge';
@@ -39,7 +39,7 @@ import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badg
 import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
 import { SetupModeFeature } from '../../../../common/enums';
 
-const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_DATA];
+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 e5b39266437d9..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,7 +15,7 @@ import {
   LOGSTASH,
   LOGSTASH_SYSTEM_ID,
   ALERT_LOGSTASH_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
 } from '../../../../common/constants';
 
 import {
@@ -41,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, ALERT_MISSING_DATA];
+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/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts
index 38e4e921cc8a7..f4f66185346e8 100644
--- a/x-pack/plugins/monitoring/public/plugin.ts
+++ b/x-pack/plugins/monitoring/public/plugin.ts
@@ -23,7 +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 { createMissingDataAlertType } from './alerts/missing_data_alert';
+import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert';
 import { createLegacyAlertTypes } from './alerts/legacy_alert';
 import { createDiskUsageAlertType } from './alerts/disk_usage_alert';
 
@@ -73,7 +73,7 @@ export class MonitoringPlugin
     }
 
     plugins.triggers_actions_ui.alertTypeRegistry.register(createCpuUsageAlertType());
-    plugins.triggers_actions_ui.alertTypeRegistry.register(createMissingDataAlertType());
+    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 07fe46b3ed7e1..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, ALERT_MISSING_DATA, APM_SYSTEM_ID } from '../../../../common/constants';
+import {
+  CODE_PATH_APM,
+  ALERT_MISSING_MONITORING_DATA,
+  APM_SYSTEM_ID,
+} from '../../../../common/constants';
 
 uiRoutes.when('/apm/instances/:uuid', {
   template,
@@ -53,7 +57,7 @@ uiRoutes.when('/apm/instances/:uuid', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: APM_SYSTEM_ID,
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 1151faa8634e7..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, ALERT_MISSING_DATA } from '../../../../common/constants';
+import {
+  APM_SYSTEM_ID,
+  CODE_PATH_APM,
+  ALERT_MISSING_MONITORING_DATA,
+} from '../../../../common/constants';
 
 uiRoutes.when('/apm/instances', {
   template,
@@ -50,7 +54,7 @@ uiRoutes.when('/apm/instances', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: APM_SYSTEM_ID,
diff --git a/x-pack/plugins/monitoring/public/views/apm/overview/index.js b/x-pack/plugins/monitoring/public/views/apm/overview/index.js
index bb5ea38967b60..12821ec432c24 100644
--- a/x-pack/plugins/monitoring/public/views/apm/overview/index.js
+++ b/x-pack/plugins/monitoring/public/views/apm/overview/index.js
@@ -12,7 +12,11 @@ import { routeInitProvider } from '../../../lib/route_init';
 import template from './index.html';
 import { MonitoringViewBaseController } from '../../base_controller';
 import { ApmOverview } from '../../../components/apm/overview';
-import { CODE_PATH_APM, ALERT_MISSING_DATA, APM_SYSTEM_ID } from '../../../../common/constants';
+import {
+  CODE_PATH_APM,
+  ALERT_MISSING_MONITORING_DATA,
+  APM_SYSTEM_ID,
+} from '../../../../common/constants';
 
 uiRoutes.when('/apm', {
   template,
@@ -45,7 +49,7 @@ uiRoutes.when('/apm', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: APM_SYSTEM_ID,
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 05153ca9a0325..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, ALERT_MISSING_DATA, BEATS_SYSTEM_ID } 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', {
@@ -55,7 +59,7 @@ uiRoutes.when('/beats/beat/:beatUuid', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: BEATS_SYSTEM_ID,
diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/plugins/monitoring/public/views/beats/listing/index.js
index 135bcec216e39..f8f0749d6d30e 100644
--- a/x-pack/plugins/monitoring/public/views/beats/listing/index.js
+++ b/x-pack/plugins/monitoring/public/views/beats/listing/index.js
@@ -14,7 +14,11 @@ import template from './index.html';
 import React, { Fragment } from 'react';
 import { Listing } from '../../../components/beats/listing/listing';
 import { SetupModeRenderer } from '../../../components/renderers';
-import { CODE_PATH_BEATS, BEATS_SYSTEM_ID, ALERT_MISSING_DATA } from '../../../../common/constants';
+import {
+  CODE_PATH_BEATS,
+  BEATS_SYSTEM_ID,
+  ALERT_MISSING_MONITORING_DATA,
+} from '../../../../common/constants';
 
 uiRoutes.when('/beats/beats', {
   template,
@@ -49,7 +53,7 @@ uiRoutes.when('/beats/beats', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: BEATS_SYSTEM_ID,
diff --git a/x-pack/plugins/monitoring/public/views/beats/overview/index.js b/x-pack/plugins/monitoring/public/views/beats/overview/index.js
index d24b21d0c4b30..ef80d7e77fe6e 100644
--- a/x-pack/plugins/monitoring/public/views/beats/overview/index.js
+++ b/x-pack/plugins/monitoring/public/views/beats/overview/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, ALERT_MISSING_DATA, BEATS_SYSTEM_ID } from '../../../../common/constants';
+import {
+  CODE_PATH_BEATS,
+  ALERT_MISSING_MONITORING_DATA,
+  BEATS_SYSTEM_ID,
+} from '../../../../common/constants';
 import { BeatsOverview } from '../../../components/beats/overview';
 
 uiRoutes.when('/beats', {
@@ -47,7 +51,7 @@ uiRoutes.when('/beats', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: BEATS_SYSTEM_ID,
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 7e8efb7207ca9..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,7 +20,7 @@ import { MonitoringViewBaseController } from '../../../base_controller';
 import {
   CODE_PATH_ELASTICSEARCH,
   ALERT_CPU_USAGE,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
   ALERT_DISK_USAGE,
 } from '../../../../../common/constants';
 
@@ -72,7 +72,7 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_DATA],
+            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 a73b5cb275813..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,7 +21,7 @@ import { MonitoringViewBaseController } from '../../base_controller';
 import {
   CODE_PATH_ELASTICSEARCH,
   ALERT_CPU_USAGE,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
   ALERT_DISK_USAGE,
 } from '../../../../common/constants';
 
@@ -56,7 +56,7 @@ uiRoutes.when('/elasticsearch/nodes/:node', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_DATA],
+            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 9e81b15a22b2d..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,7 +19,7 @@ import {
   ELASTICSEARCH_SYSTEM_ID,
   CODE_PATH_ELASTICSEARCH,
   ALERT_CPU_USAGE,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
   ALERT_DISK_USAGE,
 } from '../../../../common/constants';
 
@@ -88,7 +88,7 @@ uiRoutes.when('/elasticsearch/nodes', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_DATA],
+            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 020f67dbd723d..29852501d1667 100644
--- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js
+++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js
@@ -30,7 +30,7 @@ import { MonitoringViewBaseController } from '../../base_controller';
 import {
   CODE_PATH_KIBANA,
   ALERT_KIBANA_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
   KIBANA_SYSTEM_ID,
 } from '../../../../common/constants';
 import { AlertsCallout } from '../../../alerts/callout';
@@ -81,7 +81,7 @@ uiRoutes.when('/kibana/instances/:uuid', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: KIBANA_SYSTEM_ID,
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 c4f88b92a2b11..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,7 +17,7 @@ import {
   KIBANA_SYSTEM_ID,
   CODE_PATH_KIBANA,
   ALERT_KIBANA_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
 } from '../../../../common/constants';
 
 uiRoutes.when('/kibana/instances', {
@@ -47,7 +47,7 @@ uiRoutes.when('/kibana/instances', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_DATA],
+            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 f389fbf307b86..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
@@ -29,7 +29,7 @@ import { MonitoringTimeseriesContainer } from '../../../../components/chart';
 import {
   CODE_PATH_LOGSTASH,
   ALERT_LOGSTASH_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
   LOGSTASH_SYSTEM_ID,
 } from '../../../../../common/constants';
 import { AlertsCallout } from '../../../../alerts/callout';
@@ -78,7 +78,7 @@ uiRoutes.when('/logstash/node/:uuid/advanced', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: LOGSTASH_SYSTEM_ID,
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 a53267293f4a5..cccae6913052a 100644
--- a/x-pack/plugins/monitoring/public/views/logstash/node/index.js
+++ b/x-pack/plugins/monitoring/public/views/logstash/node/index.js
@@ -29,7 +29,7 @@ import { MonitoringViewBaseController } from '../../base_controller';
 import {
   CODE_PATH_LOGSTASH,
   ALERT_LOGSTASH_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
   LOGSTASH_SYSTEM_ID,
 } from '../../../../common/constants';
 import { AlertsCallout } from '../../../alerts/callout';
@@ -78,7 +78,7 @@ uiRoutes.when('/logstash/node/:uuid', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_DATA],
+            alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
             filters: [
               {
                 stackProduct: LOGSTASH_SYSTEM_ID,
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 2dd8f1139fca3..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,7 +16,7 @@ import {
   CODE_PATH_LOGSTASH,
   LOGSTASH_SYSTEM_ID,
   ALERT_LOGSTASH_VERSION_MISMATCH,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
 } from '../../../../common/constants';
 
 uiRoutes.when('/logstash/nodes', {
@@ -46,7 +46,7 @@ uiRoutes.when('/logstash/nodes', {
         alerts: {
           shouldFetch: true,
           options: {
-            alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_DATA],
+            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 74e11fdda1a8a..05a92cea5469b 100644
--- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
+++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
@@ -6,7 +6,7 @@
 
 import {
   CpuUsageAlert,
-  MissingDataAlert,
+  MissingMonitoringDataAlert,
   DiskUsageAlert,
   NodesChangedAlert,
   ClusterHealthAlert,
@@ -20,7 +20,7 @@ import {
   ALERT_CLUSTER_HEALTH,
   ALERT_LICENSE_EXPIRATION,
   ALERT_CPU_USAGE,
-  ALERT_MISSING_DATA,
+  ALERT_MISSING_MONITORING_DATA,
   ALERT_DISK_USAGE,
   ALERT_NODES_CHANGED,
   ALERT_LOGSTASH_VERSION_MISMATCH,
@@ -33,7 +33,7 @@ export const BY_TYPE = {
   [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert,
   [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert,
   [ALERT_CPU_USAGE]: CpuUsageAlert,
-  [ALERT_MISSING_DATA]: MissingDataAlert,
+  [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 fea47d2c88ded..61486626040f7 100644
--- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
@@ -239,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
     );
@@ -254,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/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts
index bc23d9405b2ce..41f6daa38d1dc 100644
--- a/x-pack/plugins/monitoring/server/alerts/index.ts
+++ b/x-pack/plugins/monitoring/server/alerts/index.ts
@@ -6,7 +6,7 @@
 
 export { BaseAlert } from './base_alert';
 export { CpuUsageAlert } from './cpu_usage_alert';
-export { MissingDataAlert } from './missing_data_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_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
similarity index 93%
rename from x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts
rename to x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
index d30d504fbc97a..b0d1b93e72822 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.test.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
@@ -3,24 +3,24 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-import { MissingDataAlert } from './missing_data_alert';
-import { ALERT_MISSING_DATA } from '../../common/constants';
-import { fetchMissingData } from '../lib/alerts/fetch_missing_data';
+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_data', () => ({
-  fetchMissingData: jest.fn(),
+jest.mock('../lib/alerts/fetch_missing_monitoring_data', () => ({
+  fetchMissingMonitoringData: jest.fn(),
 }));
 jest.mock('../lib/alerts/fetch_clusters', () => ({
   fetchClusters: jest.fn(),
 }));
 
-describe('MissingDataAlert', () => {
+describe('MissingMonitoringDataAlert', () => {
   it('should have defaults', () => {
-    const alert = new MissingDataAlert();
-    expect(alert.type).toBe(ALERT_MISSING_DATA);
+    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
@@ -109,7 +109,7 @@ describe('MissingDataAlert', () => {
     beforeEach(() => {
       // @ts-ignore
       Date = FakeDate;
-      (fetchMissingData as jest.Mock).mockImplementation(() => {
+      (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
         return missingData;
       });
       (fetchClusters as jest.Mock).mockImplementation(() => {
@@ -125,7 +125,7 @@ describe('MissingDataAlert', () => {
     });
 
     it('should fire actions', async () => {
-      const alert = new MissingDataAlert();
+      const alert = new MissingMonitoringDataAlert();
       alert.initializeAlertType(
         getUiSettingsService as any,
         monitoringCluster as any,
@@ -245,7 +245,7 @@ describe('MissingDataAlert', () => {
     });
 
     it('should not fire actions if under threshold', async () => {
-      (fetchMissingData as jest.Mock).mockImplementation(() => {
+      (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
         return [
           {
             ...missingData[0],
@@ -253,7 +253,7 @@ describe('MissingDataAlert', () => {
           },
         ];
       });
-      const alert = new MissingDataAlert();
+      const alert = new MissingMonitoringDataAlert();
       alert.initializeAlertType(
         getUiSettingsService as any,
         monitoringCluster as any,
@@ -294,7 +294,7 @@ describe('MissingDataAlert', () => {
     });
 
     it('should resolve with a resolved message', async () => {
-      (fetchMissingData as jest.Mock).mockImplementation(() => {
+      (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
         return [
           {
             ...missingData[0],
@@ -327,7 +327,7 @@ describe('MissingDataAlert', () => {
           ],
         };
       });
-      const alert = new MissingDataAlert();
+      const alert = new MissingMonitoringDataAlert();
       alert.initializeAlertType(
         getUiSettingsService as any,
         monitoringCluster as any,
@@ -387,7 +387,7 @@ describe('MissingDataAlert', () => {
 
     it('should handle ccs', async () => {
       const ccs = 'testCluster';
-      (fetchMissingData as jest.Mock).mockImplementation(() => {
+      (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
         return [
           {
             ...missingData[0],
@@ -395,7 +395,7 @@ describe('MissingDataAlert', () => {
           },
         ];
       });
-      const alert = new MissingDataAlert();
+      const alert = new MissingMonitoringDataAlert();
       alert.initializeAlertType(
         getUiSettingsService as any,
         monitoringCluster as any,
@@ -425,7 +425,7 @@ describe('MissingDataAlert', () => {
     });
 
     it('should fire with different messaging for cloud', async () => {
-      const alert = new MissingDataAlert();
+      const alert = new MissingMonitoringDataAlert();
       alert.initializeAlertType(
         getUiSettingsService as any,
         monitoringCluster as any,
diff --git a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
similarity index 91%
rename from x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
rename to x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
index a3b623e270c25..d9c1f38210b96 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
@@ -19,7 +19,11 @@ import {
   AlertMessageLinkToken,
 } from './types';
 import { AlertInstance, AlertServices } from '../../../alerts/server';
-import { INDEX_PATTERN, ALERT_MISSING_DATA } from '../../common/constants';
+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';
@@ -32,10 +36,12 @@ import {
   CommonAlertNodeUuidFilter,
 } from '../../common/types';
 import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
-import { fetchMissingData } from '../lib/alerts/fetch_missing_data';
+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';
 
 const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', {
   defaultMessage: 'resolved',
@@ -47,12 +53,15 @@ const FIRING = i18n.translate('xpack.monitoring.alerts.missingData.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 MissingDataAlert extends BaseAlert {
+export class MissingMonitoringDataAlert extends BaseAlert {
   public static paramDetails = {
     duration: {
       label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', {
@@ -68,7 +77,7 @@ export class MissingDataAlert extends BaseAlert {
     } as CommonAlertParamDetail,
   };
 
-  public type = ALERT_MISSING_DATA;
+  public type = ALERT_MISSING_MONITORING_DATA;
   public label = i18n.translate('xpack.monitoring.alerts.missingData.label', {
     defaultMessage: 'Missing monitoring data',
   });
@@ -144,6 +153,32 @@ export class MissingDataAlert extends BaseAlert {
     },
   ];
 
+  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,
@@ -157,13 +192,15 @@ export class MissingDataAlert extends BaseAlert {
     }
     const duration = parseDuration(((params as unknown) as MissingDataParams).duration);
     const limit = parseDuration(((params as unknown) as MissingDataParams).limit);
-    const missingData = await fetchMissingData(
+    const now = +new Date();
+    const missingData = await fetchMissingMonitoringData(
       callCluster,
       clusters,
       indexPattern,
       limit,
       this.config.ui.max_bucket_size,
-      +new Date()
+      now,
+      now - limit - LIMIT_BUFFER
     );
     return missingData.map((missing) => {
       return {
@@ -442,7 +479,7 @@ export class MissingDataAlert extends BaseAlert {
     }
   }
 
-  protected processData(
+  protected async processData(
     data: AlertData[],
     clusters: AlertCluster[],
     services: AlertServices,
diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts
index cfcd7792d749c..4b78bca9f47ca 100644
--- a/x-pack/plugins/monitoring/server/alerts/types.d.ts
+++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts
@@ -106,7 +106,7 @@ export interface AlertMissingData {
   stackProductName: string;
   clusterUuid: string;
   gapDuration: number;
-  ccs: string | null;
+  ccs?: string;
 }
 
 export interface AlertData {
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<AlertCluster[]> {
+interface RangeFilter {
+  [field: string]: {
+    format?: string;
+    gte: string | number;
+  };
+}
+
+export async function fetchClusters(
+  callCluster: any,
+  index: string,
+  rangeFilter: RangeFilter = { timestamp: { gte: 'now-2m' } }
+): Promise<AlertCluster[]> {
   const params = {
     index,
     filterPath: [
@@ -25,11 +36,7 @@ export async function fetchClusters(callCluster: any, index: string): Promise<Al
               },
             },
             {
-              range: {
-                timestamp: {
-                  gte: 'now-2m',
-                },
-              },
+              range: rangeFilter,
             },
           ],
         },
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
similarity index 94%
rename from x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
rename to x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
index f72fddcac2f58..750ec33d028cf 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
@@ -3,7 +3,7 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-import { fetchMissingData } from './fetch_missing_data';
+import { fetchMissingMonitoringData } from './fetch_missing_monitoring_data';
 
 function getResponse(
   index: string,
@@ -35,7 +35,7 @@ function getResponse(
   };
 }
 
-describe('fetchMissingData', () => {
+describe('fetchMissingMonitoringData', () => {
   let callCluster = jest.fn();
   const index = '.monitoring-*';
   const limit = 100;
@@ -137,7 +137,7 @@ describe('fetchMissingData', () => {
         },
       };
     });
-    const result = await fetchMissingData(callCluster, clusters, index, limit, size, now);
+    const result = await fetchMissingMonitoringData(callCluster, clusters, index, limit, size, now);
     expect(result).toEqual([
       {
         stackProduct: 'elasticsearch',
@@ -220,7 +220,7 @@ describe('fetchMissingData', () => {
         },
       };
     });
-    const result = await fetchMissingData(callCluster, clusters, index, limit, size, now);
+    const result = await fetchMissingMonitoringData(callCluster, clusters, index, limit, size, now);
     expect(result).toEqual([
       {
         stackProduct: 'elasticsearch',
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
similarity index 94%
rename from x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
rename to x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
index 7a739258678fe..ee7297b1e5a77 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
@@ -86,16 +86,16 @@ function getStackProductFromIndex(index: string, beatType: string) {
   return '';
 }
 
-export async function fetchMissingData(
+export async function fetchMissingMonitoringData(
   callCluster: any,
   clusters: AlertCluster[],
   index: string,
   limit: number,
   size: number,
-  nowInMS: number
+  nowInMs: number,
+  startMs: number
 ): Promise<AlertMissingData[]> {
-  const endMs = nowInMS;
-  const startMs = endMs - limit - 3 * 60 * 1000; // 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 endMs = nowInMs;
 
   const nameFields = [
     'source_node.name',
@@ -251,7 +251,7 @@ export async function fetchMissingData(
         indexName,
         get(uuidBucket, `document.hits.hits[0]._source.beats_stats.beat.type`)
       );
-      const differenceInMs = nowInMS - uuidBucket.most_recent.value;
+      const differenceInMs = nowInMs - uuidBucket.most_recent.value;
       let stackProductName = stackProductUuid;
       for (const nameField of nameFields) {
         stackProductName = get(uuidBucket, `document.hits.hits[0]._source.${nameField}`);

From c262c6ac2db79c80e49af17730614687d27a08a3 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Thu, 1 Oct 2020 09:22:38 -0400
Subject: [PATCH 15/16] PR feedback

---
 .../alerts/missing_monitoring_data_alert.ts   | 76 ++-----------------
 .../fetch_missing_monitoring_data.test.ts     | 20 ++++-
 .../alerts/fetch_missing_monitoring_data.ts   |  1 -
 3 files changed, 25 insertions(+), 72 deletions(-)

diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
index d9c1f38210b96..6017314f332e6 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
@@ -16,7 +16,6 @@ import {
   AlertMissingData,
   AlertMessageTimeToken,
   AlertInstanceState,
-  AlertMessageLinkToken,
 } from './types';
 import { AlertInstance, AlertServices } from '../../../alerts/server';
 import {
@@ -42,6 +41,7 @@ import { getListingLinkForStackProduct } from '../lib/alerts/get_listing_link_fo
 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',
@@ -88,30 +88,6 @@ export class MissingMonitoringDataAlert extends BaseAlert {
   };
 
   protected actionVariables = [
-    {
-      name: 'internalShortMessage',
-      description: i18n.translate(
-        'xpack.monitoring.alerts.missingData.actionVariables.internalShortMessage',
-        {
-          defaultMessage: 'The short internal message generated by Elastic.',
-        }
-      ),
-    },
-    {
-      name: 'internalFullMessage',
-      description: i18n.translate(
-        'xpack.monitoring.alerts.missingData.actionVariables.internalFullMessage',
-        {
-          defaultMessage: 'The full internal message generated by Elastic.',
-        }
-      ),
-    },
-    {
-      name: 'state',
-      description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.state', {
-        defaultMessage: 'The current state of the alert.',
-      }),
-    },
     {
       name: 'stackProducts',
       description: i18n.translate(
@@ -127,30 +103,7 @@ export class MissingMonitoringDataAlert extends BaseAlert {
         defaultMessage: 'The number of stack products missing monitoring data.',
       }),
     },
-    {
-      name: 'clusterName',
-      description: i18n.translate(
-        'xpack.monitoring.alerts.missingData.actionVariables.clusterName',
-        {
-          defaultMessage: 'The cluster to which the stack products belong.',
-        }
-      ),
-    },
-    {
-      name: 'action',
-      description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.action', {
-        defaultMessage: 'The recommended action for this alert.',
-      }),
-    },
-    {
-      name: 'actionPlain',
-      description: i18n.translate(
-        'xpack.monitoring.alerts.missingData.actionVariables.actionPlain',
-        {
-          defaultMessage: 'The recommended action for this alert, without any markdown.',
-        }
-      ),
-    },
+    ...Object.values(AlertingDefaults.ALERT_TYPE.context),
   ];
 
   protected async fetchClusters(
@@ -197,7 +150,6 @@ export class MissingMonitoringDataAlert extends BaseAlert {
       callCluster,
       clusters,
       indexPattern,
-      limit,
       this.config.ui.max_bucket_size,
       now,
       now - limit - LIMIT_BUFFER
@@ -271,10 +223,6 @@ export class MissingMonitoringDataAlert extends BaseAlert {
 
   protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage {
     const { missing, limit } = item.meta as { missing: AlertMissingData; limit: number };
-    const globalState = [`cluster_uuid:${item.clusterUuid}`];
-    if (item.ccs) {
-      globalState.push(`ccs:${item.ccs}`);
-    }
     if (!alertState.ui.isFiring) {
       if (missing.gapDuration > limit) {
         return {
@@ -319,27 +267,19 @@ export class MissingMonitoringDataAlert extends BaseAlert {
         },
       }),
       nextSteps: [
-        {
-          text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.hotThreads', {
+        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),
             },
           }),
-          tokens: [
-            {
-              startToken: '#start_link',
-              endToken: '#end_link',
-              type: AlertMessageTokenType.Link,
-              url: `${getListingLinkForStackProduct(missing.stackProduct)}?_g=(${globalState.join(
-                ','
-              )})`,
-            } as AlertMessageLinkToken,
-          ],
-        },
+          getListingLinkForStackProduct(missing.stackProduct),
+          AlertMessageTokenType.Link
+        ),
         {
-          text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.runningTasks', {
+          text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.verifySettings', {
             defaultMessage: `Verify monitoring settings on the {type}`,
             values: {
               type: getTypeLabelForStackProduct(missing.stackProduct, false),
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
index 750ec33d028cf..b09f5a88dba9c 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
@@ -38,7 +38,7 @@ function getResponse(
 describe('fetchMissingMonitoringData', () => {
   let callCluster = jest.fn();
   const index = '.monitoring-*';
-  const limit = 100;
+  const startMs = 100;
   const size = 10;
 
   it('fetch as expected', async () => {
@@ -137,7 +137,14 @@ describe('fetchMissingMonitoringData', () => {
         },
       };
     });
-    const result = await fetchMissingMonitoringData(callCluster, clusters, index, limit, size, now);
+    const result = await fetchMissingMonitoringData(
+      callCluster,
+      clusters,
+      index,
+      size,
+      now,
+      startMs
+    );
     expect(result).toEqual([
       {
         stackProduct: 'elasticsearch',
@@ -220,7 +227,14 @@ describe('fetchMissingMonitoringData', () => {
         },
       };
     });
-    const result = await fetchMissingMonitoringData(callCluster, clusters, index, limit, size, now);
+    const result = await fetchMissingMonitoringData(
+      callCluster,
+      clusters,
+      index,
+      size,
+      now,
+      startMs
+    );
     expect(result).toEqual([
       {
         stackProduct: 'elasticsearch',
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
index ee7297b1e5a77..91fc05137a8c1 100644
--- 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
@@ -90,7 +90,6 @@ export async function fetchMissingMonitoringData(
   callCluster: any,
   clusters: AlertCluster[],
   index: string,
-  limit: number,
   size: number,
   nowInMs: number,
   startMs: number

From 08374e1a02ee791510f730fd31d4fcf81ae85370 Mon Sep 17 00:00:00 2001
From: chrisronline <chrisronline@gmail.com>
Date: Thu, 1 Oct 2020 09:52:01 -0400
Subject: [PATCH 16/16] Fix tests

---
 .../alerts/missing_monitoring_data_alert.test.ts     | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
index b0d1b93e72822..4c06d9718c455 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
@@ -27,6 +27,8 @@ describe('MissingMonitoringDataAlert', () => {
     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.',
@@ -36,9 +38,7 @@ describe('MissingMonitoringDataAlert', () => {
         description: 'The full internal message generated by Elastic.',
       },
       { name: 'state', description: 'The current state of the alert.' },
-      { name: 'stackProducts', description: 'The stack products missing monitoring data.' },
-      { name: 'count', description: 'The number of stack products missing monitoring data.' },
-      { name: 'clusterName', description: 'The cluster to which the stack products belong.' },
+      { name: 'clusterName', description: 'The cluster to which the nodes belong.' },
       { name: 'action', description: 'The recommended action for this alert.' },
       {
         name: 'actionPlain',
@@ -144,6 +144,7 @@ describe('MissingMonitoringDataAlert', () => {
       expect(replaceState).toHaveBeenCalledWith({
         alertStates: [
           {
+            ccs: undefined,
             cluster: { clusterUuid, clusterName },
             gapDuration,
             stackProduct,
@@ -162,7 +163,7 @@ describe('MissingMonitoringDataAlert', () => {
                         startToken: '#start_link',
                         endToken: '#end_link',
                         type: 'link',
-                        url: 'elasticsearch/nodes?_g=(cluster_uuid:abc123)',
+                        url: 'elasticsearch/nodes',
                       },
                     ],
                   },
@@ -187,6 +188,7 @@ describe('MissingMonitoringDataAlert', () => {
             },
           },
           {
+            ccs: undefined,
             cluster: { clusterUuid, clusterName },
             gapDuration: gapDuration + 10,
             stackProduct: 'kibana',
@@ -205,7 +207,7 @@ describe('MissingMonitoringDataAlert', () => {
                         startToken: '#start_link',
                         endToken: '#end_link',
                         type: 'link',
-                        url: 'kibana/instances?_g=(cluster_uuid:abc123)',
+                        url: 'kibana/instances',
                       },
                     ],
                   },