mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
87212e68f7
commit
231610c720
32 changed files with 1809 additions and 799 deletions
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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?: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}) ?? []
|
||||
);
|
||||
}
|
|
@ -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'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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ノードが変更されました",
|
||||
|
|
|
@ -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 节点已更改",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue