[Monitoring] Migrate data source for legacy alerts to monitoring data directly (#87377)

* License expiration

* Fetch legacy alert data from the source

* Add back in the one test file

* Remove deprecated code

* Fix up tests

* Add test files

* Fix i18n

* Update tests

* PR feedback

* Fix types and tests

* Fix license headers

* Remove unused function

* Fix faulty license expiration logic
This commit is contained in:
Chris Roberson 2021-02-08 21:50:07 -05:00 committed by GitHub
parent 87212e68f7
commit 231610c720
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1809 additions and 799 deletions

View file

@ -6,7 +6,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<AlertTypeParams> | SanitizedAlert<AlertTypeParams>;
@ -60,6 +65,8 @@ export interface AlertInstanceState {
| AlertDiskUsageState
| AlertThreadPoolRejectionsState
| AlertNodeState
| AlertLicenseState
| AlertNodesChangedState
>;
[x: string]: unknown;
}
@ -74,6 +81,7 @@ export interface AlertState {
export interface AlertNodeState extends AlertState {
nodeId: string;
nodeName?: string;
meta: any;
[key: string]: unknown;
}
@ -96,6 +104,14 @@ export interface AlertThreadPoolRejectionsState extends AlertState {
nodeName?: string;
}
export interface AlertLicenseState extends AlertState {
expiryDateMS: number;
}
export interface AlertNodesChangedState extends AlertState {
node: AlertClusterStatsNode;
}
export interface AlertUiState {
isFiring: boolean;
resolvedMS?: number;
@ -228,3 +244,36 @@ export interface LegacyAlertNodesChangedList {
added: { [nodeName: string]: string };
restarted: { [nodeName: string]: string };
}
export interface AlertLicense {
status: string;
type: string;
expiryDateMS: number;
clusterUuid: 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[];
}

View file

@ -154,7 +154,10 @@ export interface ElasticsearchLegacySource {
cluster_state?: {
status?: string;
nodes?: {
[nodeUuid: string]: {};
[nodeUuid: string]: {
ephemeral_id?: string;
name?: string;
};
};
master_node?: boolean;
};
@ -170,6 +173,7 @@ export interface ElasticsearchLegacySource {
license?: {
status?: string;
type?: string;
expiry_date_in_millis?: number;
};
logstash_state?: {
pipeline?: {

View file

@ -26,26 +26,16 @@ import {
AlertEnableAction,
CommonAlertFilter,
CommonAlertParams,
LegacyAlert,
} from '../../common/types/alerts';
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<AlertData>;
}
type ExecutedState =
| {
@ -60,7 +50,6 @@ interface AlertOptions {
name: string;
throttle?: string | null;
interval?: string;
legacy?: LegacyOptions;
defaultParams?: Partial<CommonAlertParams>;
actionVariables: Array<{ name: string; description: string }>;
fetchClustersRange?: number;
@ -126,16 +115,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;
}
public getId() {
return this.rawAlert?.id;
}
@ -271,10 +250,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);
}
@ -312,35 +287,6 @@ export class BaseAlert {
throw new Error('Child classes must implement `fetchData`');
}
protected async fetchLegacyData(
callCluster: CallCluster,
clusters: AlertCluster[],
availableCcs: string[]
): Promise<AlertData[]> {
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[],
@ -395,34 +341,6 @@ export class BaseAlert {
return state;
}
protected async processLegacyData(
data: AlertData[],
clusters: AlertCluster[],
services: AlertServices<AlertInstanceState, never, 'default'>,
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,
@ -437,10 +355,6 @@ export class BaseAlert {
};
}
protected getVersions(legacyAlert: LegacyAlert) {
return `[${legacyAlert.message.match(/(?<=Versions: \[).+?(?=\])/)}]`;
}
protected getUiMessage(
alertState: AlertState | unknown,
item: AlertData | unknown

View file

@ -7,7 +7,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;
@ -26,8 +27,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(),
@ -63,16 +64,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();
@ -94,8 +95,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 }];
@ -120,8 +121,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: {
@ -140,7 +148,7 @@ describe('ClusterHealthAlert', () => {
},
],
},
severity: 'danger',
severity: AlertSeverity.Warning,
triggeredMS: 1,
lastCheckedMS: 0,
},
@ -160,9 +168,15 @@ describe('ClusterHealthAlert', () => {
});
});
it('should not fire actions if there is no legacy alert', async () => {
(fetchLegacyAlerts as jest.Mock).mockImplementation(() => {
return [];
it('should not fire actions if the cluster health is green', async () => {
(fetchClusterHealth as jest.Mock).mockImplementation(() => {
return [
{
health: AlertClusterHealthType.Green,
clusterUuid,
ccs,
},
];
});
const alert = new ClusterHealthAlert();
const type = alert.getAlertType();

View file

@ -13,13 +13,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',
@ -37,12 +47,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',
@ -58,15 +62,36 @@ 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<AlertData[]> {
let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH);
if (availableCcs) {
esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs);
}
const healths = await fetchClusterHealth(callCluster, clusters, esIndexPattern);
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}.`,
@ -98,52 +123,56 @@ 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,
});
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 =
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,
});
}
}

View file

@ -7,13 +7,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(),
@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({
jest.mock('../static_globals', () => ({
Globals: {
app: {
url: 'UNIT_TEST_URL',
getLogger: () => ({ debug: jest.fn() }),
config: {
ui: {
@ -67,16 +68,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();
@ -98,8 +99,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 }];
@ -125,13 +126,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,
@ -141,21 +148,26 @@ 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(() => {
return [];
it('should not fire actions if there is no mismatch', async () => {
(fetchElasticsearchVersions as jest.Mock).mockImplementation(() => {
return [
{
versions: ['8.0.0'],
clusterUuid,
ccs,
},
];
});
const alert = new ElasticsearchVersionMismatchAlert();
const type = alert.getAlertType();

View file

@ -12,29 +12,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: [
{
@ -51,15 +51,42 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert {
});
}
protected async fetchData(
params: CommonAlertParams,
callCluster: any,
clusters: AlertCluster[],
availableCcs: string[]
): Promise<AlertData[]> {
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(', '),
},
}
);
@ -71,54 +98,63 @@ 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',
{
defaultMessage: 'Verify you have the same version across all nodes.',
}
);
const fullActionText = i18n.translate(
'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction',
{
defaultMessage: 'View nodes',
}
);
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,
});
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(
'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: `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: versions.join(', '),
action,
},
}
),
state: AlertingDefaults.ALERT_STATE.firing,
clusterName: cluster.clusterName,
versionList: versions,
action,
actionPlain: shortActionText,
});
}
}

View file

@ -7,13 +7,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(),
@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({
jest.mock('../static_globals', () => ({
Globals: {
app: {
url: 'UNIT_TEST_URL',
getLogger: () => ({ debug: jest.fn() }),
config: {
ui: {
@ -70,16 +71,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();
@ -101,8 +102,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 }];
@ -127,12 +128,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,
@ -142,21 +150,26 @@ 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(() => {
return [];
it('should not fire actions if there is no mismatch', async () => {
(fetchKibanaVersions as jest.Mock).mockImplementation(() => {
return [
{
versions: ['8.0.0'],
clusterUuid,
ccs,
},
];
});
const alert = new KibanaVersionMismatchAlert();
const type = alert.getAlertType();

View file

@ -12,29 +12,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: [
{
@ -64,13 +64,40 @@ export class KibanaVersionMismatchAlert extends BaseAlert {
});
}
protected async fetchData(
params: CommonAlertParams,
callCluster: any,
clusters: AlertCluster[],
availableCcs: string[]
): Promise<AlertData[]> {
let kibanaIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_KIBANA);
if (availableCcs) {
kibanaIndexPattern = getCcsIndexPattern(kibanaIndexPattern, availableCcs);
}
const kibanaVersions = await fetchKibanaVersions(
callCluster,
clusters,
kibanaIndexPattern,
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(', '),
},
});
@ -81,54 +108,64 @@ 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',
{
defaultMessage: 'View instances',
}
);
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,
});
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(
'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: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`,
values: {
clusterName: cluster.clusterName,
shortActionText,
},
}
),
internalFullMessage,
state: AlertingDefaults.ALERT_STATE.firing,
clusterName: cluster.clusterName,
versionList: versions,
action,
actionPlain: shortActionText,
});
}
}

View file

@ -7,23 +7,20 @@
import { LicenseExpirationAlert } from './license_expiration_alert';
import { ALERT_LICENSE_EXPIRATION } from '../../common/constants';
import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts';
import { AlertSeverity } from '../../common/enums';
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', () => {
const moment = function () {
return {
format: () => 'THE_DATE',
};
};
const moment = function () {};
moment.duration = () => ({ humanize: () => 'HUMANIZED_DURATION' });
return moment;
});
@ -76,15 +73,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: 1000 * 60 * 60 * 24 * 59,
clusterUuid,
};
const replaceState = jest.fn();
@ -107,8 +100,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 }];
@ -134,7 +127,15 @@ describe('LicenseExpirationAlert', () => {
{
cluster: { clusterUuid, clusterName },
ccs: undefined,
nodeName: 'Elasticsearch cluster alert',
itemLabel: undefined,
meta: {
clusterUuid: 'abc123',
expiryDateMS: 5097600000,
status: 'expired',
type: 'gold',
},
nodeId: undefined,
nodeName: undefined,
ui: {
isFiring: true,
message: {
@ -146,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',
@ -163,7 +164,7 @@ describe('LicenseExpirationAlert', () => {
},
],
},
severity: 'warning',
severity: 'danger',
triggeredMS: 1,
lastCheckedMS: 0,
},
@ -183,9 +184,16 @@ describe('LicenseExpirationAlert', () => {
});
});
it('should not fire actions if there is no legacy alert', async () => {
(fetchLegacyAlerts as jest.Mock).mockImplementation(() => {
return [];
it('should not fire actions if the license is not expired', async () => {
(fetchLicenses as jest.Mock).mockImplementation(() => {
return [
{
status: 'active',
type: 'gold',
expiryDateMS: 1000 * 60 * 60 * 24 * 61,
clusterUuid,
},
];
});
const alert = new LicenseExpirationAlert();
const type = alert.getAlertType();
@ -197,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);
});
});
});

View file

@ -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';
@ -15,26 +14,32 @@ import {
AlertMessage,
AlertMessageTimeToken,
AlertMessageLinkToken,
LegacyAlert,
AlertInstanceState,
CommonAlertParams,
AlertLicense,
AlertLicenseState,
} from '../../common/types/alerts';
import { AlertExecutorOptions, AlertInstance } from '../../../alerts/server';
import { ALERT_LICENSE_EXPIRATION, LEGACY_ALERT_DETAILS } from '../../common/constants';
import { AlertMessageTokenType } from '../../common/enums';
import {
ALERT_LICENSE_EXPIRATION,
LEGACY_ALERT_DETAILS,
INDEX_PATTERN_ELASTICSEARCH,
} from '../../common/constants';
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: [
{
@ -71,8 +76,53 @@ export class LicenseExpirationAlert extends BaseAlert {
return await super.execute(options);
}
protected async fetchData(
params: CommonAlertParams,
callCluster: any,
clusters: AlertCluster[],
availableCcs: string[]
): Promise<AlertData[]> {
let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH);
if (availableCcs) {
esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs);
}
const licenses = await fetchLicenses(callCluster, clusters, esIndexPattern);
return licenses.map((license) => {
const { clusterUuid, type, expiryDateMS, status, ccs } = license;
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 = +new Date() + EXPIRES_DAYS[i] * 1000 * 60 * 60 * 24;
if (fromNow >= expiryDateMS) {
isExpired = true;
severity = i < 1 ? AlertSeverity.Warning : AlertSeverity.Danger;
break;
}
}
}
return {
shouldFire: isExpired,
severity,
meta: license,
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`,
@ -83,14 +133,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',
@ -104,48 +154,51 @@ 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);
const $duration = moment.duration(+new Date() - $expiry.valueOf());
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 = $duration.humanize();
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,
});
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 $duration = moment.duration(+new Date() - state.expiryDateMS);
const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', {
defaultMessage: 'Please update your license.',
});
const action = `[${actionText}](elasticsearch/nodes)`;
const expiredDate = $duration.humanize();
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,
});
}
}

View file

@ -7,13 +7,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(),
@ -22,6 +22,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('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();
@ -99,8 +100,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 }];
@ -126,12 +127,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,
@ -141,21 +149,26 @@ 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(() => {
return [];
it('should not fire actions if there is no mismatch', async () => {
(fetchLogstashVersions as jest.Mock).mockImplementation(() => {
return [
{
versions: ['8.0.0'],
clusterUuid,
ccs,
},
];
});
const alert = new LogstashVersionMismatchAlert();
const type = alert.getAlertType();

View file

@ -12,29 +12,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: [
{
@ -51,15 +51,42 @@ export class LogstashVersionMismatchAlert extends BaseAlert {
});
}
protected async fetchData(
params: CommonAlertParams,
callCluster: any,
clusters: AlertCluster[],
availableCcs: string[]
): Promise<AlertData[]> {
let logstashIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_LOGSTASH);
if (availableCcs) {
logstashIndexPattern = getCcsIndexPattern(logstashIndexPattern, availableCcs);
}
const logstashVersions = await fetchLogstashVersions(
callCluster,
clusters,
logstashIndexPattern,
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(', '),
},
}
);
@ -71,54 +98,63 @@ 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',
{
defaultMessage: 'Verify you have the same version across all nodes.',
}
);
const fullActionText = i18n.translate(
'xpack.monitoring.alerts.logstashVersionMismatch.fullAction',
{
defaultMessage: 'View nodes',
}
);
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,
});
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(
'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: `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: versions.join(', '),
action,
},
}
),
state: AlertingDefaults.ALERT_STATE.firing,
clusterName: cluster.clusterName,
versionList: versions,
action,
actionPlain: shortActionText,
});
}
}

View file

@ -7,13 +7,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(),
@ -73,23 +73,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,
const nodes = [
{
recentNodes: [
{
nodeUuid,
nodeEphemeralId: nodeEphemeralIdChanged,
nodeName,
},
],
priorNodes: [
{
nodeUuid,
nodeEphemeralId,
nodeName,
},
],
clusterUuid,
ccs,
},
nodes: {
added: {},
removed: {},
restarted: {
test: 'test',
},
},
};
];
const replaceState = jest.fn();
const scheduleActions = jest.fn();
@ -111,8 +121,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 }];
@ -138,8 +148,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: {
@ -167,9 +197,28 @@ describe('NodesChangedAlert', () => {
});
});
it('should not fire actions if there is no legacy alert', async () => {
(fetchLegacyAlerts as jest.Mock).mockImplementation(() => {
return [];
it('should not fire actions if no nodes have changed', async () => {
(fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => {
return [
{
recentNodes: [
{
nodeUuid,
nodeEphemeralId,
nodeName,
},
],
priorNodes: [
{
nodeUuid,
nodeEphemeralId,
nodeName,
},
],
clusterUuid,
ccs,
},
];
});
const alert = new NodesChangedAlert();
const type = alert.getAlertType();

View file

@ -12,26 +12,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',
@ -65,13 +100,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<AlertData[]> {
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', {
@ -80,11 +141,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',
@ -96,29 +153,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;
@ -130,55 +187,60 @@ 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,
});
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', {
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.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',
{
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,
});
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 { 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);
expect(health).toEqual([
{
health: status,
clusterUuid,
ccs: undefined,
},
]);
});
});

View file

@ -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
* 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';
export async function fetchClusterHealth(
callCluster: any,
clusters: AlertCluster[],
index: string
): Promise<AlertClusterHealth[]> {
const params = {
index,
filterPath: [
'hits.hits._source.cluster_state.status',
'hits.hits._source.cluster_uuid',
'hits.hits._index',
],
body: {
size: clusters.length,
sort: [
{
timestamp: {
order: 'desc',
unmapped_type: 'long',
},
},
],
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;
});
}

View file

@ -0,0 +1,52 @@
/*
* 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 { 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,
},
]);
});
});

View file

@ -0,0 +1,71 @@
/*
* 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 { 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<AlertVersions[]> {
const params = {
index,
filterPath: [
'hits.hits._source.cluster_stats.nodes.versions',
'hits.hits._index',
'hits.hits._source.cluster_uuid',
],
body: {
size: clusters.length,
sort: [
{
timestamp: {
order: 'desc',
unmapped_type: 'long',
},
},
],
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,
};
});
}

View file

@ -0,0 +1,74 @@
/*
* 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 { 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'],
},
]);
});
});

View file

@ -0,0 +1,111 @@
/*
* 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 { AlertCluster, AlertVersions } from '../../../common/types/alerts';
interface ESAggResponse {
key: string;
}
export async function fetchKibanaVersions(
callCluster: any,
clusters: AlertCluster[],
index: string,
size: number
): Promise<AlertVersions[]> {
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,
};
});
}

View file

@ -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' },
},
});
});
});

View file

@ -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<LegacyAlert[]> {
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;
});
}

View file

@ -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
* 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 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);
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);
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);
const params = callCluster.mock.calls[0][1];
expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m');
});
});

View file

@ -0,0 +1,75 @@
/*
* 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 { AlertLicense, AlertCluster } from '../../../common/types/alerts';
import { ElasticsearchResponse } from '../../../common/types/es';
export async function fetchLicenses(
callCluster: any,
clusters: AlertCluster[],
index: string
): Promise<AlertLicense[]> {
const params = {
index,
filterPath: [
'hits.hits._source.license.*',
'hits.hits._source.cluster_uuid',
'hits.hits._index',
],
body: {
size: clusters.length,
sort: [
{
timestamp: {
order: 'desc',
unmapped_type: 'long',
},
},
],
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: 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;
}) ?? []
);
}

View file

@ -0,0 +1,74 @@
/*
* 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 { 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'],
},
]);
});
});

View file

@ -0,0 +1,111 @@
/*
* 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 { AlertCluster, AlertVersions } from '../../../common/types/alerts';
interface ESAggResponse {
key: string;
}
export async function fetchLogstashVersions(
callCluster: any,
clusters: AlertCluster[],
index: string,
size: number
): Promise<AlertVersions[]> {
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,
};
});
}

View file

@ -0,0 +1,105 @@
/*
* 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 { AlertCluster, AlertClusterStatsNodes } from '../../../common/types/alerts';
import { ElasticsearchSource } from '../../../common/types/es';
function formatNode(
nodes: NonNullable<NonNullable<ElasticsearchSource['cluster_state']>['nodes']> | undefined
) {
if (!nodes) {
return [];
}
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<AlertClusterStatsNodes[]> {
const params = {
index,
filterPath: ['aggregations.clusters.buckets'],
body: {
size: 0,
sort: [
{
timestamp: {
order: 'desc',
unmapped_type: 'long',
},
},
],
query: {
bool: {
filter: [
{
term: {
type: 'cluster_stats',
},
},
{
range: {
timestamp: {
gte: 'now-2m',
},
},
},
],
},
},
aggs: {
clusters: {
terms: {
include: clusters.map((cluster) => cluster.clusterUuid),
field: 'cluster_uuid',
},
aggs: {
top: {
top_hits: {
sort: [
{
timestamp: {
order: 'desc',
unmapped_type: 'long',
},
},
],
_source: {
includes: ['cluster_state.nodes_hash', 'cluster_state.nodes'],
},
size: 2,
},
},
},
},
},
},
};
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;
}

View file

@ -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;
}

View file

@ -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,

View file

@ -14427,7 +14427,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",
@ -14467,7 +14466,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 {日}}",
@ -14481,7 +14479,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": "構成するものがありません。",
@ -14492,7 +14489,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 があるときにアラートを発行します。",
@ -14500,7 +14496,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": "高メモリー使用率を報告しているノード数。",
@ -14543,7 +14538,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ードが変更されました",

View file

@ -14469,7 +14469,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",
@ -14509,7 +14508,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 {天}}",
@ -14523,7 +14521,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": "没有可配置的内容。",
@ -14534,7 +14531,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 时告警。",
@ -14542,7 +14538,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": "报告高内存使用率的节点数目。",
@ -14585,7 +14580,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 节点已更改",