diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 33c571110303f..e13d791ca162b 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -97,11 +97,9 @@ export interface AlertMemoryUsageState extends AlertNodeState { memoryUsage: number; } -export interface AlertThreadPoolRejectionsState extends AlertState { +export interface AlertThreadPoolRejectionsState extends AlertNodeState { rejectionCount: number; type: string; - nodeId: string; - nodeName?: string; } export interface AlertLicenseState extends AlertState { @@ -172,6 +170,7 @@ export interface AlertThreadPoolRejectionsStats { nodeId: string; nodeName: string; rejectionCount: number; + type: string; ccs?: string; } diff --git a/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts b/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts index 8329488694b6f..d4a0ad2af2a94 100644 --- a/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts +++ b/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts @@ -43,7 +43,7 @@ export class AlertingDefaults { clusterName: { name: 'clusterName', description: i18n.translate('xpack.monitoring.alerts.actionVariables.clusterName', { - defaultMessage: 'The cluster to which the nodes belong.', + defaultMessage: 'The cluster to which the node(s) belongs.', }), }, action: { diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 8954a4ae2486d..cfd7630a65dbc 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -274,46 +274,49 @@ export class BaseAlert { state: ExecutedState ) { const currentUTC = +new Date(); + // for each cluster filter the nodes that belong to this cluster for (const cluster of clusters) { const nodes = data.filter((node) => node.clusterUuid === cluster.clusterUuid); if (!nodes.length) { continue; } - const firingNodeUuids = nodes - .filter((node) => node.shouldFire) - .map((node) => node.meta.nodeId || node.meta.instanceId) - .join(','); - const instanceId = `${this.alertOptions.id}:${cluster.clusterUuid}:${firingNodeUuids}`; - const instance = services.alertInstanceFactory(instanceId); - const newAlertStates: AlertNodeState[] = []; const key = this.alertOptions.accessorKey; + + // for each node, update the alert's state with node state for (const node of nodes) { - if (!node.shouldFire) { - continue; + const newAlertStates: AlertNodeState[] = []; + // quick fix for now so that non node level alerts will use the cluster id + const instance = services.alertInstanceFactory( + node.meta.nodeId || node.meta.instanceId || cluster.clusterUuid + ); + + if (node.shouldFire) { + const { meta } = node; + // create a default alert state for this node and add data from node.meta and other data + const nodeState = this.getDefaultAlertState(cluster, node) as AlertNodeState; + if (key) { + nodeState[key] = meta[key]; + } + nodeState.nodeId = meta.nodeId || node.nodeId! || meta.instanceId; + // TODO: make these functions more generic, so it's node/item agnostic + nodeState.nodeName = meta.itemLabel || meta.nodeName || node.nodeName || nodeState.nodeId; + nodeState.itemLabel = meta.itemLabel; + nodeState.meta = meta; + nodeState.ui.triggeredMS = currentUTC; + nodeState.ui.isFiring = true; + nodeState.ui.severity = node.severity; + nodeState.ui.message = this.getUiMessage(nodeState, node); + // store the state of each node in array. + newAlertStates.push(nodeState); } - const { meta } = node; - const nodeState = this.getDefaultAlertState(cluster, node) as AlertNodeState; - if (key) { - nodeState[key] = meta[key]; + const alertInstanceState = { alertStates: newAlertStates }; + // update the alert's state with the new node states + instance.replaceState(alertInstanceState); + if (newAlertStates.length) { + this.executeActions(instance, alertInstanceState, null, cluster); + state.lastExecutedAction = currentUTC; } - nodeState.nodeId = meta.nodeId || node.nodeId! || meta.instanceId; - // TODO: make these functions more generic, so it's node/item agnostic - nodeState.nodeName = meta.itemLabel || meta.nodeName || node.nodeName || nodeState.nodeId; - nodeState.itemLabel = meta.itemLabel; - nodeState.meta = meta; - nodeState.ui.triggeredMS = currentUTC; - nodeState.ui.isFiring = true; - nodeState.ui.severity = node.severity; - nodeState.ui.message = this.getUiMessage(nodeState, node); - newAlertStates.push(nodeState); - } - - const alertInstanceState = { alertStates: newAlertStates }; - instance.replaceState(alertInstanceState); - if (newAlertStates.length) { - this.executeActions(instance, alertInstanceState, null, cluster); - state.lastExecutedAction = currentUTC; } } diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts index 6401c5213ee7d..2995566c7c096 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts @@ -240,7 +240,7 @@ export class CCRReadExceptionsAlert extends BaseAlert { 'xpack.monitoring.alerts.ccrReadExceptions.shortAction', { defaultMessage: - 'Verify follower and leader index relationships across the affected remote clusters.', + 'Verify follower and leader index relationships on the affected remote cluster.', } ); const fullActionText = i18n.translate('xpack.monitoring.alerts.ccrReadExceptions.fullAction', { @@ -258,7 +258,7 @@ export class CCRReadExceptionsAlert extends BaseAlert { const internalShortMessage = i18n.translate( 'xpack.monitoring.alerts.ccrReadExceptions.firing.internalShortMessage', { - defaultMessage: `CCR read exceptions alert is firing for the following remote clusters: {remoteClustersList}. {shortActionText}`, + defaultMessage: `CCR read exceptions alert is firing for the following remote cluster: {remoteClustersList}. {shortActionText}`, values: { remoteClustersList, shortActionText, @@ -268,7 +268,7 @@ export class CCRReadExceptionsAlert extends BaseAlert { const internalFullMessage = i18n.translate( 'xpack.monitoring.alerts.ccrReadExceptions.firing.internalFullMessage', { - defaultMessage: `CCR read exceptions alert is firing for the following remote clusters: {remoteClustersList}. Current 'follower_index' indices are affected: {followerIndicesList}. {action}`, + defaultMessage: `CCR read exceptions alert is firing for the following remote cluster: {remoteClustersList}. Current 'follower_index' index affected: {followerIndicesList}. {action}`, values: { action, remoteClustersList, diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 1c39d6d6b9629..68d0f7b1e0ce7 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -52,7 +52,7 @@ describe('ClusterHealthAlert', () => { description: 'The full internal message generated by Elastic.', }, { name: 'state', description: 'The current state of the alert.' }, - { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, { name: 'action', description: 'The recommended action for this alert.' }, { name: 'actionPlain', diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts index be10ba15d2674..81c51366cb8aa 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -19,11 +19,11 @@ jest.mock('../lib/alerts/fetch_cpu_usage_node_stats', () => ({ jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); - jest.mock('../static_globals', () => ({ Globals: { app: { getLogger: () => ({ debug: jest.fn() }), + url: 'http://localhost:5601', config: { ui: { ccs: { enabled: true }, @@ -43,8 +43,7 @@ describe('CpuUsageAlert', () => { expect(alert.alertOptions.throttle).toBe('1d'); expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); expect(alert.alertOptions.actionVariables).toStrictEqual([ - { name: 'nodes', description: 'The list of nodes reporting high cpu usage.' }, - { name: 'count', description: 'The number of nodes reporting high cpu usage.' }, + { name: 'node', description: 'The node reporting high cpu usage.' }, { name: 'internalShortMessage', description: 'The short internal message generated by Elastic.', @@ -54,7 +53,7 @@ describe('CpuUsageAlert', () => { description: 'The full internal message generated by Elastic.', }, { name: 'state', description: 'The current state of the alert.' }, - { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, { name: 'action', description: 'The recommended action for this alert.' }, { name: 'actionPlain', @@ -140,8 +139,7 @@ describe('CpuUsageAlert', () => { ui: { isFiring: true, message: { - text: - 'Node #start_linkmyNodeName#end_link is reporting cpu usage of 91% at #absolute', + text: `Node #start_link${nodeName}#end_link is reporting cpu usage of ${cpuUsage}% at #absolute`, nextSteps: [ { text: '#start_linkCheck hot threads#end_link', @@ -192,13 +190,14 @@ describe('CpuUsageAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. [View nodes](elasticsearch/nodes)`, - internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, - action: `[View nodes](elasticsearch/nodes)`, - actionPlain: 'Verify CPU levels across affected nodes.', + internalFullMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify CPU level of node.', clusterName, count, nodes: `${nodeName}:${cpuUsage}`, + node: `${nodeName}:${cpuUsage}`, state: 'firing', }); }); @@ -242,13 +241,14 @@ describe('CpuUsageAlert', () => { } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. [View nodes](elasticsearch/nodes)`, - internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, - action: `[View nodes](elasticsearch/nodes)`, - actionPlain: 'Verify CPU levels across affected nodes.', + internalFullMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + internalShortMessage: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, + actionPlain: 'Verify CPU level of node.', clusterName, count, nodes: `${nodeName}:${cpuUsage}`, + node: `${nodeName}:${cpuUsage}`, state: 'firing', }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 438d350d366f8..44b3cb306bd9f 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -51,15 +51,9 @@ export class CpuUsageAlert extends BaseAlert { }, actionVariables: [ { - name: 'nodes', - description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.nodes', { - defaultMessage: 'The list of nodes reporting high cpu usage.', - }), - }, - { - name: 'count', - description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.count', { - defaultMessage: 'The number of nodes reporting high cpu usage.', + name: 'node', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.node', { + defaultMessage: 'The node reporting high cpu usage.', }), }, ...Object.values(AlertingDefaults.ALERT_TYPE.context), @@ -170,51 +164,58 @@ export class CpuUsageAlert extends BaseAlert { if (alertStates.length === 0) { return; } - - const firingNodes = alertStates.filter( - (alertState) => alertState.ui.isFiring - ) as AlertCpuUsageState[]; - const firingCount = firingNodes.length; - if (firingCount > 0) { - const shortActionText = i18n.translate('xpack.monitoring.alerts.cpuUsage.shortAction', { - defaultMessage: 'Verify CPU levels across affected nodes.', - }); - const fullActionText = i18n.translate('xpack.monitoring.alerts.cpuUsage.fullAction', { - defaultMessage: 'View nodes', - }); - const action = `[${fullActionText}](elasticsearch/nodes)`; - const internalShortMessage = i18n.translate( - 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', - { - defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - shortActionText, - }, - } - ); - const internalFullMessage = i18n.translate( - 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', - { - defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - action, - }, - } - ); - instance.scheduleActions('default', { - internalShortMessage, - internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, - state: AlertingDefaults.ALERT_STATE.firing, - nodes: firingNodes.map(({ nodeName, cpuUsage }) => `${nodeName}:${cpuUsage}`).toString(), - count: firingCount, - clusterName: cluster.clusterName, - action, - actionPlain: shortActionText, - }); + const firingNode = alertStates[0] as AlertCpuUsageState; + if (!firingNode || !firingNode.ui.isFiring) { + return; } + const shortActionText = i18n.translate('xpack.monitoring.alerts.cpuUsage.shortAction', { + defaultMessage: 'Verify CPU level of node.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.cpuUsage.fullAction', { + defaultMessage: 'View node', + }); + const ccs = firingNode.ccs; + const globalStateLink = this.createGlobalStateLink( + `elasticsearch/nodes/${firingNode.nodeId}`, + cluster.clusterUuid, + ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', + { + defaultMessage: `CPU usage alert is firing for node {nodeName} in cluster: {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + nodeName: firingNode.nodeName, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', + { + defaultMessage: `CPU usage alert is firing for node {nodeName} in cluster: {clusterName}. {action}`, + values: { + clusterName: cluster.clusterName, + nodeName: firingNode.nodeName, + action, + }, + } + ); + instance.scheduleActions('default', { + internalShortMessage, + internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 + */ + nodes: `${firingNode.nodeName}:${firingNode.cpuUsage}`, + count: 1, + node: `${firingNode.nodeName}:${firingNode.cpuUsage}`, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts index 4c40a170e40b4..0514e7b3cb078 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts @@ -36,6 +36,7 @@ jest.mock('../static_globals', () => ({ Globals: { app: { getLogger: () => ({ debug: jest.fn() }), + url: 'http://localhost:5601', config: { ui: { ccs: { enabled: true }, @@ -55,8 +56,7 @@ describe('DiskUsageAlert', () => { expect(alert.alertOptions.throttle).toBe('1d'); expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); expect(alert.alertOptions.actionVariables).toStrictEqual([ - { name: 'nodes', description: 'The list of nodes reporting high disk usage.' }, - { name: 'count', description: 'The number of nodes reporting high disk usage.' }, + { name: 'node', description: 'The node reporting high disk usage.' }, { name: 'internalShortMessage', description: 'The short internal message generated by Elastic.', @@ -66,7 +66,7 @@ describe('DiskUsageAlert', () => { description: 'The full internal message generated by Elastic.', }, { name: 'state', description: 'The current state of the alert.' }, - { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, { name: 'action', description: 'The recommended action for this alert.' }, { name: 'actionPlain', @@ -134,13 +134,14 @@ describe('DiskUsageAlert', () => { } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `Disk usage alert is firing for ${count} node(s) in cluster: ${clusterName}. [View nodes](elasticsearch/nodes)`, - internalShortMessage: `Disk usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify disk usage levels across affected nodes.`, - action: `[View nodes](elasticsearch/nodes)`, - actionPlain: 'Verify disk usage levels across affected nodes.', + internalFullMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify disk usage level of node.', clusterName, count, nodes: `${nodeName}:${diskUsage}`, + node: `${nodeName}:${diskUsage}`, state: 'firing', }); }); @@ -163,13 +164,14 @@ describe('DiskUsageAlert', () => { } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `Disk usage alert is firing for ${count} node(s) in cluster: ${clusterName}. [View nodes](elasticsearch/nodes)`, - internalShortMessage: `Disk usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify disk usage levels across affected nodes.`, - action: `[View nodes](elasticsearch/nodes)`, - actionPlain: 'Verify disk usage levels across affected nodes.', + internalFullMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + internalShortMessage: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/myNodeId?_g=(cluster_uuid:abc123,ccs:testCluster))`, + actionPlain: 'Verify disk usage level of node.', clusterName, count, nodes: `${nodeName}:${diskUsage}`, + node: `${nodeName}:${diskUsage}`, state: 'firing', }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts index 8eb36f322168c..ac7829d121a3a 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts @@ -50,15 +50,9 @@ export class DiskUsageAlert extends BaseAlert { }, actionVariables: [ { - name: 'nodes', - description: i18n.translate('xpack.monitoring.alerts.diskUsage.actionVariables.nodes', { - defaultMessage: 'The list of nodes reporting high disk usage.', - }), - }, - { - name: 'count', - description: i18n.translate('xpack.monitoring.alerts.diskUsage.actionVariables.count', { - defaultMessage: 'The number of nodes reporting high disk usage.', + name: 'node', + description: i18n.translate('xpack.monitoring.alerts.diskUsage.actionVariables.node', { + defaultMessage: 'The node reporting high disk usage.', }), }, ...Object.values(AlertingDefaults.ALERT_TYPE.context), @@ -174,53 +168,63 @@ export class DiskUsageAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - const firingNodes = alertStates.filter( - (alertState) => alertState.ui.isFiring - ) as AlertDiskUsageState[]; - const firingCount = firingNodes.length; - - if (firingCount > 0) { - const shortActionText = i18n.translate('xpack.monitoring.alerts.diskUsage.shortAction', { - defaultMessage: 'Verify disk usage levels across affected nodes.', - }); - const fullActionText = i18n.translate('xpack.monitoring.alerts.diskUsage.fullAction', { - defaultMessage: 'View nodes', - }); + if (alertStates.length === 0) { + return; + } + const firingNode = alertStates[0] as AlertDiskUsageState; + if (!firingNode || !firingNode.ui.isFiring) { + return; + } - const action = `[${fullActionText}](elasticsearch/nodes)`; - const internalShortMessage = i18n.translate( - 'xpack.monitoring.alerts.diskUsage.firing.internalShortMessage', - { - defaultMessage: `Disk usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - shortActionText, - }, - } - ); - const internalFullMessage = i18n.translate( - 'xpack.monitoring.alerts.diskUsage.firing.internalFullMessage', - { - defaultMessage: `Disk usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - action, - }, - } - ); + const shortActionText = i18n.translate('xpack.monitoring.alerts.diskUsage.shortAction', { + defaultMessage: 'Verify disk usage level of node.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.diskUsage.fullAction', { + defaultMessage: 'View node', + }); + const ccs = firingNode.ccs; + const globalStateLink = this.createGlobalStateLink( + `elasticsearch/nodes/${firingNode.nodeId}`, + cluster.clusterUuid, + ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.diskUsage.firing.internalShortMessage', + { + defaultMessage: `Disk usage alert is firing for node {nodeName} in cluster: {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + nodeName: firingNode.nodeName, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.diskUsage.firing.internalFullMessage', + { + defaultMessage: `Disk usage alert is firing for node {nodeName} in cluster: {clusterName}. {action}`, + values: { + clusterName: cluster.clusterName, + nodeName: firingNode.nodeName, + action, + }, + } + ); - instance.scheduleActions('default', { - internalShortMessage, - internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, - state: AlertingDefaults.ALERT_STATE.firing, - nodes: firingNodes.map((state) => `${state.nodeName}:${state.diskUsage}`).join(','), - count: firingCount, - clusterName: cluster.clusterName, - action, - actionPlain: shortActionText, - }); - } + instance.scheduleActions('default', { + internalShortMessage, + internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 + */ + nodes: `${firingNode.nodeName}:${firingNode.diskUsage}`, + count: 1, + node: `${firingNode.nodeName}:${firingNode.diskUsage}`, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 2bd67298e7b5a..3ac15d795d027 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -56,7 +56,7 @@ describe('ElasticsearchVersionMismatchAlert', () => { description: 'The full internal message generated by Elastic.', }, { name: 'state', description: 'The current state of the alert.' }, - { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, { name: 'action', description: 'The recommended action for this alert.' }, { name: 'actionPlain', diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts index a6a101bc42afa..75e22fb41025c 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts @@ -35,7 +35,6 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -const MAX_INDICES_LIST = 10; export class LargeShardSizeAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { @@ -45,11 +44,11 @@ export class LargeShardSizeAlert extends BaseAlert { defaultParams: { indexPattern: '-.*', threshold: 55 }, actionVariables: [ { - name: 'shardIndices', + name: 'shardIndex', description: i18n.translate( 'xpack.monitoring.alerts.shardSize.actionVariables.shardIndex', { - defaultMessage: 'List of indices which are experiencing large average shard size.', + defaultMessage: 'The index experiencing large average shard size.', } ), }, @@ -166,23 +165,11 @@ export class LargeShardSizeAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - let sortedAlertStates = alertStates.slice(0).sort((alertStateA, alertStateB) => { - const { meta: metaA } = alertStateA as { meta?: IndexShardSizeUIMeta }; - const { meta: metaB } = alertStateB as { meta?: IndexShardSizeUIMeta }; - return metaB!.shardSize - metaA!.shardSize; - }); - - let suffix = ''; - if (sortedAlertStates.length > MAX_INDICES_LIST) { - const diff = sortedAlertStates.length - MAX_INDICES_LIST; - sortedAlertStates = sortedAlertStates.slice(0, MAX_INDICES_LIST); - suffix = `, and ${diff} more`; + if (alertStates.length === 0) { + return; } - - const shardIndices = - sortedAlertStates - .map((alertState) => (alertState.meta as IndexShardSizeUIMeta).shardIndex) - .join(', ') + suffix; + const shardIndexMeta = alertStates[0].meta as IndexShardSizeUIMeta; + const { shardIndex } = shardIndexMeta; const shortActionText = i18n.translate('xpack.monitoring.alerts.shardSize.shortAction', { defaultMessage: 'Investigate indices with large shard sizes.', @@ -193,7 +180,7 @@ export class LargeShardSizeAlert extends BaseAlert { const ccs = alertStates.find((state) => state.ccs)?.ccs; const globalStateLink = this.createGlobalStateLink( - 'elasticsearch/indices', + `elasticsearch/indices/${shardIndex}`, cluster.clusterUuid, ccs ); @@ -202,9 +189,9 @@ export class LargeShardSizeAlert extends BaseAlert { const internalShortMessage = i18n.translate( 'xpack.monitoring.alerts.shardSize.firing.internalShortMessage', { - defaultMessage: `Large shard size alert is firing for the following indices: {shardIndices}. {shortActionText}`, + defaultMessage: `Large shard size alert is firing for the following index: {shardIndex}. {shortActionText}`, values: { - shardIndices, + shardIndex, shortActionText, }, } @@ -212,10 +199,10 @@ export class LargeShardSizeAlert extends BaseAlert { const internalFullMessage = i18n.translate( 'xpack.monitoring.alerts.shardSize.firing.internalFullMessage', { - defaultMessage: `Large shard size alert is firing for the following indices: {shardIndices}. {action}`, + defaultMessage: `Large shard size alert is firing for the following index: {shardIndex}. {action}`, values: { action, - shardIndices, + shardIndex, }, } ); @@ -224,7 +211,7 @@ export class LargeShardSizeAlert extends BaseAlert { internalShortMessage, internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - shardIndices, + shardIndex, clusterName: cluster.clusterName, action, actionPlain: shortActionText, diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index 7c73c63f293f3..9f0096a7e2981 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -57,7 +57,7 @@ describe('LogstashVersionMismatchAlert', () => { description: 'The full internal message generated by Elastic.', }, { name: 'state', description: 'The current state of the alert.' }, - { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, { name: 'action', description: 'The recommended action for this alert.' }, { name: 'actionPlain', diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts new file mode 100644 index 0000000000000..4076eff956ee9 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts @@ -0,0 +1,290 @@ +/* + * 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 { MemoryUsageAlert } from './memory_usage_alert'; +import { ALERT_MEMORY_USAGE } from '../../common/constants'; +import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_memory_usage_node_stats', () => ({ + fetchMemoryUsageNodeStats: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); +jest.mock('../static_globals', () => ({ + Globals: { + app: { + getLogger: () => ({ debug: jest.fn() }), + url: 'http://localhost:5601', + config: { + ui: { + ccs: { enabled: true }, + metricbeat: { index: 'metricbeat-*' }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('MemoryUsageAlert', () => { + it('should have defaults', () => { + const alert = new MemoryUsageAlert(); + expect(alert.alertOptions.id).toBe(ALERT_MEMORY_USAGE); + expect(alert.alertOptions.name).toBe('Memory Usage (JVM)'); + expect(alert.alertOptions.throttle).toBe('1d'); + expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); + expect(alert.alertOptions.actionVariables).toStrictEqual([ + { name: 'node', description: 'The node reporting high memory usage.' }, + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const nodeId = 'myNodeId'; + const nodeName = 'myNodeName'; + const memoryUsage = 91; + const stat = { + clusterUuid, + nodeId, + nodeName, + memoryUsage, + }; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => { + return [stat]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new MemoryUsageAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + params: alert.alertOptions.defaultParams, + } as any); + const count = 1; + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + ccs: undefined, + cluster: { clusterUuid, clusterName }, + memoryUsage, + itemLabel: undefined, + meta: { + clusterUuid, + memoryUsage, + nodeId, + nodeName, + }, + nodeId, + nodeName, + ui: { + isFiring: true, + message: { + text: `Node #start_link${nodeName}#end_link is reporting JVM memory usage of ${memoryUsage}% at #absolute`, + nextSteps: [ + { + text: '#start_linkTune thread pools#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html', + }, + ], + }, + { + text: '#start_linkManaging ES Heap#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: '{elasticWebsiteUrl}blog/a-heap-of-trouble', + }, + ], + }, + { + text: '#start_linkIdentify large indices/shards#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/indices', + }, + ], + }, + { + text: '#start_linkAdd more data nodes#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html', + }, + ], + }, + { + text: '#start_linkResize your deployment (ECE)#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html', + }, + ], + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/nodes/myNodeId', + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify memory usage level of node.', + clusterName, + count, + nodes: `${nodeName}:${memoryUsage}.00`, + node: `${nodeName}:${memoryUsage}.00`, + state: 'firing', + }); + }); + + it('should not fire actions if under threshold', async () => { + (fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + memoryUsage: 1, + }, + ]; + }); + const alert = new MemoryUsageAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [], + }); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + ccs, + }, + ]; + }); + const alert = new MemoryUsageAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + params: alert.alertOptions.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + internalShortMessage: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, + actionPlain: 'Verify memory usage level of node.', + clusterName, + count, + nodes: `${nodeName}:${memoryUsage}.00`, + node: `${nodeName}:${memoryUsage}.00`, + state: 'firing', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts index 06cd90ca80729..86357e7b6f0ed 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts @@ -51,15 +51,9 @@ export class MemoryUsageAlert extends BaseAlert { }, actionVariables: [ { - name: 'nodes', - description: i18n.translate('xpack.monitoring.alerts.memoryUsage.actionVariables.nodes', { - defaultMessage: 'The list of nodes reporting high memory usage.', - }), - }, - { - name: 'count', - description: i18n.translate('xpack.monitoring.alerts.memoryUsage.actionVariables.count', { - defaultMessage: 'The number of nodes reporting high memory usage.', + name: 'node', + description: i18n.translate('xpack.monitoring.alerts.memoryUsage.actionVariables.node', { + defaultMessage: 'The node reporting high memory usage.', }), }, ...Object.values(AlertingDefaults.ALERT_TYPE.context), @@ -180,61 +174,64 @@ export class MemoryUsageAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - const firingNodes = alertStates.filter( - (alertState) => alertState.ui.isFiring - ) as AlertMemoryUsageState[]; - const firingCount = firingNodes.length; + if (alertStates.length === 0) { + return; + } + const firingNode = alertStates[0] as AlertMemoryUsageState; + if (!firingNode || !firingNode.ui.isFiring) { + return; + } - if (firingCount > 0) { - const shortActionText = i18n.translate('xpack.monitoring.alerts.memoryUsage.shortAction', { - defaultMessage: 'Verify memory usage levels across affected nodes.', - }); - const fullActionText = i18n.translate('xpack.monitoring.alerts.memoryUsage.fullAction', { - defaultMessage: 'View nodes', - }); + const shortActionText = i18n.translate('xpack.monitoring.alerts.memoryUsage.shortAction', { + defaultMessage: 'Verify memory usage level of node.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.memoryUsage.fullAction', { + defaultMessage: 'View node', + }); - const ccs = alertStates.find((state) => state.ccs)?.ccs; - const globalStateLink = this.createGlobalStateLink( - 'elasticsearch/nodes', - cluster.clusterUuid, - ccs - ); - const action = `[${fullActionText}](${globalStateLink})`; - const internalShortMessage = i18n.translate( - 'xpack.monitoring.alerts.memoryUsage.firing.internalShortMessage', - { - defaultMessage: `Memory usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - shortActionText, - }, - } - ); - const internalFullMessage = i18n.translate( - 'xpack.monitoring.alerts.memoryUsage.firing.internalFullMessage', - { - defaultMessage: `Memory usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - action, - }, - } - ); + const ccs = alertStates.find((state) => state.ccs)?.ccs; + const globalStateLink = this.createGlobalStateLink( + `elasticsearch/nodes/${firingNode.nodeId}`, + cluster.clusterUuid, + ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.memoryUsage.firing.internalShortMessage', + { + defaultMessage: `Memory usage alert is firing for node {nodeName} in cluster: {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + nodeName: firingNode.nodeName, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.memoryUsage.firing.internalFullMessage', + { + defaultMessage: `Memory usage alert is firing for node {nodeName} in cluster: {clusterName}. {action}`, + values: { + clusterName: cluster.clusterName, + nodeName: firingNode.nodeName, + action, + }, + } + ); - instance.scheduleActions('default', { - internalShortMessage, - internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, - state: AlertingDefaults.ALERT_STATE.firing, - nodes: firingNodes - .map((state) => `${state.nodeName}:${state.memoryUsage.toFixed(2)}`) - .join(','), - count: firingCount, - clusterName: cluster.clusterName, - action, - actionPlain: shortActionText, - }); - } + instance.scheduleActions('default', { + internalShortMessage, + internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 + */ + nodes: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`, + count: 1, + node: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts index 87790ee111326..c6bf853b7787c 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts @@ -45,8 +45,7 @@ describe('MissingMonitoringDataAlert', () => { expect(alert.alertOptions.throttle).toBe('6h'); expect(alert.alertOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); expect(alert.alertOptions.actionVariables).toStrictEqual([ - { name: 'nodes', description: 'The list of nodes missing monitoring data.' }, - { name: 'count', description: 'The number of nodes missing monitoring data.' }, + { name: 'node', description: 'The node missing monitoring data.' }, { name: 'internalShortMessage', description: 'The short internal message generated by Elastic.', @@ -56,7 +55,7 @@ describe('MissingMonitoringDataAlert', () => { description: 'The full internal message generated by Elastic.', }, { name: 'state', description: 'The current state of the alert.' }, - { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, { name: 'action', description: 'The recommended action for this alert.' }, { name: 'actionPlain', @@ -181,12 +180,13 @@ describe('MissingMonitoringDataAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `We have not detected any monitoring data for 1 node(s) in cluster: testCluster. [View what monitoring data we do have for these nodes.](http://localhost:5601/app/monitoring#/overview?_g=(cluster_uuid:abc123))`, - internalShortMessage: `We have not detected any monitoring data for 1 node(s) in cluster: testCluster. Verify these nodes are up and running, then double check the monitoring settings.`, - nodes: 'node: esName1', - action: `[View what monitoring data we do have for these nodes.](http://localhost:5601/app/monitoring#/overview?_g=(cluster_uuid:abc123))`, + internalFullMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. [View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. Verify the node is up and running, then double check the monitoring settings.`, + nodes: `node: ${nodeName}`, + node: `node: ${nodeName}`, + action: `[View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: - 'Verify these nodes are up and running, then double check the monitoring settings.', + 'Verify the node is up and running, then double check the monitoring settings.', clusterName, count, state: 'firing', @@ -234,12 +234,13 @@ describe('MissingMonitoringDataAlert', () => { } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `We have not detected any monitoring data for 1 node(s) in cluster: testCluster. [View what monitoring data we do have for these nodes.](http://localhost:5601/app/monitoring#/overview?_g=(cluster_uuid:abc123,ccs:testCluster))`, - internalShortMessage: `We have not detected any monitoring data for 1 node(s) in cluster: testCluster. Verify these nodes are up and running, then double check the monitoring settings.`, - nodes: 'node: esName1', - action: `[View what monitoring data we do have for these nodes.](http://localhost:5601/app/monitoring#/overview?_g=(cluster_uuid:abc123,ccs:testCluster))`, + internalFullMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. [View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + internalShortMessage: `We have not detected any monitoring data for node ${nodeName} in cluster: ${clusterName}. Verify the node is up and running, then double check the monitoring settings.`, + nodes: `node: ${nodeName}`, + node: `node: ${nodeName}`, + action: `[View what monitoring data we do have for this node.](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, actionPlain: - 'Verify these nodes are up and running, then double check the monitoring settings.', + 'Verify the node is up and running, then double check the monitoring settings.', clusterName, count, state: 'firing', diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts index ed35f775b249c..77581df2303bb 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -17,6 +17,7 @@ import { AlertMessageTimeToken, CommonAlertParams, CommonAlertFilter, + AlertNodeState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { @@ -50,15 +51,9 @@ export class MissingMonitoringDataAlert extends BaseAlert { throttle: '6h', actionVariables: [ { - name: 'nodes', - description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.nodes', { - defaultMessage: 'The list of nodes missing monitoring data.', - }), - }, - { - name: 'count', - description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.count', { - defaultMessage: 'The number of nodes missing monitoring data.', + name: 'node', + description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.node', { + defaultMessage: 'The node missing monitoring data.', }), }, ...Object.values(AlertingDefaults.ALERT_TYPE.context), @@ -158,53 +153,64 @@ export class MissingMonitoringDataAlert extends BaseAlert { item: AlertData | null, cluster: AlertCluster ) { - const firingNodes = alertStates.filter((alertState) => alertState.ui.isFiring); - const firingCount = firingNodes.length; + if (alertStates.length === 0) { + return; + } + const firingNode = alertStates[0] as AlertNodeState; + if (!firingNode || !firingNode.ui.isFiring) { + return; + } - if (firingCount > 0) { - const shortActionText = i18n.translate('xpack.monitoring.alerts.missingData.shortAction', { - defaultMessage: - 'Verify these nodes are up and running, then double check the monitoring settings.', - }); - const fullActionText = i18n.translate('xpack.monitoring.alerts.missingData.fullAction', { - defaultMessage: 'View what monitoring data we do have for these nodes.', - }); + const shortActionText = i18n.translate('xpack.monitoring.alerts.missingData.shortAction', { + defaultMessage: + 'Verify the node is up and running, then double check the monitoring settings.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.missingData.fullAction', { + defaultMessage: 'View what monitoring data we do have for this node.', + }); - const ccs = alertStates.find((state) => state.ccs)?.ccs; - const globalStateLink = this.createGlobalStateLink('overview', cluster.clusterUuid, ccs); - const action = `[${fullActionText}](${globalStateLink})`; - const internalShortMessage = i18n.translate( - 'xpack.monitoring.alerts.missingData.firing.internalShortMessage', - { - defaultMessage: `We have not detected any monitoring data for {count} node(s) in cluster: {clusterName}. {shortActionText}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - shortActionText, - }, - } - ); - const internalFullMessage = i18n.translate( - 'xpack.monitoring.alerts.missingData.firing.internalFullMessage', - { - defaultMessage: `We have not detected any monitoring data for {count} node(s) in cluster: {clusterName}. {action}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - action, - }, - } - ); - instance.scheduleActions('default', { - internalShortMessage, - internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, - state: AlertingDefaults.ALERT_STATE.firing, - nodes: firingNodes.map((state) => `node: ${state.nodeName}`).toString(), - count: firingCount, - clusterName: cluster.clusterName, - action, - actionPlain: shortActionText, - }); - } + const ccs = alertStates.find((state) => state.ccs)?.ccs; + const globalStateLink = this.createGlobalStateLink( + `elasticsearch/nodes/${firingNode.nodeId}`, + cluster.clusterUuid, + ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.missingData.firing.internalShortMessage', + { + defaultMessage: `We have not detected any monitoring data for node {nodeName} in cluster: {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + nodeName: firingNode.nodeName, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.missingData.firing.internalFullMessage', + { + defaultMessage: `We have not detected any monitoring data for node {nodeName} in cluster: {clusterName}. {action}`, + values: { + clusterName: cluster.clusterName, + nodeName: firingNode.nodeName, + action, + }, + } + ); + instance.scheduleActions('default', { + internalShortMessage, + internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 + */ + nodes: `node: ${firingNode.nodeName}`, + count: 1, + node: `node: ${firingNode.nodeName}`, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index fa97de364d792..d628c1c30a7e1 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -61,7 +61,7 @@ describe('NodesChangedAlert', () => { description: 'The full internal message generated by Elastic.', }, { name: 'state', description: 'The current state of the alert.' }, - { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, { name: 'action', description: 'The recommended action for this alert.' }, { name: 'actionPlain', @@ -81,7 +81,7 @@ describe('NodesChangedAlert', () => { const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const nodes = [ + const nodesChanged = [ { recentNodes: [ { @@ -101,6 +101,36 @@ describe('NodesChangedAlert', () => { ccs, }, ]; + const nodesAddedChangedRemoved = [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + { + nodeUuid: 'newNodeId', + nodeEphemeralId: 'newNodeEmpheralId', + nodeName: 'newNodeName', + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + { + nodeUuid: 'removedNodeId', + nodeEphemeralId: 'removedNodeEmpheralId', + nodeName: 'removedNodeName', + }, + ], + clusterUuid, + ccs, + }, + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -122,9 +152,6 @@ describe('NodesChangedAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { - return nodes; - }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; }); @@ -137,7 +164,10 @@ describe('NodesChangedAlert', () => { getState.mockReset(); }); - it('should fire actions', async () => { + it('should fire actions when nodes change', async () => { + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return nodesChanged; + }); const alert = new NodesChangedAlert(); const type = alert.getAlertType(); await type.executor({ @@ -197,6 +227,80 @@ describe('NodesChangedAlert', () => { state: 'firing', }); }); + it('should fire actions when nodes added, changed, and removed', async () => { + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return nodesAddedChangedRemoved; + }); + const alert = new NodesChangedAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + { + nodeUuid: 'newNodeId', + nodeEphemeralId: 'newNodeEmpheralId', + nodeName: 'newNodeName', + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + { + nodeUuid: 'removedNodeId', + nodeEphemeralId: 'removedNodeEmpheralId', + nodeName: 'removedNodeName', + }, + ], + }, + ui: { + isFiring: true, + message: { + text: + "Elasticsearch nodes 'newNodeName' added to this cluster. Elasticsearch nodes 'removedNodeName' removed from this cluster. Elasticsearch nodes 'test' restarted in this cluster.", + }, + severity: 'warning', + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: '[View nodes](elasticsearch/nodes)', + actionPlain: 'Verify that you added, removed, or restarted nodes.', + internalFullMessage: + 'Nodes changed alert is firing for testCluster. The following Elasticsearch nodes have been added:newNodeName removed:removedNodeName restarted:test. [View nodes](elasticsearch/nodes)', + internalShortMessage: + 'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.', + added: 'newNodeName', + removed: 'removedNodeName', + restarted: 'test', + clusterName, + state: 'firing', + }); + }); it('should not fire actions if no nodes have changed', async () => { (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts index bb91418fc2090..3b11d3464a215 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts @@ -17,6 +17,8 @@ import { AlertMessageLinkToken, ThreadPoolRejectionsAlertParams, CommonAlertFilter, + AlertState, + AlertThreadPoolRejectionsStats, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; @@ -34,11 +36,11 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { protected static createActionVariables(type: string) { return [ { - name: 'count', + name: 'node', description: i18n.translate( - 'xpack.monitoring.alerts.threadPoolRejections.actionVariables.count', + 'xpack.monitoring.alerts.threadPoolRejections.actionVariables.node', { - defaultMessage: 'The number of nodes reporting high thread pool {type} rejections.', + defaultMessage: 'The node reporting high thread pool {type} rejections.', values: { type }, } ), @@ -88,11 +90,10 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { ); return stats.map((stat) => { - const { clusterUuid, rejectionCount, ccs } = stat; + const { clusterUuid, ccs } = stat; return { - shouldFire: rejectionCount > threshold, - rejectionCount, + shouldFire: stat.rejectionCount > threshold, severity: AlertSeverity.Danger, meta: stat, clusterUuid, @@ -105,14 +106,19 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { return super.filterAlertInstance(alertInstance, filters, true); } - protected getUiMessage(alertState: AlertThreadPoolRejectionsState): AlertMessage { - const { nodeName, nodeId, rejectionCount } = alertState; + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const { + nodeName, + nodeId, + type: threadPoolType, + rejectionCount, + } = item.meta as AlertThreadPoolRejectionsStats; return { text: i18n.translate('xpack.monitoring.alerts.threadPoolRejections.ui.firingMessage', { - defaultMessage: `Node #start_link{nodeName}#end_link is reporting {rejectionCount} {type} rejections at #absolute`, + defaultMessage: `Node #start_link{nodeName}#end_link is reporting {rejectionCount} {threadPoolType} rejections at #absolute`, values: { nodeName, - type: this.threadPoolType, + threadPoolType, rejectionCount, }, }), @@ -178,20 +184,28 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { ], }; } - protected executeActions( instance: AlertInstance, - alertStates: AlertThreadPoolRejectionsState[], + { alertStates }: { alertStates: AlertState[] }, + item: AlertData | null, cluster: AlertCluster ) { const type = this.threadPoolType; - const count = alertStates.length; const { clusterName: clusterKnownName, clusterUuid } = cluster; const clusterName = clusterKnownName || clusterUuid; + + if (alertStates.length === 0) { + return; + } + const firingNode = alertStates[0] as AlertThreadPoolRejectionsState; + const { nodeName, nodeId } = firingNode; + if (!firingNode || !firingNode.ui.isFiring) { + return; + } const shortActionText = i18n.translate( 'xpack.monitoring.alerts.threadPoolRejections.shortAction', { - defaultMessage: 'Verify thread pool {type} rejections across affected nodes.', + defaultMessage: 'Verify thread pool {type} rejections for the affected node.', values: { type, }, @@ -201,21 +215,25 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { const fullActionText = i18n.translate( 'xpack.monitoring.alerts.threadPoolRejections.fullAction', { - defaultMessage: 'View nodes', + defaultMessage: 'View node', } ); const ccs = alertStates.find((state) => state.ccs)?.ccs; - const globalStateLink = this.createGlobalStateLink('elasticsearch/nodes', clusterUuid, ccs); + const globalStateLink = this.createGlobalStateLink( + `elasticsearch/nodes/${nodeId}`, + cluster.clusterUuid, + ccs + ); const action = `[${fullActionText}](${globalStateLink})`; const internalShortMessage = i18n.translate( 'xpack.monitoring.alerts.threadPoolRejections.firing.internalShortMessage', { - defaultMessage: `Thread pool {type} rejections alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, + defaultMessage: `Thread pool {type} rejections alert is firing for node {nodeName} in cluster: {clusterName}. {shortActionText}`, values: { - count, clusterName, + nodeName, shortActionText, type, }, @@ -224,10 +242,10 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { const internalFullMessage = i18n.translate( 'xpack.monitoring.alerts.threadPoolRejections.firing.internalFullMessage', { - defaultMessage: `Thread pool {type} rejections alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, + defaultMessage: `Thread pool {type} rejections alert is firing for node {nodeName} in cluster: {clusterName}. {action}`, values: { - count, clusterName, + nodeName, action, type, }, @@ -239,7 +257,11 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, threadPoolType: type, state: AlertingDefaults.ALERT_STATE.firing, - count, + /* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544 + see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 + */ + count: 1, + node: nodeName, clusterName, action, actionPlain: shortActionText, diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts new file mode 100644 index 0000000000000..78f3e937016a4 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts @@ -0,0 +1,298 @@ +/* + * 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 { ThreadPoolSearchRejectionsAlert } from './thread_pool_search_rejections_alert'; +import { ALERT_THREAD_POOL_SEARCH_REJECTIONS } from '../../common/constants'; +import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_thread_pool_rejections_stats', () => ({ + fetchThreadPoolRejectionStats: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +jest.mock('../static_globals', () => ({ + Globals: { + app: { + getLogger: () => ({ debug: jest.fn() }), + url: 'http://localhost:5601', + config: { + ui: { + show_license_expiration: true, + ccs: { enabled: true }, + metricbeat: { index: 'metricbeat-*' }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('ThreadpoolSearchRejectionsAlert', () => { + it('should have defaults', () => { + const alert = new ThreadPoolSearchRejectionsAlert(); + expect(alert.alertOptions.id).toBe(ALERT_THREAD_POOL_SEARCH_REJECTIONS); + expect(alert.alertOptions.name).toBe('Thread pool search rejections'); + expect(alert.alertOptions.throttle).toBe('1d'); + expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); + expect(alert.alertOptions.actionVariables).toStrictEqual([ + { name: 'node', description: 'The node reporting high thread pool search rejections.' }, + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const nodeId = 'esNode1'; + const nodeName = 'esName1'; + const threadPoolType = 'search'; + const rejectionCount = 400; + const stat = [ + { + rejectionCount, + type: threadPoolType, + clusterUuid, + nodeId, + nodeName, + ccs: null, + }, + ]; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return stat; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new ThreadPoolSearchRejectionsAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + ccs: null, + cluster: { clusterUuid, clusterName }, + nodeId, + nodeName, + itemLabel: undefined, + meta: { + rejectionCount, + clusterUuid, + type: threadPoolType, + nodeId, + nodeName, + ccs: null, + }, + ui: { + isFiring: true, + message: { + text: `Node #start_link${nodeName}#end_link is reporting ${rejectionCount} ${threadPoolType} rejections at #absolute`, + nextSteps: [ + { + text: '#start_linkMonitor this node#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/nodes/esNode1/advanced', + }, + ], + }, + { + text: '#start_linkOptimize complex queries#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries', + }, + ], + }, + { + text: '#start_linkAdd more nodes#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html', + }, + ], + }, + { + text: '#start_linkResize your deployment (ECE)#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html', + }, + ], + }, + { + text: '#start_linkThread pool settings#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html', + }, + ], + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: `elasticsearch/nodes/${nodeId}`, + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`, + node: `${nodeName}`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`, + clusterName, + count: 1, + threadPoolType, + state: 'firing', + }); + }); + it('should not fire actions if under threshold', async () => { + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat[0], + rejectionCount: 1, + }, + ]; + }); + const alert = new ThreadPoolSearchRejectionsAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [], + }); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat[0], + ccs, + }, + ]; + }); + const alert = new ThreadPoolSearchRejectionsAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + internalShortMessage: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`, + node: `${nodeName}`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/esNode1?_g=(cluster_uuid:abc123,ccs:testCluster))`, + actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`, + clusterName, + count, + state: 'firing', + threadPoolType, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts new file mode 100644 index 0000000000000..83ba6fc7532a3 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts @@ -0,0 +1,298 @@ +/* + * 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 { ThreadPoolWriteRejectionsAlert } from './thread_pool_write_rejections_alert'; +import { ALERT_THREAD_POOL_WRITE_REJECTIONS } from '../../common/constants'; +import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_thread_pool_rejections_stats', () => ({ + fetchThreadPoolRejectionStats: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +jest.mock('../static_globals', () => ({ + Globals: { + app: { + getLogger: () => ({ debug: jest.fn() }), + url: 'http://localhost:5601', + config: { + ui: { + show_license_expiration: true, + ccs: { enabled: true }, + metricbeat: { index: 'metricbeat-*' }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('ThreadpoolWriteRejectionsAlert', () => { + it('should have defaults', () => { + const alert = new ThreadPoolWriteRejectionsAlert(); + expect(alert.alertOptions.id).toBe(ALERT_THREAD_POOL_WRITE_REJECTIONS); + expect(alert.alertOptions.name).toBe(`Thread pool write rejections`); + expect(alert.alertOptions.throttle).toBe('1d'); + expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); + expect(alert.alertOptions.actionVariables).toStrictEqual([ + { name: 'node', description: 'The node reporting high thread pool write rejections.' }, + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'clusterName', description: 'The cluster to which the node(s) belongs.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const nodeId = 'esNode1'; + const nodeName = 'esName1'; + const threadPoolType = 'write'; + const rejectionCount = 400; + const stat = [ + { + rejectionCount, + type: threadPoolType, + clusterUuid, + nodeId, + nodeName, + ccs: null, + }, + ]; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return stat; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new ThreadPoolWriteRejectionsAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + ccs: null, + cluster: { clusterUuid, clusterName }, + nodeId, + nodeName, + itemLabel: undefined, + meta: { + rejectionCount, + clusterUuid, + type: threadPoolType, + nodeId, + nodeName, + ccs: null, + }, + ui: { + isFiring: true, + message: { + text: `Node #start_link${nodeName}#end_link is reporting ${rejectionCount} ${threadPoolType} rejections at #absolute`, + nextSteps: [ + { + text: '#start_linkMonitor this node#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/nodes/esNode1/advanced', + }, + ], + }, + { + text: '#start_linkOptimize complex queries#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries', + }, + ], + }, + { + text: '#start_linkAdd more nodes#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html', + }, + ], + }, + { + text: '#start_linkResize your deployment (ECE)#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html', + }, + ], + }, + { + text: '#start_linkThread pool settings#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html', + }, + ], + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: `elasticsearch/nodes/${nodeId}`, + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`, + node: `${nodeName}`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`, + clusterName, + count: 1, + threadPoolType, + state: 'firing', + }); + }); + it('should not fire actions if under threshold', async () => { + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat[0], + rejectionCount: 1, + }, + ]; + }); + const alert = new ThreadPoolWriteRejectionsAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [], + }); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat[0], + ccs, + }, + ]; + }); + const alert = new ThreadPoolWriteRejectionsAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. [View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/${nodeId}?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + internalShortMessage: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`, + node: `${nodeName}`, + action: `[View node](http://localhost:5601/app/monitoring#/elasticsearch/nodes/esNode1?_g=(cluster_uuid:abc123,ccs:testCluster))`, + actionPlain: `Verify thread pool ${threadPoolType} rejections for the affected node.`, + clusterName, + count, + state: 'firing', + threadPoolType, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 620f32014dae6..a7b2457130744 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -49,7 +49,9 @@ export async function fetchStatus( if (!states) { return result; } - + // puts all alert states associated with this rule into a flat array. this works with both the legacy alert + // of having multiple alert states per alert, each representing a firing node, and the current alert where each firing + // node is an alert with a single alert state, the node itself. https://github.com/elastic/kibana/pull/102544 result.states = Object.values(states).reduce((accum: CommonAlertState[], instance: any) => { const alertInstanceState = instance.state as AlertInstanceState; if (!alertInstanceState.alertStates) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ca2be10624e66..90bcb2cb01362 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15712,11 +15712,7 @@ "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearchクラスターの正常性は{health}です。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}. #start_linkView now#end_link", "xpack.monitoring.alerts.clusterHealth.yellowMessage": "見つからないレプリカシャードを割り当て", - "xpack.monitoring.alerts.cpuUsage.actionVariables.count": "高CPU使用率を報告しているノード数。", - "xpack.monitoring.alerts.cpuUsage.actionVariables.nodes": "高 CPU 使用率を報告しているノードのリスト。", "xpack.monitoring.alerts.cpuUsage.description": "ノードの CPU 負荷が常に高いときにアラートを発行します。", - "xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage": "CPU 使用状況アラートは、クラスター {clusterName} の {count} 個のノードで実行されています。{action}", - "xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage": "CPU使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{shortActionText}", "xpack.monitoring.alerts.cpuUsage.fullAction": "ノードの表示", "xpack.monitoring.alerts.cpuUsage.label": "CPU使用状況", "xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label": "平均を確認", @@ -15725,11 +15721,7 @@ "xpack.monitoring.alerts.cpuUsage.ui.firingMessage": "ノード#start_link{nodeName}#end_linkは、#absoluteでCPU使用率{cpuUsage}%を報告しています", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.hotThreads": "#start_linkCheck hot threads#end_link", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.runningTasks": "#start_linkCheck long running tasks#end_link", - "xpack.monitoring.alerts.diskUsage.actionVariables.count": "高ディスク使用率を報告しているノード数。", - "xpack.monitoring.alerts.diskUsage.actionVariables.nodes": "高ディスク使用率を報告しているノードのリスト。", "xpack.monitoring.alerts.diskUsage.description": "ノードのディスク使用率が常に高いときにアラートを発行します。", - "xpack.monitoring.alerts.diskUsage.firing.internalFullMessage": "ディスク使用状況アラートは、クラスター {clusterName} の {count} 個のノードで実行されています。{action}", - "xpack.monitoring.alerts.diskUsage.firing.internalShortMessage": "ディスク使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{shortActionText}", "xpack.monitoring.alerts.diskUsage.fullAction": "ノードの表示", "xpack.monitoring.alerts.diskUsage.label": "ディスク使用量", "xpack.monitoring.alerts.diskUsage.paramDetails.duration.label": "平均を確認", @@ -15775,11 +15767,7 @@ "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash バージョン不一致", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Logstash ({versions}) が実行されています。", - "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "高メモリー使用率を報告しているノード数。", - "xpack.monitoring.alerts.memoryUsage.actionVariables.nodes": "高メモリー使用率を報告しているノードのリスト。", "xpack.monitoring.alerts.memoryUsage.description": "ノードが高いメモリ使用率を報告するときにアラートを発行します。", - "xpack.monitoring.alerts.memoryUsage.firing.internalFullMessage": "メモリー使用状況アラートは、クラスター {clusterName} の {count} 個のノードで実行されています。{action}", - "xpack.monitoring.alerts.memoryUsage.firing.internalShortMessage": "メモリー使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{shortActionText}", "xpack.monitoring.alerts.memoryUsage.fullAction": "ノードの表示", "xpack.monitoring.alerts.memoryUsage.label": "メモリー使用状況 (JVM) ", "xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label": "平均を確認", @@ -15792,11 +15780,7 @@ "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.resizeYourDeployment": "#start_linkデプロイのサイズを変更 (ECE) #end_link", "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.tuneThreadPools": "#start_linkスレッドプールの微調整#end_link", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} は必須フィールドです。", - "xpack.monitoring.alerts.missingData.actionVariables.count": "監視データが見つからないノード数。", - "xpack.monitoring.alerts.missingData.actionVariables.nodes": "監視データが見つからないノードのリスト。", "xpack.monitoring.alerts.missingData.description": "監視データが見つからない場合にアラートを発行します。", - "xpack.monitoring.alerts.missingData.firing.internalFullMessage": "クラスター {clusterName} では、{count} 個のノードの監視データが検出されませんでした。{action}", - "xpack.monitoring.alerts.missingData.firing.internalShortMessage": "クラスター {clusterName} では、{count} 個のノードの監視データが検出されませんでした。{shortActionText}", "xpack.monitoring.alerts.missingData.fullAction": "これらのノードに関連する監視データを表示します。", "xpack.monitoring.alerts.missingData.label": "見つからない監視データ", "xpack.monitoring.alerts.missingData.paramDetails.duration.label": "最後の監視データが見つからない場合に通知", @@ -15828,8 +15812,6 @@ "xpack.monitoring.alerts.searchThreadPoolRejections.description": "検索スレッドプールの拒否数がしきい値を超過するときにアラートを発行します。", "xpack.monitoring.alerts.shardSize.actionVariables.shardIndex": "大きい平均シャードサイズが発生しているインデックスのリスト。", "xpack.monitoring.alerts.shardSize.description": "平均シャードサイズが構成されたしきい値よりも大きい場合にアラートが発生します。", - "xpack.monitoring.alerts.shardSize.firing.internalFullMessage": "次のインデックスに対して大きいシャードサイズのアラートが発行されています。{shardIndices}。{action}", - "xpack.monitoring.alerts.shardSize.firing.internalShortMessage": "次のインデックスに対して大きいシャードサイズのアラートが発行されています。{shardIndices}。{shortActionText}", "xpack.monitoring.alerts.shardSize.fullAction": "インデックスシャードサイズ統計情報を表示", "xpack.monitoring.alerts.shardSize.label": "シャードサイズ", "xpack.monitoring.alerts.shardSize.paramDetails.indexPattern.label": "次のインデックスパターンを確認", @@ -15846,13 +15828,9 @@ "xpack.monitoring.alerts.status.highSeverityTooltip": "すぐに対処が必要な致命的な問題があります!", "xpack.monitoring.alerts.status.lowSeverityTooltip": "低重要度の問題があります。", "xpack.monitoring.alerts.status.mediumSeverityTooltip": "スタックに影響を及ぼす可能性がある問題があります。", - "xpack.monitoring.alerts.threadPoolRejections.actionVariables.count": "高いスレッドプール {type} 拒否を報告するノード数。", - "xpack.monitoring.alerts.threadPoolRejections.firing.internalFullMessage": "スレッドプール {type} 拒否アラートは、クラスター {clusterName} の {count} 個のノードで実行されています。{action}", - "xpack.monitoring.alerts.threadPoolRejections.firing.internalShortMessage": "スレッドプール {type} 拒否アラートは、クラスター {clusterName} の {count} 個のノードで実行されています。{shortActionText}", "xpack.monitoring.alerts.threadPoolRejections.fullAction": "ノードの表示", "xpack.monitoring.alerts.threadPoolRejections.label": "スレッドプール {type} 拒否", "xpack.monitoring.alerts.threadPoolRejections.shortAction": "影響を受けるノード全体でスレッドプール {type} 拒否を検証します。", - "xpack.monitoring.alerts.threadPoolRejections.ui.firingMessage": "ノード #start_link{nodeName}#end_link は、#absolute で {rejectionCount} {type} 拒否を報告しています", "xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.addMoreNodes": "#start_linkAdd more nodes#end_link", "xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.monitorThisNode": "#start_linkMonitor this node#end_link", "xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.optimizeQueries": "#start_linkOptimize complex queries#end_link", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 341741dd4046a..6b97364aafa6a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15944,11 +15944,7 @@ "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearch 集群运行状况为 {health}。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}。#start_link立即查看#end_link", "xpack.monitoring.alerts.clusterHealth.yellowMessage": "分配缺失的副本分片", - "xpack.monitoring.alerts.cpuUsage.actionVariables.count": "报告高 CPU 使用率的节点数目。", - "xpack.monitoring.alerts.cpuUsage.actionVariables.nodes": "报告高 CPU 使用率的节点列表。", "xpack.monitoring.alerts.cpuUsage.description": "节点的 CPU 负载持续偏高时告警。", - "xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage": "为集群 {clusterName} 中 {count} 个节点触发了 CPU 使用率告警。{action}", - "xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage": "为集群 {clusterName} 中 {count} 个节点触发了 CPU 使用率告警。{shortActionText}", "xpack.monitoring.alerts.cpuUsage.fullAction": "查看节点", "xpack.monitoring.alerts.cpuUsage.label": "CPU 使用率", "xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label": "查看以下期间的平均值:", @@ -15957,11 +15953,7 @@ "xpack.monitoring.alerts.cpuUsage.ui.firingMessage": "节点 #start_link{nodeName}#end_link 于 #absolute报告 cpu 使用率为 {cpuUsage}%", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.hotThreads": "#start_link检查热线程#end_link", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.runningTasks": "#start_link检查长时间运行的任务#end_link", - "xpack.monitoring.alerts.diskUsage.actionVariables.count": "报告高磁盘使用率的节点数目。", - "xpack.monitoring.alerts.diskUsage.actionVariables.nodes": "报告高磁盘使用率的节点列表。", "xpack.monitoring.alerts.diskUsage.description": "节点的磁盘使用率持续偏高时告警。", - "xpack.monitoring.alerts.diskUsage.firing.internalFullMessage": "为集群 {clusterName} 中的 {count} 个节点触发了磁盘使用率告警。{action}", - "xpack.monitoring.alerts.diskUsage.firing.internalShortMessage": "为集群 {clusterName} 中的 {count} 个节点触发了磁盘使用率告警。{shortActionText}", "xpack.monitoring.alerts.diskUsage.fullAction": "查看节点", "xpack.monitoring.alerts.diskUsage.label": "磁盘使用率", "xpack.monitoring.alerts.diskUsage.paramDetails.duration.label": "查看以下期间的平均值:", @@ -16011,11 +16003,7 @@ "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash 版本不匹配", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Logstash 版本 ({versions})。", - "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "报告高内存使用率的节点数目。", - "xpack.monitoring.alerts.memoryUsage.actionVariables.nodes": "报告高内存使用率的节点列表。", "xpack.monitoring.alerts.memoryUsage.description": "节点报告高的内存使用率时告警。", - "xpack.monitoring.alerts.memoryUsage.firing.internalFullMessage": "为集群 {clusterName} 中的 {count} 个节点触发了内存使用率告警。{action}", - "xpack.monitoring.alerts.memoryUsage.firing.internalShortMessage": "为集群 {clusterName} 中的 {count} 个节点触发了内存使用率告警。{shortActionText}", "xpack.monitoring.alerts.memoryUsage.fullAction": "查看节点", "xpack.monitoring.alerts.memoryUsage.label": "内存使用率 (JVM)", "xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label": "查看以下期间的平均值:", @@ -16028,11 +16016,7 @@ "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.resizeYourDeployment": "#start_link对您的部署进行大小调整 (ECE)#end_link", "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.tuneThreadPools": "#start_link调整线程池#end_link", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} 是必填字段。", - "xpack.monitoring.alerts.missingData.actionVariables.count": "缺少监测数据的节点数量。", - "xpack.monitoring.alerts.missingData.actionVariables.nodes": "缺少监测数据的节点列表。", "xpack.monitoring.alerts.missingData.description": "监测数据缺失时告警。", - "xpack.monitoring.alerts.missingData.firing.internalFullMessage": "我们尚未检测到集群 {clusterName} 中 {count} 个节点的任何监测数据。{action}", - "xpack.monitoring.alerts.missingData.firing.internalShortMessage": "我们尚未检测到集群 {clusterName} 中 {count} 个节点的任何监测数据。{shortActionText}", "xpack.monitoring.alerts.missingData.fullAction": "查看我们拥有这些节点的哪些监测数据。", "xpack.monitoring.alerts.missingData.label": "缺少监测数据", "xpack.monitoring.alerts.missingData.paramDetails.duration.label": "缺少以下过去持续时间的监测数据时通知:", @@ -16064,8 +16048,6 @@ "xpack.monitoring.alerts.searchThreadPoolRejections.description": "当搜索线程池中的拒绝数目超过阈值时告警。", "xpack.monitoring.alerts.shardSize.actionVariables.shardIndex": "平均分片大小过大的索引列表。", "xpack.monitoring.alerts.shardSize.description": "平均分片大小大于配置的阈值时告警。", - "xpack.monitoring.alerts.shardSize.firing.internalFullMessage": "以下索引触发分片大小过大告警:{shardIndices}。{action}", - "xpack.monitoring.alerts.shardSize.firing.internalShortMessage": "以下索引触发分片大小过大告警:{shardIndices}。{shortActionText}", "xpack.monitoring.alerts.shardSize.fullAction": "查看索引分片大小统计", "xpack.monitoring.alerts.shardSize.label": "分片大小", "xpack.monitoring.alerts.shardSize.paramDetails.indexPattern.label": "检查以下索引模式", @@ -16082,13 +16064,9 @@ "xpack.monitoring.alerts.status.highSeverityTooltip": "有一些紧急问题需要您立即关注!", "xpack.monitoring.alerts.status.lowSeverityTooltip": "存在一些低紧急问题。", "xpack.monitoring.alerts.status.mediumSeverityTooltip": "有一些问题可能会影响堆栈。", - "xpack.monitoring.alerts.threadPoolRejections.actionVariables.count": "报告高线程池 {type} 拒绝的节点数量。", - "xpack.monitoring.alerts.threadPoolRejections.firing.internalFullMessage": "为集群 {clusterName} 中的 {count} 个节点触发了线程池 {type} 拒绝告警。{action}", - "xpack.monitoring.alerts.threadPoolRejections.firing.internalShortMessage": "为集群 {clusterName} 中的 {count} 个节点触发了线程池 {type} 拒绝告警。{shortActionText}", "xpack.monitoring.alerts.threadPoolRejections.fullAction": "查看节点", "xpack.monitoring.alerts.threadPoolRejections.label": "线程池 {type} 拒绝", "xpack.monitoring.alerts.threadPoolRejections.shortAction": "验证受影响节点的线程池 {type} 拒绝。", - "xpack.monitoring.alerts.threadPoolRejections.ui.firingMessage": "节点 #start_link{nodeName}#end_link 在 #absolute报告了 {rejectionCount} 个 {type} 拒绝", "xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.addMoreNodes": "#start_link添加更多节点#end_link", "xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.monitorThisNode": "#start_link监测此节点#end_link", "xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.optimizeQueries": "#start_link优化复杂查询#end_link", diff --git a/x-pack/test/functional/apps/infra/metrics_anomalies.ts b/x-pack/test/functional/apps/infra/metrics_anomalies.ts index 5b481abae48d7..a1619467fe6bb 100644 --- a/x-pack/test/functional/apps/infra/metrics_anomalies.ts +++ b/x-pack/test/functional/apps/infra/metrics_anomalies.ts @@ -15,7 +15,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraHome']); const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); - // Failing: See https://github.com/elastic/kibana/issues/100445 describe('Metrics UI Anomaly Flyout', function () { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana');