From 08393507758b4b5ebe5350334849c6a735cc147e Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 22 Dec 2020 12:56:10 -0500 Subject: [PATCH 01/13] License expiration --- .../plugins/monitoring/common/types/alerts.ts | 13 ++ .../monitoring/server/alerts/base_alert.ts | 93 +++++------ .../alerts/license_expiration_alert.test.ts | 44 +++-- .../server/alerts/license_expiration_alert.ts | 157 ++++++++++++------ .../server/lib/alerts/fetch_licenses.test.ts | 61 +++++++ .../server/lib/alerts/fetch_licenses.ts | 66 ++++++++ 6 files changed, 317 insertions(+), 117 deletions(-) create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index df6e169f37f7a..1b7f013927043 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -57,6 +57,7 @@ export interface AlertInstanceState { | AlertDiskUsageState | AlertThreadPoolRejectionsState | AlertNodeState + | AlertLicenseState >; [x: string]: unknown; } @@ -93,6 +94,10 @@ export interface AlertThreadPoolRejectionsState extends AlertState { nodeName?: string; } +export interface AlertLicenseState extends AlertState { + expiryDateMS: number; +} + export interface AlertUiState { isFiring: boolean; resolvedMS?: number; @@ -213,3 +218,11 @@ export interface LegacyAlertNodesChangedList { added: { [nodeName: string]: string }; restarted: { [nodeName: string]: string }; } + +export interface AlertLicense { + status: string; + type: string; + expiryDateMS: number; + clusterUuid: string; + ccs: string; +} diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 46adfebfd17bf..2f7921c188503 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -30,21 +30,15 @@ import { import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { MonitoringLicenseService } from '../types'; import { mbSafeQuery } from '../lib/mb_safe_query'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerts/common/parse_duration'; import { Globals } from '../static_globals'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; - -interface LegacyOptions { - watchName: string; - nodeNameLabel: string; - changeDataValues?: Partial; -} +// import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +// import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; type ExecutedState = | { @@ -59,7 +53,7 @@ interface AlertOptions { name: string; throttle?: string | null; interval?: string; - legacy?: LegacyOptions; + // legacy?: LegacyOptions; defaultParams?: CommonAlertParams; actionVariables: Array<{ name: string; description: string }>; fetchClustersRange?: number; @@ -118,12 +112,12 @@ export class BaseAlert { } public isEnabled(licenseService: MonitoringLicenseService) { - if (this.alertOptions.legacy) { - const watcherFeature = licenseService.getWatcherFeature(); - if (!watcherFeature.isAvailable || !watcherFeature.isEnabled) { - return false; - } - } + // if (this.alertOptions.legacy) { + // const watcherFeature = licenseService.getWatcherFeature(); + // if (!watcherFeature.isAvailable || !watcherFeature.isEnabled) { + // return false; + // } + // } return true; } @@ -260,10 +254,10 @@ export class BaseAlert { params as CommonAlertParams, availableCcs ); - if (this.alertOptions.legacy) { - const data = await this.fetchLegacyData(callCluster, clusters, availableCcs); - return await this.processLegacyData(data, clusters, services, state); - } + // if (this.alertOptions.legacy) { + // const data = await this.fetchLegacyData(callCluster, clusters, availableCcs); + // return await this.processLegacyData(data, clusters, services, state); + // } const data = await this.fetchData(params, callCluster, clusters, availableCcs); return await this.processData(data, clusters, services, state); } @@ -298,37 +292,38 @@ export class BaseAlert { clusters: AlertCluster[], availableCcs: string[] ): Promise> { - throw new Error('Child classes must implement `fetchData`'); + return []; + // throw new Error('Child classes must implement `fetchData`'); } - protected async fetchLegacyData( - callCluster: CallCluster, - clusters: AlertCluster[], - availableCcs: string[] - ): Promise { - let alertIndexPattern = INDEX_ALERTS; - if (availableCcs) { - alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); - } - const legacyAlerts = await fetchLegacyAlerts( - callCluster, - clusters, - alertIndexPattern, - this.alertOptions.legacy!.watchName, - Globals.app.config.ui.max_bucket_size - ); - - return legacyAlerts.map((legacyAlert) => { - return { - clusterUuid: legacyAlert.metadata.cluster_uuid, - shouldFire: !legacyAlert.resolved_timestamp, - severity: mapLegacySeverity(legacyAlert.metadata.severity), - meta: legacyAlert, - nodeName: this.alertOptions.legacy!.nodeNameLabel, - ...this.alertOptions.legacy!.changeDataValues, - }; - }); - } + // protected async fetchLegacyData( + // callCluster: CallCluster, + // clusters: AlertCluster[], + // availableCcs: string[] + // ): Promise { + // let alertIndexPattern = INDEX_ALERTS; + // if (availableCcs) { + // alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + // } + // const legacyAlerts = await fetchLegacyAlerts( + // callCluster, + // clusters, + // alertIndexPattern, + // this.alertOptions.legacy!.watchName, + // Globals.app.config.ui.max_bucket_size + // ); + + // return legacyAlerts.map((legacyAlert) => { + // return { + // clusterUuid: legacyAlert.metadata.cluster_uuid, + // shouldFire: !legacyAlert.resolved_timestamp, + // severity: mapLegacySeverity(legacyAlert.metadata.severity), + // meta: legacyAlert, + // nodeName: this.alertOptions.legacy!.nodeNameLabel, + // ...this.alertOptions.legacy!.changeDataValues, + // }; + // }); + // } protected async processData( data: AlertData[], diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index c64b6e4b92984..d3b06ed5ae5ab 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -5,23 +5,27 @@ */ import { LicenseExpirationAlert } from './license_expiration_alert'; import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_licenses', () => ({ + fetchLicenses: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); jest.mock('moment', () => { - return function () { + const moment = function () { return { format: () => 'THE_DATE', }; }; + moment.utc = () => ({ + format: () => 'THE_DATE', + }); + return moment; }); jest.mock('../static_globals', () => ({ @@ -72,15 +76,11 @@ describe('LicenseExpirationAlert', () => { const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: - 'The license for this cluster expires in {{#relativeTime}}metadata.time{{/relativeTime}} at {{#absoluteTime}}metadata.time{{/absoluteTime}}.', - message: 'Update your license.', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, - time: 1, - }, + const license = { + status: 'expired', + type: 'gold', + expiryDateMS: 1, + clusterUuid, }; const replaceState = jest.fn(); @@ -103,8 +103,8 @@ describe('LicenseExpirationAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [license]; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -130,7 +130,15 @@ describe('LicenseExpirationAlert', () => { { cluster: { clusterUuid, clusterName }, ccs: undefined, - nodeName: 'Elasticsearch cluster alert', + itemLabel: undefined, + meta: { + clusterUuid: 'abc123', + expiryDateMS: 1, + status: 'expired', + type: 'gold', + }, + nodeId: undefined, + nodeName: undefined, ui: { isFiring: true, message: { @@ -159,7 +167,7 @@ describe('LicenseExpirationAlert', () => { }, ], }, - severity: 'warning', + severity: 'danger', triggeredMS: 1, lastCheckedMS: 0, }, @@ -180,7 +188,7 @@ describe('LicenseExpirationAlert', () => { }); it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + (fetchLicenses as jest.Mock).mockImplementation(() => { return []; }); const alert = new LicenseExpirationAlert(); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index 9dacba1dfe0ec..a396e0c86e660 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -14,30 +14,33 @@ import { AlertMessage, AlertMessageTimeToken, AlertMessageLinkToken, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertLicense, + AlertLicenseState, } from '../../common/types/alerts'; import { AlertExecutorOptions, AlertInstance } from '../../../alerts/server'; import { ALERT_LICENSE_EXPIRATION, FORMAT_DURATION_TEMPLATE_SHORT, LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; -import { AlertMessageTokenType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; + +const EXPIRES_DAYS = [60, 30, 14, 7]; export class LicenseExpirationAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_LICENSE_EXPIRATION, name: LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label, - legacy: { - watchName: 'xpack_license_expiration', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.licenseExpiration.nodeNameLabel', { - defaultMessage: 'Elasticsearch cluster alert', - }), - }, interval: '1d', actionVariables: [ { @@ -74,8 +77,59 @@ export class LicenseExpirationAlert extends BaseAlert { return await super.execute(options); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const stats = await fetchLicenses( + callCluster, + clusters, + esIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return stats.map((stat) => { + const { clusterUuid, type, expiryDateMS, status, ccs } = stat; + const $expiry = moment.utc(expiryDateMS); + let isExpired = false; + let severity = AlertSeverity.Success; + + if (status !== 'active') { + isExpired = true; + severity = AlertSeverity.Danger; + } else if (expiryDateMS) { + for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { + if (type === 'trial' && i < 2) { + break; + } + + const $fromNow = moment.utc().add(EXPIRES_DAYS[i], 'days'); + if ($fromNow.isAfter($expiry)) { + isExpired = true; + severity = i > 1 ? AlertSeverity.Warning : AlertSeverity.Danger; + break; + } + } + } + + return { + shouldFire: isExpired, + severity, + meta: stat, + clusterUuid, + ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; + const license = item.meta as AlertLicense; return { text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { defaultMessage: `The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link`, @@ -86,14 +140,14 @@ export class LicenseExpirationAlert extends BaseAlert { type: AlertMessageTokenType.Time, isRelative: true, isAbsolute: false, - timestamp: legacyAlert.metadata.time, + timestamp: license.expiryDateMS, } as AlertMessageTimeToken, { startToken: '#absolute', type: AlertMessageTokenType.Time, isAbsolute: true, isRelative: false, - timestamp: legacyAlert.metadata.time, + timestamp: license.expiryDateMS, } as AlertMessageTimeToken, { startToken: '#start_link', @@ -107,47 +161,50 @@ export class LicenseExpirationAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const $expiry = moment(legacyAlert.metadata.time); - if (alertState.ui.isFiring) { - const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { - defaultMessage: 'Please update your license.', - }); - const action = `[${actionText}](elasticsearch/nodes)`; - const expiredDate = $expiry.format(FORMAT_DURATION_TEMPLATE_SHORT).trim(); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - expiredDate, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, - }); + // This alert does not feature grouping + if (alertStates.length !== 1) { + return; } + + const state: AlertLicenseState = alertStates[0] as AlertLicenseState; + const $expiry = moment.utc(state.expiryDateMS); + const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { + defaultMessage: 'Please update your license.', + }); + const action = `[${actionText}](elasticsearch/nodes)`; + const expiredDate = $expiry.format(FORMAT_DURATION_TEMPLATE_SHORT).trim(); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + expiredDate, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts new file mode 100644 index 0000000000000..0d60824f4b0ba --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -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 { fetchLicenses } from './fetch_licenses'; + +describe('fetchLicenses', () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const size = 1000; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + + it('return a list of licenses', async () => { + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index, size); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + }, + ]); + }); + + it('should only search for the clusters provided', async () => { + const callCluster = jest.fn(); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index, size); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + }); + + it('should limit the time period in the query', async () => { + const callCluster = jest.fn(); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index, size); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts new file mode 100644 index 0000000000000..9fd018efa855e --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -0,0 +1,66 @@ +/* + * 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 { AlertLicense, AlertCluster } from '../../../common/types/alerts'; + +export async function fetchLicenses( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.license.*', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'hits.hits', []).map((hit: any) => { + const rawLicense: any = get(hit, '_source.license', {}); + const license: AlertLicense = { + status: rawLicense.status, + type: rawLicense.type, + expiryDateMS: rawLicense.expiry_date_in_millis, + clusterUuid: get(hit, '_source.cluster_uuid'), + ccs: get(hit, '_index'), + }; + return license; + }); +} From 34073308d015cc4dac07ddafd428818927e4bceb Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 5 Jan 2021 13:16:27 -0500 Subject: [PATCH 02/13] Fetch legacy alert data from the source --- .../plugins/monitoring/common/types/alerts.ts | 40 +++- x-pack/plugins/monitoring/common/types/es.ts | 9 + .../server/alerts/cluster_health_alert.ts | 151 ++++++++----- .../elasticsearch_version_mismatch_alert.ts | 151 ++++++++----- .../alerts/kibana_version_mismatch_alert.ts | 156 ++++++++----- .../server/alerts/license_expiration_alert.ts | 8 +- .../alerts/logstash_version_mismatch_alert.ts | 151 ++++++++----- .../server/alerts/nodes_changed_alert.ts | 205 ++++++++++++------ .../server/lib/alerts/fetch_cluster_health.ts | 62 ++++++ .../alerts/fetch_elasticsearch_versions.ts | 69 ++++++ .../lib/alerts/fetch_kibana_versions.ts | 110 ++++++++++ .../lib/alerts/fetch_logstash_versions.ts | 110 ++++++++++ .../alerts/fetch_nodes_from_cluster_stats.ts | 87 ++++++++ 13 files changed, 996 insertions(+), 313 deletions(-) create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 1b7f013927043..70ff9e9235428 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -5,7 +5,12 @@ */ import { Alert, AlertTypeParams, SanitizedAlert } from '../../../alerts/common'; -import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums'; +import { + AlertParamType, + AlertMessageTokenType, + AlertSeverity, + AlertClusterHealthType, +} from '../enums'; export type CommonAlert = Alert | SanitizedAlert; @@ -58,6 +63,7 @@ export interface AlertInstanceState { | AlertThreadPoolRejectionsState | AlertNodeState | AlertLicenseState + | AlertNodesChangedState >; [x: string]: unknown; } @@ -72,6 +78,7 @@ export interface AlertState { export interface AlertNodeState extends AlertState { nodeId: string; nodeName?: string; + meta: any; [key: string]: unknown; } @@ -98,6 +105,10 @@ export interface AlertLicenseState extends AlertState { expiryDateMS: number; } +export interface AlertNodesChangedState extends AlertState { + node: AlertClusterStatsNode; +} + export interface AlertUiState { isFiring: boolean; resolvedMS?: number; @@ -224,5 +235,30 @@ export interface AlertLicense { type: string; expiryDateMS: number; clusterUuid: string; - ccs: string; + ccs?: string; +} + +export interface AlertClusterStatsNodes { + clusterUuid: string; + recentNodes: AlertClusterStatsNode[]; + priorNodes: AlertClusterStatsNode[]; + ccs?: string; +} + +export interface AlertClusterStatsNode { + nodeUuid: string; + nodeEphemeralId: string; + nodeName: string; +} + +export interface AlertClusterHealth { + health: AlertClusterHealthType; + clusterUuid: string; + ccs?: string; +} + +export interface AlertVersions { + clusterUuid: string; + ccs?: string; + versions: string[]; } diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index 853e140ec66c7..5afb0c7fe6fd1 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -26,7 +26,16 @@ export interface ElasticsearchSourceKibanaStats { export interface ElasticsearchSource { timestamp: string; + cluster_uuid: string; kibana_stats?: ElasticsearchSourceKibanaStats; + cluster_state?: { + status?: string; + }; + cluster_stats?: { + nodes?: { + versions?: string[]; + }; + }; beats_stats?: { timestamp?: string; beat?: { diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index d7c33ae85cc9d..b4baac7990bc7 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -12,13 +12,23 @@ import { AlertState, AlertMessage, AlertMessageLinkToken, - LegacyAlert, + CommonAlertParams, + AlertClusterHealth, + AlertInstanceState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_CLUSTER_HEALTH, LEGACY_ALERT_DETAILS } from '../../common/constants'; -import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums'; +import { + ALERT_CLUSTER_HEALTH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; +import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { defaultMessage: 'Allocate missing primary and replica shards', @@ -36,12 +46,6 @@ export class ClusterHealthAlert extends BaseAlert { super(rawAlert, { id: ALERT_CLUSTER_HEALTH, name: LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label, - legacy: { - watchName: 'elasticsearch_cluster_status', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.clusterHealth.nodeNameLabel', { - defaultMessage: 'Elasticsearch cluster alert', - }), - }, actionVariables: [ { name: 'clusterHealth', @@ -57,15 +61,42 @@ export class ClusterHealthAlert extends BaseAlert { }); } - private getHealth(legacyAlert: LegacyAlert) { - return legacyAlert.prefix - .replace('Elasticsearch cluster status is ', '') - .slice(0, -1) as AlertClusterHealthType; + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const healths = await fetchClusterHealth( + callCluster, + clusters, + esIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return healths.map((clusterHealth) => { + const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; + const severity = + clusterHealth.health === AlertClusterHealthType.Red + ? AlertSeverity.Danger + : AlertSeverity.Warning; + + return { + shouldFire, + severity, + meta: clusterHealth, + clusterUuid: clusterHealth.clusterUuid, + ccs: clusterHealth.ccs, + }; + }); } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const health = this.getHealth(legacyAlert); + const { health } = item.meta as AlertClusterHealth; return { text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.firingMessage', { defaultMessage: `Elasticsearch cluster health is {health}.`, @@ -97,52 +128,54 @@ export class ClusterHealthAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const health = this.getHealth(legacyAlert); - if (alertState.ui.isFiring) { - const actionText = - health === AlertClusterHealthType.Red - ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { - defaultMessage: `Allocate missing primary and replica shards.`, - }) - : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { - defaultMessage: `Allocate missing replica shards.`, - }); - - const action = `[${actionText}](elasticsearch/indices)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, - values: { - clusterName: cluster.clusterName, - health, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, - values: { - clusterName: cluster.clusterName, - health, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterHealth: health, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, - }); + // This alert does not feature grouping + if (alertStates.length !== 1) { + return; } + const state = alertStates[0]; + const { health } = state.meta as AlertClusterHealth; + const actionText = + health === AlertClusterHealthType.Red + ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { + defaultMessage: `Allocate missing primary and replica shards.`, + }) + : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { + defaultMessage: `Allocate missing replica shards.`, + }); + + const action = `[${actionText}](elasticsearch/indices)`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, + values: { + clusterName: cluster.clusterName, + health, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, + values: { + clusterName: cluster.clusterName, + health, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterHealth: health, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index 9002fb54fcf23..f7740b3c47958 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -11,29 +11,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_ELASTICSEARCH_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; export class ElasticsearchVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_ELASTICSEARCH_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label, - legacy: { - watchName: 'elasticsearch_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Elasticsearch node alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -50,15 +50,42 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const elasticsearchVersions = await fetchElasticsearchVersions( + callCluster, + clusters, + esIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return elasticsearchVersions.map((elasticsearchVersion) => { + return { + shouldFire: elasticsearchVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: elasticsearchVersion, + clusterUuid: elasticsearchVersion.clusterUuid, + ccs: elasticsearchVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate( 'xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Elasticsearch ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, } ); @@ -70,54 +97,62 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + // This alert does not feature grouping + if (alertStates.length !== 1) { + return; + } + + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'elasticsearch/nodes', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'Verify you have the same version across all nodes.', + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', { - defaultMessage: 'View nodes', + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, } - ); - const action = `[${fullActionText}](elasticsearch/nodes)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index 0d394b8f45ecc..3d379a3853a92 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -11,29 +11,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_KIBANA_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_KIBANA_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_KIBANA, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; export class KibanaVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_KIBANA_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label, - legacy: { - watchName: 'kibana_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Kibana instance alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -63,13 +63,40 @@ export class KibanaVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_KIBANA); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const kibanaVersions = await fetchKibanaVersions( + callCluster, + clusters, + esIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return kibanaVersions.map((kibanaVersion) => { + return { + shouldFire: kibanaVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: kibanaVersion, + clusterUuid: kibanaVersion.clusterUuid, + ccs: kibanaVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Kibana ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, }); @@ -80,54 +107,63 @@ export class KibanaVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', - { - defaultMessage: 'Verify you have the same version across all instances.', - } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + // This alert does not feature grouping + if (alertStates.length !== 1) { + return; + } + + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all instances.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + { + defaultMessage: 'View instances', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'kibana/instances', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, + } + ); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'View instances', + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const action = `[${fullActionText}](kibana/instances)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index a396e0c86e660..7dd8feb17472e 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -87,15 +87,15 @@ export class LicenseExpirationAlert extends BaseAlert { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const stats = await fetchLicenses( + const licenses = await fetchLicenses( callCluster, clusters, esIndexPattern, Globals.app.config.ui.max_bucket_size ); - return stats.map((stat) => { - const { clusterUuid, type, expiryDateMS, status, ccs } = stat; + return licenses.map((license) => { + const { clusterUuid, type, expiryDateMS, status, ccs } = license; const $expiry = moment.utc(expiryDateMS); let isExpired = false; let severity = AlertSeverity.Success; @@ -121,7 +121,7 @@ export class LicenseExpirationAlert extends BaseAlert { return { shouldFire: isExpired, severity, - meta: stat, + meta: license, clusterUuid, ccs, }; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 4eae3cd12eed4..7c87a02f8bc4d 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -11,29 +11,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_LOGSTASH_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_LOGSTASH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_LOGSTASH, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; export class LogstashVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_LOGSTASH_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label, - legacy: { - watchName: 'logstash_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Logstash node alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -50,15 +50,42 @@ export class LogstashVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_LOGSTASH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const logstashVersions = await fetchLogstashVersions( + callCluster, + clusters, + esIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return logstashVersions.map((logstashVersion) => { + return { + shouldFire: logstashVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: logstashVersion, + clusterUuid: logstashVersion.clusterUuid, + ccs: logstashVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate( 'xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Logstash ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, } ); @@ -70,54 +97,62 @@ export class LogstashVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + // This alert does not feature grouping + if (alertStates.length !== 1) { + return; + } + + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'logstash/nodes', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'Verify you have the same version across all nodes.', + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', { - defaultMessage: 'View nodes', + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, } - ); - const action = `[${fullActionText}](logstash/nodes)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 09c12d345d930..12cf5389e7978 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -11,26 +11,61 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, - LegacyAlertNodesChangedList, + AlertClusterStatsNodes, + AlertClusterStatsNode, + CommonAlertParams, + AlertInstanceState, + AlertNodesChangedState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_NODES_CHANGED, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_NODES_CHANGED, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { AlertSeverity } from '../../common/enums'; + +interface AlertNodesChangedStates { + removed: AlertClusterStatsNode[]; + added: AlertClusterStatsNode[]; + restarted: AlertClusterStatsNode[]; +} + +function getNodeStates(nodes: AlertClusterStatsNodes): AlertNodesChangedStates { + const removed = nodes.priorNodes.filter( + (priorNode) => + !nodes.recentNodes.find((recentNode) => priorNode.nodeUuid === recentNode.nodeUuid) + ); + const added = nodes.recentNodes.filter( + (recentNode) => + !nodes.priorNodes.find((priorNode) => priorNode.nodeUuid === recentNode.nodeUuid) + ); + const restarted = nodes.recentNodes.filter( + (recentNode) => + nodes.priorNodes.find((priorNode) => priorNode.nodeUuid === recentNode.nodeUuid) && + !nodes.priorNodes.find( + (priorNode) => priorNode.nodeEphemeralId === recentNode.nodeEphemeralId + ) + ); + + return { + removed, + added, + restarted, + }; +} export class NodesChangedAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_NODES_CHANGED, name: LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label, - legacy: { - watchName: 'elasticsearch_nodes', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.nodesChanged.nodeNameLabel', { - defaultMessage: 'Elasticsearch nodes alert', - }), - changeDataValues: { shouldFire: true }, - }, actionVariables: [ { name: 'added', @@ -64,13 +99,39 @@ export class NodesChangedAlert extends BaseAlert { }); } - private getNodeStates(legacyAlert: LegacyAlert): LegacyAlertNodesChangedList { - return legacyAlert.nodes || { added: {}, removed: {}, restarted: {} }; + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const nodesFromClusterStats = await fetchNodesFromClusterStats( + callCluster, + clusters, + esIndexPattern + ); + return nodesFromClusterStats.map((nodes) => { + const { removed, added, restarted } = getNodeStates(nodes); + const shouldFire = removed.length > 0 || added.length > 0 || restarted.length > 0; + const severity = AlertSeverity.Warning; + + return { + shouldFire, + severity, + meta: nodes, + clusterUuid: nodes.clusterUuid, + ccs: nodes.ccs, + }; + }); } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const states = this.getNodeStates(legacyAlert); + const nodes = item.meta as AlertClusterStatsNodes; + const states = getNodeStates(nodes); if (!alertState.ui.isFiring) { return { text: i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage', { @@ -79,11 +140,7 @@ export class NodesChangedAlert extends BaseAlert { }; } - if ( - Object.values(states.added).length === 0 && - Object.values(states.removed).length === 0 && - Object.values(states.restarted).length === 0 - ) { + if (states.added.length === 0 && states.removed.length === 0 && states.restarted.length === 0) { return { text: i18n.translate( 'xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage', @@ -95,29 +152,29 @@ export class NodesChangedAlert extends BaseAlert { } const addedText = - Object.values(states.added).length > 0 + states.added.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage', { defaultMessage: `Elasticsearch nodes '{added}' added to this cluster.`, values: { - added: Object.values(states.added).join(','), + added: states.added.map((n) => n.nodeName).join(','), }, }) : null; const removedText = - Object.values(states.removed).length > 0 + states.removed.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage', { defaultMessage: `Elasticsearch nodes '{removed}' removed from this cluster.`, values: { - removed: Object.values(states.removed).join(','), + removed: states.removed.map((n) => n.nodeName).join(','), }, }) : null; const restartedText = - Object.values(states.restarted).length > 0 + states.restarted.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage', { defaultMessage: `Elasticsearch nodes '{restarted}' restarted in this cluster.`, values: { - restarted: Object.values(states.restarted).join(','), + restarted: states.restarted.map((n) => n.nodeName).join(','), }, }) : null; @@ -129,55 +186,59 @@ export class NodesChangedAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { - defaultMessage: 'Verify that you added, removed, or restarted nodes.', - }); - const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { - defaultMessage: 'View nodes', - }); - const action = `[${fullActionText}](elasticsearch/nodes)`; - const states = this.getNodeStates(legacyAlert); - const added = Object.values(states.added).join(','); - const removed = Object.values(states.removed).join(','); - const restarted = Object.values(states.restarted).join(','); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, - values: { - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - actionPlain: shortActionText, - }); + // This alert does not feature grouping + if (alertStates.length !== 1) { + return; } + + const state = alertStates[0] as AlertNodesChangedState; + const nodes = state.meta as AlertClusterStatsNodes; + const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { + defaultMessage: 'Verify that you added, removed, or restarted nodes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { + defaultMessage: 'View nodes', + }); + const action = `[${fullActionText}](elasticsearch/nodes)`; + const states = getNodeStates(nodes); + const added = states.added.join(','); + const removed = states.removed.join(','); + const restarted = states.restarted.join(','); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, + values: { + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts new file mode 100644 index 0000000000000..44ba9706b8f59 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -0,0 +1,62 @@ +/* + * 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 { AlertCluster, AlertClusterHealth } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +export async function fetchClusterHealth( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_state.status', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + return { + health: hit._source.cluster_state?.status, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : undefined, + } as AlertClusterHealth; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts new file mode 100644 index 0000000000000..e7fcaae780419 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -0,0 +1,69 @@ +/* + * 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 { AlertCluster, AlertVersions } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +export async function fetchElasticsearchVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_stats.nodes.versions', + 'hits.hits._index', + 'hits.hits._source.cluster_uuid', + ], + body: { + size: 1, + sort: [ + { + timestamp: { + order: 'desc', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + const versions = hit._source.cluster_stats?.nodes?.versions; + return { + versions, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts new file mode 100644 index 0000000000000..9a3c797233f16 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; + +interface ESAggResponse { + key: string; +} + +export async function fetchKibanaVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'kibana_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + cluster: { + terms: { + field: 'cluster_uuid', + size: 1, + }, + aggs: { + group_by_kibana: { + terms: { + field: 'kibana_stats.kibana.uuid', + size, + }, + aggs: { + group_by_version: { + terms: { + field: 'kibana_stats.kibana.version', + size: 1, + order: { + latest_report: 'desc', + }, + }, + aggs: { + latest_report: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const indexName = get(response, 'aggregations.index.buckets[0].key', ''); + const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; + return clusterList.map((cluster) => { + const clusterUuid = cluster.key; + const uuids = get(cluster, 'group_by_kibana.buckets', []); + const byVersion: { [version: string]: boolean } = {}; + for (const uuid of uuids) { + const version = get(uuid, 'group_by_version.buckets[0].key', ''); + if (!version) { + continue; + } + byVersion[version] = true; + } + return { + versions: Object.keys(byVersion), + clusterUuid, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts new file mode 100644 index 0000000000000..d3ff3d45acc72 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; + +interface ESAggResponse { + key: string; +} + +export async function fetchLogstashVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'logstash_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + cluster: { + terms: { + field: 'cluster_uuid', + size: 1, + }, + aggs: { + group_by_logstash: { + terms: { + field: 'logstash_stats.logstash.uuid', + size, + }, + aggs: { + group_by_version: { + terms: { + field: 'logstash_stats.logstash.version', + size: 1, + order: { + latest_report: 'desc', + }, + }, + aggs: { + latest_report: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const indexName = get(response, 'aggregations.index.buckets[0].key', ''); + const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; + return clusterList.map((cluster) => { + const clusterUuid = cluster.key; + const uuids = get(cluster, 'group_by_logstash.buckets', []); + const byVersion: { [version: string]: boolean } = {}; + for (const uuid of uuids) { + const version = get(uuid, 'group_by_version.buckets[0].key', ''); + if (!version) { + continue; + } + byVersion[version] = true; + } + return { + versions: Object.keys(byVersion), + clusterUuid, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts new file mode 100644 index 0000000000000..49838244761da --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertClusterStatsNodes } from '../../../common/types/alerts'; + +interface ClusterStateNodesESResponse { + [nodeUuid: string]: { + name: string; + ephemeral_id: string; + }; +} + +function formatNode(nodes: ClusterStateNodesESResponse) { + return Object.keys(nodes).map((nodeUuid) => { + return { + nodeUuid, + nodeEphemeralId: nodes[nodeUuid].ephemeral_id, + nodeName: nodes[nodeUuid].name, + }; + }); +} + +export async function fetchNodesFromClusterStats( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + return await Promise.all( + clusters.map(async (cluster) => { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_state.nodes_hash', + 'hits.hits._source.cluster_state.nodes', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size: 2, + sort: [ + { + timestamp: { + order: 'desc', + }, + }, + ], + query: { + bool: { + filter: [ + { + term: { + cluster_uuid: cluster.clusterUuid, + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2d', + }, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + const hits = get(response, 'hits.hits', []); + const indexName = get(hits[0], '_index', ''); + return { + clusterUuid: get(hits[0], '_source.cluster_uuid', ''), + recentNodes: formatNode(get(hits[0], '_source.cluster_state.nodes')), + priorNodes: formatNode(get(hits[1], '_source.cluster_state.nodes')), + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }) + ); +} From a677d8871f2c423cd21fcb23186f36e6f073c4dd Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 5 Jan 2021 13:37:51 -0500 Subject: [PATCH 03/13] Add back in the one test file --- .../lib/alerts/fetch_cluster_health.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts new file mode 100644 index 0000000000000..4841966bcdce1 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { fetchClusterHealth } from './fetch_cluster_health'; + +describe('fetchClusterHealth', () => { + it('should return the cluster health', async () => { + const status = 'green'; + const clusterUuid = 'sdfdsaj34434'; + const callCluster = jest.fn(() => ({ + hits: { + hits: [ + { + _index: '.monitoring-es-7', + _source: { + cluster_state: { + status, + }, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + + const clusters = [{ clusterUuid, clusterName: 'foo' }]; + const index = '.monitoring-es-*'; + + const health = await fetchClusterHealth(callCluster, clusters, index, 1); + expect(health).toEqual([ + { + health: status, + clusterUuid, + ccs: undefined, + }, + ]); + }); +}); From 2c75ff2fb03caf855c50046d82c1d5e81f2a2576 Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 5 Jan 2021 13:48:22 -0500 Subject: [PATCH 04/13] Remove deprecated code --- .../monitoring/server/alerts/base_alert.ts | 69 +------------------ 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 2f7921c188503..80d6b6120af81 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -25,7 +25,6 @@ import { AlertEnableAction, CommonAlertFilter, CommonAlertParams, - LegacyAlert, } from '../../common/types/alerts'; import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; @@ -254,10 +253,6 @@ export class BaseAlert { params as CommonAlertParams, availableCcs ); - // if (this.alertOptions.legacy) { - // const data = await this.fetchLegacyData(callCluster, clusters, availableCcs); - // return await this.processLegacyData(data, clusters, services, state); - // } const data = await this.fetchData(params, callCluster, clusters, availableCcs); return await this.processData(data, clusters, services, state); } @@ -292,39 +287,9 @@ export class BaseAlert { clusters: AlertCluster[], availableCcs: string[] ): Promise> { - return []; - // throw new Error('Child classes must implement `fetchData`'); + throw new Error('Child classes must implement `fetchData`'); } - // protected async fetchLegacyData( - // callCluster: CallCluster, - // clusters: AlertCluster[], - // availableCcs: string[] - // ): Promise { - // let alertIndexPattern = INDEX_ALERTS; - // if (availableCcs) { - // alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); - // } - // const legacyAlerts = await fetchLegacyAlerts( - // callCluster, - // clusters, - // alertIndexPattern, - // this.alertOptions.legacy!.watchName, - // Globals.app.config.ui.max_bucket_size - // ); - - // return legacyAlerts.map((legacyAlert) => { - // return { - // clusterUuid: legacyAlert.metadata.cluster_uuid, - // shouldFire: !legacyAlert.resolved_timestamp, - // severity: mapLegacySeverity(legacyAlert.metadata.severity), - // meta: legacyAlert, - // nodeName: this.alertOptions.legacy!.nodeNameLabel, - // ...this.alertOptions.legacy!.changeDataValues, - // }; - // }); - // } - protected async processData( data: AlertData[], clusters: AlertCluster[], @@ -379,34 +344,6 @@ export class BaseAlert { return state; } - protected async processLegacyData( - data: AlertData[], - clusters: AlertCluster[], - services: AlertServices, - state: ExecutedState - ) { - const currentUTC = +new Date(); - for (const item of data) { - const instanceId = `${this.alertOptions.id}:${item.clusterUuid}`; - const instance = services.alertInstanceFactory(instanceId); - if (!item.shouldFire) { - instance.replaceState({ alertStates: [] }); - continue; - } - const cluster = clusters.find((c: AlertCluster) => c.clusterUuid === item.clusterUuid); - const alertState: AlertState = this.getDefaultAlertState(cluster!, item); - alertState.nodeName = item.nodeName; - alertState.ui.triggeredMS = currentUTC; - alertState.ui.isFiring = true; - alertState.ui.severity = item.severity; - alertState.ui.message = this.getUiMessage(alertState, item); - instance.replaceState({ alertStates: [alertState] }); - this.executeActions(instance, alertState, item, cluster); - } - state.lastChecked = currentUTC; - return state; - } - protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { return { cluster, @@ -421,10 +358,6 @@ export class BaseAlert { }; } - protected getVersions(legacyAlert: LegacyAlert) { - return `[${legacyAlert.message.match(/(?<=Versions: \[).+?(?=\])/)}]`; - } - protected getUiMessage( alertState: AlertState | unknown, item: AlertData | unknown From ef9080124f72d6d718f48d2ffcb61370ec8da514 Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 19 Jan 2021 13:49:49 -0500 Subject: [PATCH 05/13] Fix up tests --- .../alerts/cluster_health_alert.test.ts | 40 ++++++---- ...asticsearch_version_mismatch_alert.test.ts | 48 ++++++------ .../kibana_version_mismatch_alert.test.ts | 47 +++++++----- .../alerts/license_expiration_alert.test.ts | 1 + .../logstash_version_mismatch_alert.test.ts | 47 +++++++----- .../server/alerts/nodes_changed_alert.test.ts | 74 +++++++++++++------ .../server/alerts/nodes_changed_alert.ts | 6 +- 7 files changed, 161 insertions(+), 102 deletions(-) diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index fbf81bc3513f6..79685fcd8b263 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -5,7 +5,8 @@ */ import { ClusterHealthAlert } from './cluster_health_alert'; import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { AlertClusterHealthType, AlertSeverity } from '../../common/enums'; +import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; @@ -24,8 +25,8 @@ jest.mock('../static_globals', () => ({ }, })); -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_cluster_health', () => ({ + fetchClusterHealth: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -61,16 +62,16 @@ describe('ClusterHealthAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'Elasticsearch cluster status is yellow.', - message: 'Allocate missing replica shards.', - metadata: { - severity: 2000, - cluster_uuid: clusterUuid, + const healths = [ + { + health: AlertClusterHealthType.Yellow, + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -92,8 +93,8 @@ describe('ClusterHealthAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchClusterHealth as jest.Mock).mockImplementation(() => { + return healths; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -118,8 +119,15 @@ describe('ClusterHealthAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Elasticsearch cluster alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + health: AlertClusterHealthType.Yellow, + }, ui: { isFiring: true, message: { @@ -138,7 +146,7 @@ describe('ClusterHealthAlert', () => { }, ], }, - severity: 'danger', + severity: AlertSeverity.Warning, triggeredMS: 1, lastCheckedMS: 0, }, @@ -159,7 +167,7 @@ describe('ClusterHealthAlert', () => { }); it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + (fetchClusterHealth as jest.Mock).mockImplementation(() => { return []; }); const alert = new ClusterHealthAlert(); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index ed39cbea3381c..ff273f4fc7aab 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -5,13 +5,13 @@ */ import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_elasticsearch_versions', () => ({ + fetchElasticsearchVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -20,6 +20,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -65,16 +66,16 @@ describe('ElasticsearchVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Elasticsearch.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const elasticsearchVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -96,8 +97,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { + return elasticsearchVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -123,13 +124,19 @@ describe('ElasticsearchVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Elasticsearch node alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: - 'Multiple versions of Elasticsearch ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Elasticsearch (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -139,20 +146,19 @@ describe('ElasticsearchVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View nodes](elasticsearch/nodes)', + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: - 'Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running [8.0.0, 7.2.1]. [View nodes](elasticsearch/nodes)', + internalFullMessage: `Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { return []; }); const alert = new ElasticsearchVersionMismatchAlert(); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 96464e16f8c72..34302dcda9ea0 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -5,13 +5,13 @@ */ import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_kibana_versions', () => ({ + fetchKibanaVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -20,6 +20,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -68,16 +69,16 @@ describe('KibanaVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Kibana.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const kibanaVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -99,8 +100,8 @@ describe('KibanaVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchKibanaVersions as jest.Mock).mockImplementation(() => { + return kibanaVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -125,12 +126,19 @@ describe('KibanaVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Kibana instance alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: 'Multiple versions of Kibana ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Kibana (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -140,20 +148,19 @@ describe('KibanaVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View instances](kibana/instances)', + action: `[View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all instances.', - internalFullMessage: - 'Kibana version mismatch alert is firing for testCluster. Kibana is running [8.0.0, 7.2.1]. [View instances](kibana/instances)', + internalFullMessage: `Kibana version mismatch alert is firing for testCluster. Kibana is running 8.0.0, 7.2.1. [View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + (fetchKibanaVersions as jest.Mock).mockImplementation(() => { return []; }); const alert = new KibanaVersionMismatchAlert(); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index 92436cb3f38c1..16ab8fdbf9d0d 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -23,6 +23,7 @@ jest.mock('moment', () => { }; }; moment.duration = () => ({ humanize: () => 'HUMANIZED_DURATION' }); + moment.utc = () => ''; return moment; }); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index dd23cfc76dc6d..4cd9ce2dcfe87 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -5,13 +5,13 @@ */ import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_logstash_versions', () => ({ + fetchLogstashVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -20,6 +20,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -66,16 +67,16 @@ describe('LogstashVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Logstash.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const logstashVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -97,8 +98,8 @@ describe('LogstashVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchLogstashVersions as jest.Mock).mockImplementation(() => { + return logstashVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -124,12 +125,19 @@ describe('LogstashVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Logstash node alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: 'Multiple versions of Logstash ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Logstash (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -139,20 +147,19 @@ describe('LogstashVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View nodes](logstash/nodes)', + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: - 'Logstash version mismatch alert is firing for testCluster. Logstash is running [8.0.0, 7.2.1]. [View nodes](logstash/nodes)', + internalFullMessage: `Logstash version mismatch alert is firing for testCluster. Logstash is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + (fetchLogstashVersions as jest.Mock).mockImplementation(() => { return []; }); const alert = new LogstashVersionMismatchAlert(); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index e6017e799cba8..477d326a3c3e0 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -5,13 +5,13 @@ */ import { NodesChangedAlert } from './nodes_changed_alert'; import { ALERT_NODES_CHANGED } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_nodes_from_cluster_stats', () => ({ + fetchNodesFromClusterStats: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -71,23 +71,33 @@ describe('NodesChangedAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const nodeUuid = 'myNodeUuid'; + const nodeEphemeralId = 'myEphemeralId'; + const nodeEphemeralIdChanged = 'myEphemeralIdChanged'; + const nodeName = 'test'; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'Elasticsearch cluster nodes have changed!', - message: 'Node was restarted [1]: [test].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, - }, - nodes: { - added: {}, - removed: {}, - restarted: { - test: 'test', - }, + const nodes = [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -109,8 +119,8 @@ describe('NodesChangedAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return nodes; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -136,8 +146,28 @@ describe('NodesChangedAlert', () => { alertStates: [ { cluster: { clusterUuid, clusterName }, - ccs: undefined, - nodeName: 'Elasticsearch nodes alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + }, ui: { isFiring: true, message: { @@ -166,7 +196,7 @@ describe('NodesChangedAlert', () => { }); it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { return []; }); const alert = new NodesChangedAlert(); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 12cf5389e7978..ff4ca86bc6a80 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -205,9 +205,9 @@ export class NodesChangedAlert extends BaseAlert { }); const action = `[${fullActionText}](elasticsearch/nodes)`; const states = getNodeStates(nodes); - const added = states.added.join(','); - const removed = states.removed.join(','); - const restarted = states.restarted.join(','); + const added = states.added.map((node) => node.nodeName).join(','); + const removed = states.removed.map((node) => node.nodeName).join(','); + const restarted = states.restarted.map((node) => node.nodeName).join(','); instance.scheduleActions('default', { internalShortMessage: i18n.translate( 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', From 654e73e164e931b079544d13dfb365f283fe2541 Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 19 Jan 2021 15:07:30 -0500 Subject: [PATCH 06/13] Add test files --- .../fetch_elasticsearch_versions.test.ts | 51 +++++++++++++ .../lib/alerts/fetch_kibana_versions.test.ts | 73 +++++++++++++++++++ .../alerts/fetch_logstash_versions.test.ts | 73 +++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts new file mode 100644 index 0000000000000..0bb3c676139ed --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fetchElasticsearchVersions } from './fetch_elasticsearch_versions'; + +describe('fetchElasticsearchVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-es-*'; + const size = 10; + const versions = ['8.0.0', '7.2.1']; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + hits: { + hits: [ + { + _index: `Monitoring:${index}`, + _source: { + cluster_uuid: 'cluster123', + cluster_stats: { + nodes: { + versions, + }, + }, + }, + }, + ], + }, + }; + }); + + const result = await fetchElasticsearchVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions, + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts new file mode 100644 index 0000000000000..a4c477d2d0692 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { fetchKibanaVersions } from './fetch_kibana_versions'; + +describe('fetchKibanaVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-kibana-*'; + const size = 10; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + aggregations: { + index: { + buckets: [ + { + key: `Monitoring:${index}`, + }, + ], + }, + cluster: { + buckets: [ + { + key: 'cluster123', + group_by_kibana: { + buckets: [ + { + group_by_version: { + buckets: [ + { + key: '8.0.0', + }, + ], + }, + }, + { + group_by_version: { + buckets: [ + { + key: '7.2.1', + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + const result = await fetchKibanaVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions: ['8.0.0', '7.2.1'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts new file mode 100644 index 0000000000000..c3b0f6b0162fe --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { fetchLogstashVersions } from './fetch_logstash_versions'; + +describe('fetchLogstashVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-logstash-*'; + const size = 10; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + aggregations: { + index: { + buckets: [ + { + key: `Monitoring:${index}`, + }, + ], + }, + cluster: { + buckets: [ + { + key: 'cluster123', + group_by_logstash: { + buckets: [ + { + group_by_version: { + buckets: [ + { + key: '8.0.0', + }, + ], + }, + }, + { + group_by_version: { + buckets: [ + { + key: '7.2.1', + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + const result = await fetchLogstashVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions: ['8.0.0', '7.2.1'], + }, + ]); + }); +}); From 7ab849d7dc9ba324abca87ff8de2c0fe7e9e6a7a Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 19 Jan 2021 16:12:38 -0500 Subject: [PATCH 07/13] Fix i18n --- x-pack/plugins/translations/translations/ja-JP.json | 6 ------ x-pack/plugins/translations/translations/zh-CN.json | 6 ------ 2 files changed, 12 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6dd72d179b210..548ec780e1dfc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14517,7 +14517,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{action}", "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "クラスターの正常性", - "xpack.monitoring.alerts.clusterHealth.nodeNameLabel": "Elasticsearch クラスターアラート", "xpack.monitoring.alerts.clusterHealth.redMessage": "見つからないプライマリおよびレプリカシャードを割り当て", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearchクラスターの正常性は{health}です。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}. #start_linkView now#end_link", @@ -14557,7 +14556,6 @@ "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "{clusterName}に対してElasticsearchバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch バージョン不一致", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel": "Elasticsearch ノードアラート", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Elasticsearch({versions})が実行されています。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, other {日}}", @@ -14571,7 +14569,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "{clusterName}に対してKibanaバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "インスタンスを表示", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana バージョン不一致", - "xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel": "Kibana インスタンスアラート", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "すべてのインスタンスのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Kibana({versions})が実行されています。", "xpack.monitoring.alerts.legacyAlert.expressionText": "構成するものがありません。", @@ -14582,7 +14579,6 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "ライセンス有効期限アラートが {clusterName} に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "ライセンス有効期限アラートが {clusterName} に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.nodeNameLabel": "Elasticsearch クラスターアラート", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは#absoluteの#relativeに期限切れになります。#start_linkライセンスを更新してください。#end_link", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "このクラスターを実行している Logstash のバージョン。", "xpack.monitoring.alerts.logstashVersionMismatch.description": "クラスターに複数のバージョンの Logstash があるときにアラートを発行します。", @@ -14590,7 +14586,6 @@ "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "{clusterName}に対してLogstashバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash バージョン不一致", - "xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel": "Logstash ノードアラート", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Logstash({versions})が実行されています。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "高メモリー使用率を報告しているノード数。", @@ -14633,7 +14628,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "{clusterName}に対してノード変更アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "ノードの表示", "xpack.monitoring.alerts.nodesChanged.label": "ノードが変更されました", - "xpack.monitoring.alerts.nodesChanged.nodeNameLabel": "Elasticsearch ノードアラート", "xpack.monitoring.alerts.nodesChanged.shortAction": "ノードを追加、削除、または再起動したことを確認してください。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearchノード「{added}」がこのクラスターに追加されました。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearchノードが変更されました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3ab1c7ee56a32..199af754bd3fe 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14559,7 +14559,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{action}", "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "集群运行状况", - "xpack.monitoring.alerts.clusterHealth.nodeNameLabel": "Elasticsearch 集群告警", "xpack.monitoring.alerts.clusterHealth.redMessage": "分配缺失的主分片和副本分片", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearch 集群运行状况为 {health}。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}。#start_link立即查看#end_link", @@ -14599,7 +14598,6 @@ "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Elasticsearch 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch 版本不匹配", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel": "Elasticsearch 节点告警", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Elasticsearch ({versions}) 版本。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, other {天}}", @@ -14613,7 +14611,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Kibana 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "查看实例", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana 版本不匹配", - "xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel": "Kibana 实例告警", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "确认所有实例具有相同的版本。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Kibana 版本 ({versions})。", "xpack.monitoring.alerts.legacyAlert.expressionText": "没有可配置的内容。", @@ -14624,7 +14621,6 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "许可证到期", - "xpack.monitoring.alerts.licenseExpiration.nodeNameLabel": "Elasticsearch 集群告警", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将于 #relative后,即 #absolute到期。 #start_link请更新您的许可证。#end_link", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "此集群中运行的 Logstash 版本。", "xpack.monitoring.alerts.logstashVersionMismatch.description": "集群包含多个版本的 Logstash 时告警。", @@ -14632,7 +14628,6 @@ "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Logstash 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash 版本不匹配", - "xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel": "Logstash 节点告警", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Logstash 版本 ({versions})。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "报告高内存使用率的节点数目。", @@ -14675,7 +14670,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "为 {clusterName} 触发了节点已更改告警。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "查看节点", "xpack.monitoring.alerts.nodesChanged.label": "节点已更改", - "xpack.monitoring.alerts.nodesChanged.nodeNameLabel": "Elasticsearch 节点告警", "xpack.monitoring.alerts.nodesChanged.shortAction": "确认您已添加、移除或重新启动节点。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearch 节点“{added}”已添加到此集群。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearch 节点已更改", From 80eff8e5f058aead0e0dd2b492edde968a6d5717 Mon Sep 17 00:00:00 2001 From: chrisronline Date: Wed, 20 Jan 2021 09:56:20 -0500 Subject: [PATCH 08/13] Update tests --- .../monitoring/server/alerts/base_alert.ts | 6 ----- .../alerts/cluster_health_alert.test.ts | 10 ++++++-- ...asticsearch_version_mismatch_alert.test.ts | 10 ++++++-- .../kibana_version_mismatch_alert.test.ts | 10 ++++++-- .../alerts/license_expiration_alert.test.ts | 17 +++++++++++--- .../logstash_version_mismatch_alert.test.ts | 10 ++++++-- .../server/alerts/nodes_changed_alert.test.ts | 23 +++++++++++++++++-- 7 files changed, 67 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index df6265ec2f53c..6a7a7bb5cab2e 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -112,12 +112,6 @@ export class BaseAlert { } public isEnabled(licenseService: MonitoringLicenseService) { - // if (this.alertOptions.legacy) { - // const watcherFeature = licenseService.getWatcherFeature(); - // if (!watcherFeature.isAvailable || !watcherFeature.isEnabled) { - // return false; - // } - // } return true; } diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 79685fcd8b263..a3bec8a4f9f33 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -166,9 +166,15 @@ describe('ClusterHealthAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { + it('should not fire actions if the cluster health is green', async () => { (fetchClusterHealth as jest.Mock).mockImplementation(() => { - return []; + return [ + { + health: AlertClusterHealthType.Green, + clusterUuid, + ccs, + }, + ]; }); const alert = new ClusterHealthAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index ff273f4fc7aab..6de2e12be041d 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -157,9 +157,15 @@ describe('ElasticsearchVersionMismatchAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { + it('should not fire actions if there is no mismatch', async () => { (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { - return []; + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new ElasticsearchVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 34302dcda9ea0..a11e107386292 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -159,9 +159,15 @@ describe('KibanaVersionMismatchAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { + it('should not fire actions if there is no mismatch', async () => { (fetchKibanaVersions as jest.Mock).mockImplementation(() => { - return []; + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new KibanaVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index 16ab8fdbf9d0d..1add0f7bc6fe6 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -23,7 +23,11 @@ jest.mock('moment', () => { }; }; moment.duration = () => ({ humanize: () => 'HUMANIZED_DURATION' }); - moment.utc = () => ''; + moment.utc = () => ({ + add: () => ({ + isAfter: () => false, + }), + }); return moment; }); @@ -186,9 +190,16 @@ describe('LicenseExpirationAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { + it('should not fire actions if the license is not expired', async () => { (fetchLicenses as jest.Mock).mockImplementation(() => { - return []; + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1, + clusterUuid, + }, + ]; }); const alert = new LicenseExpirationAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index 4cd9ce2dcfe87..ffce384a911f9 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -158,9 +158,15 @@ describe('LogstashVersionMismatchAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { + it('should not fire actions if there is no mismatch', async () => { (fetchLogstashVersions as jest.Mock).mockImplementation(() => { - return []; + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new LogstashVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 477d326a3c3e0..6e8aa941bd6f8 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -195,9 +195,28 @@ describe('NodesChangedAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { + it('should not fire actions if no nodes have changed', async () => { (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { - return []; + return [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + clusterUuid, + ccs, + }, + ]; }); const alert = new NodesChangedAlert(); const type = alert.getAlertType(); From 956f761fb4da4c73f8f3fffa48006300c51991f1 Mon Sep 17 00:00:00 2001 From: chrisronline Date: Fri, 5 Feb 2021 12:39:36 -0500 Subject: [PATCH 09/13] PR feedback --- .../plugins/monitoring/common/types/alerts.ts | 4 +- x-pack/plugins/monitoring/common/types/es.ts | 5 +- .../server/alerts/cluster_health_alert.ts | 14 +- .../elasticsearch_version_mismatch_alert.ts | 5 +- .../alerts/kibana_version_mismatch_alert.ts | 11 +- .../server/alerts/license_expiration_alert.ts | 5 +- .../alerts/logstash_version_mismatch_alert.ts | 11 +- .../server/alerts/nodes_changed_alert.ts | 5 +- .../server/lib/alerts/fetch_cluster_health.ts | 19 ++- .../alerts/fetch_elasticsearch_versions.ts | 8 +- .../lib/alerts/fetch_legacy_alerts.test.ts | 96 ------------- .../server/lib/alerts/fetch_legacy_alerts.ts | 97 ------------- .../server/lib/alerts/fetch_licenses.ts | 23 ++-- .../alerts/fetch_nodes_from_cluster_stats.ts | 128 ++++++++++-------- .../server/lib/alerts/fetch_status.ts | 2 +- .../server/routes/api/v1/alerts/enable.ts | 3 +- 16 files changed, 140 insertions(+), 296 deletions(-) delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 34400f727007d..649b92cb7ac82 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -262,8 +262,8 @@ export interface AlertClusterStatsNodes { export interface AlertClusterStatsNode { nodeUuid: string; - nodeEphemeralId: string; - nodeName: string; + nodeEphemeralId?: string; + nodeName?: string; } export interface AlertClusterHealth { diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index cb3d44d0080ed..75e167a483f98 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -154,7 +154,10 @@ export interface ElasticsearchLegacySource { cluster_state?: { status?: string; nodes?: { - [nodeUuid: string]: {}; + [nodeUuid: string]: { + ephemeral_id?: string; + name?: string; + }; }; master_node?: boolean; }; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 2b327a4399d08..c4e5de3d55356 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -72,13 +72,7 @@ export class ClusterHealthAlert extends BaseAlert { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const healths = await fetchClusterHealth( - callCluster, - clusters, - esIndexPattern, - Globals.app.config.ui.max_bucket_size - ); - + const healths = await fetchClusterHealth(callCluster, clusters, esIndexPattern); return healths.map((clusterHealth) => { const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; const severity = @@ -133,10 +127,12 @@ export class ClusterHealthAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - // This alert does not feature grouping - if (alertStates.length !== 1) { + if (alertStates.length === 0) { return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state const state = alertStates[0]; const { health } = state.meta as AlertClusterHealth; const actionText = diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index 45addaa81201e..e8e93e4b3afec 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -102,11 +102,12 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - // This alert does not feature grouping - if (alertStates.length !== 1) { + if (alertStates.length === 0) { return; } + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state const state = alertStates[0]; const { versions } = state.meta as AlertVersions; const shortActionText = i18n.translate( diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index 424f4fccabf4a..f1f8959787003 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -70,14 +70,14 @@ export class KibanaVersionMismatchAlert extends BaseAlert { clusters: AlertCluster[], availableCcs: string[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_KIBANA); + let kibanaIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_KIBANA); if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + kibanaIndexPattern = getCcsIndexPattern(kibanaIndexPattern, availableCcs); } const kibanaVersions = await fetchKibanaVersions( callCluster, clusters, - esIndexPattern, + kibanaIndexPattern, Globals.app.config.ui.max_bucket_size ); @@ -112,11 +112,12 @@ export class KibanaVersionMismatchAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - // This alert does not feature grouping - if (alertStates.length !== 1) { + if (alertStates.length === 0) { return; } + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state const state = alertStates[0]; const { versions } = state.meta as AlertVersions; const shortActionText = i18n.translate( diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index c96d6bc8e6598..269e9444820a0 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -165,11 +165,12 @@ export class LicenseExpirationAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - // This alert does not feature grouping - if (alertStates.length !== 1) { + if (alertStates.length === 0) { return; } + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state const state: AlertLicenseState = alertStates[0] as AlertLicenseState; const $expiry = moment.utc(state.expiryDateMS); const $duration = moment.duration(+new Date() - $expiry.valueOf()); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 8aedbdc7f92d4..d903dd49600ad 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -57,14 +57,14 @@ export class LogstashVersionMismatchAlert extends BaseAlert { clusters: AlertCluster[], availableCcs: string[] ): Promise { - let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_LOGSTASH); + let logstashIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_LOGSTASH); if (availableCcs) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + logstashIndexPattern = getCcsIndexPattern(logstashIndexPattern, availableCcs); } const logstashVersions = await fetchLogstashVersions( callCluster, clusters, - esIndexPattern, + logstashIndexPattern, Globals.app.config.ui.max_bucket_size ); @@ -102,11 +102,12 @@ export class LogstashVersionMismatchAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - // This alert does not feature grouping - if (alertStates.length !== 1) { + if (alertStates.length === 0) { return; } + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state const state = alertStates[0]; const { versions } = state.meta as AlertVersions; const shortActionText = i18n.translate( diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 7ef58b8aca805..63b3ef672405e 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -191,11 +191,12 @@ export class NodesChangedAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - // This alert does not feature grouping - if (alertStates.length !== 1) { + if (alertStates.length === 0) { return; } + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state const state = alertStates[0] as AlertNodesChangedState; const nodes = state.meta as AlertClusterStatsNodes; const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts index 44ba9706b8f59..bcfa2da0958a2 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -1,7 +1,8 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { AlertCluster, AlertClusterHealth } from '../../../common/types/alerts'; import { ElasticsearchSource } from '../../../common/types/es'; @@ -9,8 +10,7 @@ import { ElasticsearchSource } from '../../../common/types/es'; export async function fetchClusterHealth( callCluster: any, clusters: AlertCluster[], - index: string, - size: number + index: string ): Promise { const params = { index, @@ -20,8 +20,15 @@ export async function fetchClusterHealth( 'hits.hits._index', ], body: { - size, - sort: [{ timestamp: { order: 'desc' } }], + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], query: { bool: { filter: [ diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts index e7fcaae780419..373ddb62aaee8 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -1,7 +1,8 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; import { ElasticsearchSource } from '../../../common/types/es'; @@ -20,11 +21,12 @@ export async function fetchElasticsearchVersions( 'hits.hits._source.cluster_uuid', ], body: { - size: 1, + size: clusters.length, sort: [ { timestamp: { order: 'desc', + unmapped_type: 'long', }, }, ], diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts deleted file mode 100644 index 086c5c7da9139..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { fetchLegacyAlerts } from './fetch_legacy_alerts'; - -describe('fetchLegacyAlerts', () => { - let callCluster = jest.fn(); - const clusters = [ - { - clusterUuid: 'abc123', - clusterName: 'test', - }, - ]; - const index = '.monitoring-es-*'; - const size = 10; - - it('fetch legacy alerts', async () => { - const prefix = 'thePrefix'; - const message = 'theMessage'; - const nodes = {}; - const metadata = { - severity: 2000, - cluster_uuid: clusters[0].clusterUuid, - metadata: {}, - }; - callCluster = jest.fn().mockImplementation(() => { - return { - hits: { - hits: [ - { - _source: { - prefix, - message, - nodes, - metadata, - }, - }, - ], - }, - }; - }); - const result = await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); - expect(result).toEqual([ - { - message, - metadata, - nodes, - nodeName: '', - prefix, - }, - ]); - }); - - it('should use consistent params', async () => { - let params = null; - callCluster = jest.fn().mockImplementation((...args) => { - params = args[1]; - }); - await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); - expect(params).toStrictEqual({ - index, - filterPath: [ - 'hits.hits._source.prefix', - 'hits.hits._source.message', - 'hits.hits._source.resolved_timestamp', - 'hits.hits._source.nodes', - 'hits.hits._source.metadata.*', - ], - body: { - size, - sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], - query: { - bool: { - minimum_should_match: 1, - filter: [ - { - terms: { 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid) }, - }, - { term: { 'metadata.watch': 'myWatch' } }, - ], - should: [ - { range: { timestamp: { gte: 'now-2m' } } }, - { range: { resolved_timestamp: { gte: 'now-2m' } } }, - { bool: { must_not: { exists: { field: 'resolved_timestamp' } } } }, - ], - }, - }, - collapse: { field: 'metadata.cluster_uuid' }, - }, - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts deleted file mode 100644 index 96438da111b6d..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get } from 'lodash'; -import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../../common/types/alerts'; - -export async function fetchLegacyAlerts( - callCluster: any, - clusters: AlertCluster[], - index: string, - watchName: string, - size: number -): Promise { - const params = { - index, - filterPath: [ - 'hits.hits._source.prefix', - 'hits.hits._source.message', - 'hits.hits._source.resolved_timestamp', - 'hits.hits._source.nodes', - 'hits.hits._source.metadata.*', - ], - body: { - size, - sort: [ - { - timestamp: { - order: 'desc', - unmapped_type: 'long', - }, - }, - ], - query: { - bool: { - minimum_should_match: 1, - filter: [ - { - terms: { - 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid), - }, - }, - { - term: { - 'metadata.watch': watchName, - }, - }, - ], - should: [ - { - range: { - timestamp: { - gte: 'now-2m', - }, - }, - }, - { - range: { - resolved_timestamp: { - gte: 'now-2m', - }, - }, - }, - { - bool: { - must_not: { - exists: { - field: 'resolved_timestamp', - }, - }, - }, - }, - ], - }, - }, - collapse: { - field: 'metadata.cluster_uuid', - }, - }, - }; - - const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - const legacyAlert: LegacyAlert = { - prefix: get(hit, '_source.prefix'), - message: get(hit, '_source.message'), - resolved_timestamp: get(hit, '_source.resolved_timestamp'), - nodes: get(hit, '_source.nodes'), - nodeName: '', // This is set by BaseAlert - metadata: get(hit, '_source.metadata') as LegacyAlertMetadata, - }; - return legacyAlert; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 9fd018efa855e..c428d4681890c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -1,9 +1,9 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ -import { get } from 'lodash'; import { AlertLicense, AlertCluster } from '../../../common/types/alerts'; export async function fetchLicenses( @@ -21,7 +21,14 @@ export async function fetchLicenses( ], body: { size, - sort: [{ timestamp: { order: 'desc' } }], + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], query: { bool: { filter: [ @@ -52,14 +59,14 @@ export async function fetchLicenses( }; const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - const rawLicense: any = get(hit, '_source.license', {}); + return response.hits.hits.map((hit: any) => { + const rawLicense: any = hit._source.license; const license: AlertLicense = { status: rawLicense.status, type: rawLicense.type, expiryDateMS: rawLicense.expiry_date_in_millis, - clusterUuid: get(hit, '_source.cluster_uuid'), - ccs: get(hit, '_index'), + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index, }; return license; }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index 49838244761da..c399594c170fa 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -1,19 +1,18 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ -import { get } from 'lodash'; import { AlertCluster, AlertClusterStatsNodes } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; -interface ClusterStateNodesESResponse { - [nodeUuid: string]: { - name: string; - ephemeral_id: string; - }; -} - -function formatNode(nodes: ClusterStateNodesESResponse) { +function formatNode( + nodes: NonNullable['nodes']> | undefined +) { + if (!nodes) { + return []; + } return Object.keys(nodes).map((nodeUuid) => { return { nodeUuid, @@ -28,60 +27,79 @@ export async function fetchNodesFromClusterStats( clusters: AlertCluster[], index: string ): Promise { - return await Promise.all( - clusters.map(async (cluster) => { - const params = { - index, - filterPath: [ - 'hits.hits._source.cluster_state.nodes_hash', - 'hits.hits._source.cluster_state.nodes', - 'hits.hits._source.cluster_uuid', - 'hits.hits._index', - ], - body: { - size: 2, - sort: [ + const params = { + index, + filterPath: ['aggregations.clusters.buckets'], + body: { + size: 0, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ { - timestamp: { - order: 'desc', + term: { + type: 'cluster_stats', }, }, - ], - query: { - bool: { - filter: [ - { - term: { - cluster_uuid: cluster.clusterUuid, - }, - }, - { - term: { - type: 'cluster_stats', - }, + { + range: { + timestamp: { + gte: 'now-2m', }, - { - range: { + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + include: clusters.map((cluster) => cluster.clusterUuid), + field: 'cluster_uuid', + }, + aggs: { + top: { + top_hits: { + sort: [ + { timestamp: { - gte: 'now-2d', + order: 'desc', + unmapped_type: 'long', }, }, + ], + _source: { + includes: ['cluster_state.nodes_hash', 'cluster_state.nodes'], }, - ], + size: 2, + }, }, }, }, - }; + }, + }, + }; - const response = await callCluster('search', params); - const hits = get(response, 'hits.hits', []); - const indexName = get(hits[0], '_index', ''); - return { - clusterUuid: get(hits[0], '_source.cluster_uuid', ''), - recentNodes: formatNode(get(hits[0], '_source.cluster_state.nodes')), - priorNodes: formatNode(get(hits[1], '_source.cluster_state.nodes')), - ccs: indexName.includes(':') ? indexName.split(':')[0] : null, - }; - }) - ); + const response = await callCluster('search', params); + const nodes = []; + const clusterBuckets = response.aggregations.clusters.buckets; + for (const clusterBucket of clusterBuckets) { + const clusterUuid = clusterBucket.key; + const hits = clusterBucket.top.hits.hits; + const indexName = hits[0]._index; + nodes.push({ + clusterUuid, + recentNodes: formatNode(hits[0]._source.cluster_state?.nodes), + priorNodes: formatNode(hits[1]._source.cluster_state?.nodes), + ccs: indexName.includes(':') ? indexName.split(':')[0] : undefined, + }); + } + return nodes; } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 6c08a0b3db758..399b26a6c5c31 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -28,7 +28,7 @@ export async function fetchStatus( await Promise.all( (alertTypes || ALERTS).map(async (type) => { const alert = await AlertsFactory.getByType(type, alertsClient); - if (!alert || !alert.isEnabled(licenseService) || !alert.rawAlert) { + if (!alert || !alert.rawAlert) { return; } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index a8389e26d4f9f..901ea96d525e8 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -25,8 +25,7 @@ export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) }, async (context, request, response) => { try { - const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService)); - + const alerts = AlertsFactory.getAll(); if (alerts.length) { const { isSufficientlySecure, From ca6c7cfa26c91716d7ca621ffed5365511afde9f Mon Sep 17 00:00:00 2001 From: chrisronline Date: Fri, 5 Feb 2021 13:28:52 -0500 Subject: [PATCH 10/13] Fix types and tests --- x-pack/plugins/monitoring/common/types/es.ts | 1 + .../server/alerts/license_expiration_alert.ts | 7 +--- .../lib/alerts/fetch_cluster_health.test.ts | 7 ++-- .../server/lib/alerts/fetch_licenses.test.ts | 12 +++---- .../server/lib/alerts/fetch_licenses.ts | 32 ++++++++++--------- 5 files changed, 29 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index 75e167a483f98..9dce32211f4b1 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -173,6 +173,7 @@ export interface ElasticsearchLegacySource { license?: { status?: string; type?: string; + expiry_date_in_millis?: number; }; logstash_state?: { pipeline?: { diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index 269e9444820a0..d25eda30b3071 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -87,12 +87,7 @@ export class LicenseExpirationAlert extends BaseAlert { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const licenses = await fetchLicenses( - callCluster, - clusters, - esIndexPattern, - Globals.app.config.ui.max_bucket_size - ); + const licenses = await fetchLicenses(callCluster, clusters, esIndexPattern); return licenses.map((license) => { const { clusterUuid, type, expiryDateMS, status, ccs } = license; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts index 4841966bcdce1..2fdbbe80b7e89 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts @@ -1,7 +1,8 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { fetchClusterHealth } from './fetch_cluster_health'; @@ -29,7 +30,7 @@ describe('fetchClusterHealth', () => { const clusters = [{ clusterUuid, clusterName: 'foo' }]; const index = '.monitoring-es-*'; - const health = await fetchClusterHealth(callCluster, clusters, index, 1); + const health = await fetchClusterHealth(callCluster, clusters, index); expect(health).toEqual([ { health: status, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts index 0d60824f4b0ba..715c8c50a45e7 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -1,14 +1,14 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { fetchLicenses } from './fetch_licenses'; describe('fetchLicenses', () => { const clusterName = 'MyCluster'; const clusterUuid = 'clusterA'; - const size = 1000; const license = { status: 'active', expiry_date_in_millis: 1579532493876, @@ -30,7 +30,7 @@ describe('fetchLicenses', () => { })); const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; - const result = await fetchLicenses(callCluster, clusters, index, size); + const result = await fetchLicenses(callCluster, clusters, index); expect(result).toEqual([ { status: license.status, @@ -45,7 +45,7 @@ describe('fetchLicenses', () => { const callCluster = jest.fn(); const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; - await fetchLicenses(callCluster, clusters, index, size); + await fetchLicenses(callCluster, clusters, index); const params = callCluster.mock.calls[0][1]; expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); }); @@ -54,7 +54,7 @@ describe('fetchLicenses', () => { const callCluster = jest.fn(); const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; - await fetchLicenses(callCluster, clusters, index, size); + await fetchLicenses(callCluster, clusters, index); const params = callCluster.mock.calls[0][1]; expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index c428d4681890c..6cec7f3296926 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -5,12 +5,12 @@ * 2.0. */ import { AlertLicense, AlertCluster } from '../../../common/types/alerts'; +import { ElasticsearchResponse } from '../../../common/types/es'; export async function fetchLicenses( callCluster: any, clusters: AlertCluster[], - index: string, - size: number + index: string ): Promise { const params = { index, @@ -20,7 +20,7 @@ export async function fetchLicenses( 'hits.hits._index', ], body: { - size, + size: clusters.length, sort: [ { timestamp: { @@ -58,16 +58,18 @@ export async function fetchLicenses( }, }; - const response = await callCluster('search', params); - return response.hits.hits.map((hit: any) => { - const rawLicense: any = hit._source.license; - const license: AlertLicense = { - status: rawLicense.status, - type: rawLicense.type, - expiryDateMS: rawLicense.expiry_date_in_millis, - clusterUuid: hit._source.cluster_uuid, - ccs: hit._index, - }; - return license; - }); + const response: ElasticsearchResponse = await callCluster('search', params); + return ( + response?.hits?.hits.map((hit) => { + const rawLicense = hit._source.license ?? {}; + const license: AlertLicense = { + status: rawLicense.status ?? '', + type: rawLicense.type ?? '', + expiryDateMS: rawLicense.expiry_date_in_millis ?? 0, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index, + }; + return license; + }) ?? [] + ); } From aa4df51916f87e16d912e0c8b6c450db371f058e Mon Sep 17 00:00:00 2001 From: chrisronline Date: Fri, 5 Feb 2021 15:15:19 -0500 Subject: [PATCH 11/13] Fix license headers --- .../server/lib/alerts/fetch_elasticsearch_versions.test.ts | 5 +++-- .../server/lib/alerts/fetch_kibana_versions.test.ts | 5 +++-- .../monitoring/server/lib/alerts/fetch_kibana_versions.ts | 5 +++-- .../server/lib/alerts/fetch_logstash_versions.test.ts | 5 +++-- .../monitoring/server/lib/alerts/fetch_logstash_versions.ts | 5 +++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts index 0bb3c676139ed..e4f4a4d364ebf 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts @@ -1,7 +1,8 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { fetchElasticsearchVersions } from './fetch_elasticsearch_versions'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts index a4c477d2d0692..518828ef0b1c8 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -1,7 +1,8 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { fetchKibanaVersions } from './fetch_kibana_versions'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts index 9a3c797233f16..2e7fe192df656 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -1,7 +1,8 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { get } from 'lodash'; import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts index c3b0f6b0162fe..a739593df27e9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -1,7 +1,8 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { fetchLogstashVersions } from './fetch_logstash_versions'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts index d3ff3d45acc72..8f20c64d6243e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -1,7 +1,8 @@ /* * 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. + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { get } from 'lodash'; import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; From deaa335a35390713fb4cc06aefd503c94ba257d4 Mon Sep 17 00:00:00 2001 From: chrisronline Date: Fri, 5 Feb 2021 16:23:58 -0500 Subject: [PATCH 12/13] Remove unused function --- x-pack/plugins/monitoring/server/alerts/base_alert.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 9e93a6bcfc2c1..e79eb78f7f66b 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -32,7 +32,6 @@ import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; -import { MonitoringLicenseService } from '../types'; import { mbSafeQuery } from '../lib/mb_safe_query'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerts/common/parse_duration'; @@ -116,10 +115,6 @@ export class BaseAlert { }; } - public isEnabled(licenseService: MonitoringLicenseService) { - return true; - } - public getId() { return this.rawAlert?.id; } From 7854ae24cc1b0ade78326cdf009b340ac96b697f Mon Sep 17 00:00:00 2001 From: chrisronline Date: Fri, 5 Feb 2021 17:28:00 -0500 Subject: [PATCH 13/13] Fix faulty license expiration logic --- .../alerts/license_expiration_alert.test.ts | 64 ++++++++++++++----- .../server/alerts/license_expiration_alert.ts | 11 ++-- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index 849296adc0bb4..0d1c1d20097e5 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -7,6 +7,7 @@ import { LicenseExpirationAlert } from './license_expiration_alert'; import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; +import { AlertSeverity } from '../../common/enums'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; @@ -19,17 +20,8 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); jest.mock('moment', () => { - const moment = function () { - return { - format: () => 'THE_DATE', - }; - }; + const moment = function () {}; moment.duration = () => ({ humanize: () => 'HUMANIZED_DURATION' }); - moment.utc = () => ({ - add: () => ({ - isAfter: () => false, - }), - }); return moment; }); @@ -84,7 +76,7 @@ describe('LicenseExpirationAlert', () => { const license = { status: 'expired', type: 'gold', - expiryDateMS: 1, + expiryDateMS: 1000 * 60 * 60 * 24 * 59, clusterUuid, }; @@ -138,7 +130,7 @@ describe('LicenseExpirationAlert', () => { itemLabel: undefined, meta: { clusterUuid: 'abc123', - expiryDateMS: 1, + expiryDateMS: 5097600000, status: 'expired', type: 'gold', }, @@ -155,14 +147,14 @@ describe('LicenseExpirationAlert', () => { type: 'time', isRelative: true, isAbsolute: false, - timestamp: 1, + timestamp: 5097600000, }, { startToken: '#absolute', type: 'time', isAbsolute: true, isRelative: false, - timestamp: 1, + timestamp: 5097600000, }, { startToken: '#start_link', @@ -198,7 +190,7 @@ describe('LicenseExpirationAlert', () => { { status: 'active', type: 'gold', - expiryDateMS: 1, + expiryDateMS: 1000 * 60 * 60 * 24 * 61, clusterUuid, }, ]; @@ -213,5 +205,47 @@ describe('LicenseExpirationAlert', () => { expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); }); + + it('should use danger severity for a license expiring soon', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 2, + clusterUuid, + }, + ]; + }); + const alert = new LicenseExpirationAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Danger); + }); + + it('should use warning severity for a license expiring in a bit', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 31, + clusterUuid, + }, + ]; + }); + const alert = new LicenseExpirationAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Warning); + }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index d25eda30b3071..24fbd98ef2e8b 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { BaseAlert } from './base_alert'; @@ -91,7 +90,6 @@ export class LicenseExpirationAlert extends BaseAlert { return licenses.map((license) => { const { clusterUuid, type, expiryDateMS, status, ccs } = license; - const $expiry = moment.utc(expiryDateMS); let isExpired = false; let severity = AlertSeverity.Success; @@ -104,10 +102,10 @@ export class LicenseExpirationAlert extends BaseAlert { break; } - const $fromNow = moment.utc().add(EXPIRES_DAYS[i], 'days'); - if ($fromNow.isAfter($expiry)) { + const fromNow = +new Date() + EXPIRES_DAYS[i] * 1000 * 60 * 60 * 24; + if (fromNow >= expiryDateMS) { isExpired = true; - severity = i > 1 ? AlertSeverity.Warning : AlertSeverity.Danger; + severity = i < 1 ? AlertSeverity.Warning : AlertSeverity.Danger; break; } } @@ -167,8 +165,7 @@ export class LicenseExpirationAlert extends BaseAlert { // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) // However, some alerts operate on the state of the cluster itself and are only concerned with a single state const state: AlertLicenseState = alertStates[0] as AlertLicenseState; - const $expiry = moment.utc(state.expiryDateMS); - const $duration = moment.duration(+new Date() - $expiry.valueOf()); + const $duration = moment.duration(+new Date() - state.expiryDateMS); const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { defaultMessage: 'Please update your license.', });